Apache HttpComponents 4.2から、Fluent APIというものが増えています。
Chapter 5. Fluent API
https://hc.apache.org/httpcomponents-client-4.3.x/tutorial/html/fluent.html
こんな感じに書きます(HttpClient Quick Start(https://hc.apache.org/httpcomponents-client-4.3.x/quickstart.html)より)。
// The fluent API relieves the user from having to deal with manual deallocation of system // resources at the cost of having to buffer response content in memory in some cases. Request.Get("http://targethost/homepage") .execute().returnContent(); Request.Post("http://targethost/login") .bodyForm(Form.form().add("username", "vip").add("password", "secret").build()) .execute().returnContent();
コメントを見ていると、リソースの開放はユーザは気にしなくていいよーみたいなことが書かれていますが、HttpClientは過去にこういうのを書いたこともあり、若干リソース周りは心配です。
HttpClientとConnectionManagerとCLOSE_WAITと
http://d.hatena.ne.jp/Kazuhira/20121013/1350118101
なので、ちょっと確認してみましょう。
準備
依存関係の定義。pom.xmlに、以下を追加します。
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.3.3</version> </dependency> <!-- Fluent APIを依存関係に追加する場合は、 httpclientは明示的に追加しなくてもよい --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>fluent-hc</artifactId> <version>4.3.3</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.easytesting</groupId> <artifactId>fest-assert-core</artifactId> <version>2.0M10</version> <scope>test</scope> </dependency>
Fluent APIを使う場合は、追加の依存関係が必要です。まあ、「fluent-hc」を追加すると「httpclient」も引っ張ってくるので要らないのですが、今回はついでということで。
あとは、テスティングフレームワークを追加しています。
以降、テストコードでは以下のimport文が宣言されているものとします。
import static org.fest.assertions.api.Assertions.*; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; // Fluent API import org.apache.http.client.fluent.Request; import org.apache.http.client.fluent.Response; // こちらは、4.3ではすでに非推奨API import org.apache.http.impl.client.DefaultHttpClient; import org.junit.Test;
確認観点
ここはひとつ、先ほど作成したダミーのHTTPサーバを使用して確認してみます。
JDK付属、Undertowを使ったGroovy&Clojure、Perlでの簡単なHTTPサーバ
http://d.hatena.ne.jp/Kazuhira/20140412/1397293467
JDK付属 on Groovyを使います。
$ groovy simple-jdk-httpd.groovy [Sat Apr 12 20:21:20 JST 2014] SimpleJdkHttpd Startup[/0:0:0:0:0:0:0:0:8080]
これに対して、こんな感じのテストコードを書いて
@Test public void testXXXX() throws IOException { for (int i = 0; i < 10; i++) { // この中で、HTTPリクエストを繰り返し実行 } try { Thread.sleep(30 * 1000L); } catch (Exception e) { } }
「mvn test」でテストを実行して、少々スリープさせます。
$ mvn test
この間にソケットの様子を見てみます。
$ netstat -an | grep 8080
では、いってみましょう。
非推奨になったDefaultHttpClient
まずは、今は非推奨となっているDefaultHttpClientを使った例から。
@Test public void testDeprecatedClient() throws IOException { for (int i = 0; i < 10; i++) { DefaultHttpClient client = new DefaultHttpClient(); HttpGet httpGet = new HttpGet("http://localhost:8080/"); try { HttpResponse response = client.execute(httpGet); HttpEntity entity = response.getEntity(); assertThat(response.getStatusLine().toString()) .isEqualTo("HTTP/1.1 200 OK"); assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)) .startsWith("<html>") .contains("<title>タイトル</title>") .contains("<body>本文</body>") .contains("</html>"); } finally { httpGet.releaseConnection(); // こちらでは、クローズされない // client.getConnectionManager().shutdown(); } } try { Thread.sleep(30 * 1000L); } catch (Exception e) { } }
これは、ソケットがリークする例です。
テストを実行すると、こんな感じのソケットが見れます。
$ netstat -an | grep 8080 tcp6 0 0 :::8080 :::* LISTEN tcp6 0 0 127.0.0.1:51550 127.0.0.1:8080 ESTABLISHED tcp6 0 0 127.0.0.1:8080 127.0.0.1:51550 ESTABLISHED tcp6 0 0 127.0.0.1:51545 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:8080 127.0.0.1:51553 ESTABLISHED tcp6 0 0 127.0.0.1:51544 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:8080 127.0.0.1:51549 ESTABLISHED tcp6 0 0 127.0.0.1:51548 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51546 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51553 127.0.0.1:8080 ESTABLISHED tcp6 0 0 127.0.0.1:8080 127.0.0.1:51551 ESTABLISHED tcp6 0 0 127.0.0.1:51552 127.0.0.1:8080 ESTABLISHED tcp6 0 0 127.0.0.1:51549 127.0.0.1:8080 ESTABLISHED tcp6 0 0 127.0.0.1:51551 127.0.0.1:8080 ESTABLISHED tcp6 0 0 127.0.0.1:8080 127.0.0.1:51552 ESTABLISHED tcp6 0 0 127.0.0.1:51547 127.0.0.1:8080 TIME_WAIT
もうしばらく放っておくと、こんな感じになります。
$ netstat -an | grep 8080 tcp6 0 0 :::8080 :::* LISTEN tcp6 1 0 127.0.0.1:51550 127.0.0.1:8080 CLOSE_WAIT tcp6 0 0 127.0.0.1:8080 127.0.0.1:51550 FIN_WAIT2 tcp6 0 0 127.0.0.1:51545 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:8080 127.0.0.1:51553 FIN_WAIT2 tcp6 0 0 127.0.0.1:51544 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:8080 127.0.0.1:51549 FIN_WAIT2 tcp6 0 0 127.0.0.1:51548 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51546 127.0.0.1:8080 TIME_WAIT tcp6 1 0 127.0.0.1:51553 127.0.0.1:8080 CLOSE_WAIT tcp6 0 0 127.0.0.1:8080 127.0.0.1:51551 FIN_WAIT2 tcp6 1 0 127.0.0.1:51552 127.0.0.1:8080 CLOSE_WAIT tcp6 1 0 127.0.0.1:51549 127.0.0.1:8080 CLOSE_WAIT tcp6 1 0 127.0.0.1:51551 127.0.0.1:8080 CLOSE_WAIT tcp6 0 0 127.0.0.1:8080 127.0.0.1:51552 FIN_WAIT2 tcp6 0 0 127.0.0.1:51547 127.0.0.1:8080 TIME_WAIT
今回みたいな場合はクライアントのプロセスが終了するのでそんなに問題になりませんが、サーバプログラムだとこのまま閉じられないソケットが問題になります。
なので、先ほどのfinally句の中をこのように修正すると
} finally { // httpGet.releaseConnection(); // こちらでは、クローズされない client.getConnectionManager().shutdown(); }
キレイに「TIME_WAIT」に全部シフトします。
$ netstat -an | grep 8080 tcp6 0 0 :::8080 :::* LISTEN tcp6 0 0 127.0.0.1:51588 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51585 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51587 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51584 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51586 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51580 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51582 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51579 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51581 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51583 127.0.0.1:8080 TIME_WAIT
CloseableHttpClient
非推奨となったDefaultHttpClient(とその親クラスであるAbstractHttpClient)に代わった、CloseableHttpClientを使用。
@Test public void testCurrentClient() throws IOException { for (int i = 0; i < 10; i++) { HttpGet httpGet = new HttpGet("http://localhost:8080/"); try (CloseableHttpClient client = HttpClients.createDefault(); CloseableHttpResponse response = client.execute(httpGet)) { assertThat(response.getStatusLine().toString()) .isEqualTo("HTTP/1.1 200 OK"); assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)) .startsWith("<html>") .contains("<title>タイトル</title>") .contains("<body>本文</body>") .contains("</html>"); } } try { Thread.sleep(30 * 1000L); } catch (Exception e) { } }
ClientもResponseもCloseableなので、try-with-resourcesで扱えますねー。
ソケットの様子は
$ netstat -an | grep 8080 tcp6 0 0 :::8080 :::* LISTEN tcp6 0 0 127.0.0.1:51779 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51782 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51783 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51788 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51781 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51787 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51784 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51780 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51786 127.0.0.1:8080 TIME_WAIT tcp6 0 0 127.0.0.1:51785 127.0.0.1:8080 TIME_WAIT
という感じ。
これは、普通にcloseでよさそうですね。
Fluent API
で、今度はFluent API。確かに流れるような形で書けて便利そうなのですが、どうなんでしょう?
というわけで、まずは簡単に書いてみます。
@Test public void testFluentClientSimple() throws IOException { for (int i = 0; i < 10; i++) { String responseString = Request .Get("http://localhost:8080/") .execute() .returnContent() .asString(); assertThat(responseString) .startsWith("<html>") .contains("<title>タイトル</title>") .contains("<body>本文</body>") .contains("</html>"); } try { Thread.sleep(30 * 1000L); } catch (Exception e) { } }
実行中のソケットの様子。
$ netstat -an | grep 8080 tcp6 0 0 :::8080 :::* LISTEN tcp6 0 0 127.0.0.1:52135 127.0.0.1:8080 ESTABLISHED tcp6 0 0 127.0.0.1:8080 127.0.0.1:52135 ESTABLISHED
1本しか接続がありませんけど、この動きは…。
$ netstat -an | grep 8080 tcp6 0 0 :::8080 :::* LISTEN tcp6 1 0 127.0.0.1:52135 127.0.0.1:8080 CLOSE_WAIT tcp6 0 0 127.0.0.1:8080 127.0.0.1:52135 FIN_WAIT2
最終的には、やっぱりこうなりました。
えー。
やっぱり、明示的にResponse#discardContentした方がいいのかな?ということで、テスト追加。
@Test public void testFluentClientExplicitlyDiscard() throws IOException { for (int i = 0; i < 10; i++) { Response response = null; try { response = Request .Get("http://localhost:8080/") .execute(); String responseString = response.returnContent().asString(); assertThat(responseString) .startsWith("<html>") .contains("<title>タイトル</title>") .contains("<body>本文</body>") .contains("</html>"); } finally { if (response != null) { response.discardContent(); } } } try { Thread.sleep(30 * 1000L); } catch (Exception e) { } }
結果は?
$ netstat -an | grep 8080 tcp6 0 0 :::8080 :::* LISTEN tcp6 0 0 127.0.0.1:8080 127.0.0.1:52277 ESTABLISHED tcp6 0 0 127.0.0.1:52277 127.0.0.1:8080 ESTABLISHED
…
$ netstat -an | grep 8080 tcp6 0 0 :::8080 :::* LISTEN tcp6 0 0 127.0.0.1:8080 127.0.0.1:52277 FIN_WAIT2 tcp6 1 0 127.0.0.1:52277 127.0.0.1:8080 CLOSE_WAIT
一緒やん!
さすがに、これはどうなの?と思い、ちらっとソース確認。
private void dispose() { if (this.consumed) { return; } try { final HttpEntity entity = this.response.getEntity(); final InputStream content = entity.getContent(); if (content != null) { content.close(); } } catch (final Exception ignore) { } finally { this.consumed = true; } }
このdisposeメソッドを、Respone#returnContentとかdisposeContentとかで通っているようですが…
http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/fluent-hc/src/main/java/org/apache/http/client/fluent/Response.java
まあ、InputStreamをクローズしてるだけですねぇ…。
Request#abortとかも試してみましたけど、結果変わらず。
すぐにプロセス自体が終了してしまうちょっとしたプログラムならまだしも、これなら素直にCloseableHttpClientを使うのがいいんでしょうかねぇ?