CLOVER🍀

That was when it all began.

Apache HttpComponentsのFluent APIってどうなの?

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&ClojurePerlでの簡単な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を使うのがいいんでしょうかねぇ?