LoginSignup
9
8

More than 5 years have passed since last update.

【更新中】httpclient3.x系から4.5まで上げたら大変だったので変更点まとめを書く

Last updated at Posted at 2018-03-12

なにやったか自分でもわからなくなってくるので、だらだらと変更したことを書いていきます。

参考にさせていただきました。

Java apache HttpClient 4.3からの大幅なインターフェース変更に対応 (@mychaelstyle)
https://qiita.com/mychaelstyle/items/e02b3011d1e71bfa26c5
Apache httpclient の CloseableHttpClient で HTTPS なサイトへ BASIC 認証付きリクエスト (@sh_kawakami )
https://qiita.com/sh_kawakami/items/bf6d1397851cccd134b2
httpclientのインターフェースが4.3から大きく変わったみたいですよ (@sakito )
https://qiita.com/sakito/items/6366015dbbc4a88d56fc
What does setDefaultMaxPerRoute and setMaxTotal mean in HttpClient?
https://stackoverflow.com/questions/30689995/what-does-setdefaultmaxperroute-and-setmaxtotal-mean-in-httpclient の解答欄

環境

JDK 1.8.162
httpclient 4.5.4

pom.xml

pom.xml
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.4</version>
</dependency>

素敵なDeprecatedList

こんなにDeprecatedListに助けられたことはないってぐらい見まくってる

素敵なチュートリアル

素敵なQuickGuide

3.xから4.0に上げたときのガイドなので、4.5までとなるとまた変わってることも。
http://debuguide.blogspot.jp/2013/01/quick-guide-for-migration-of-commons.html

便利な定数一覧

コンパイルエラーをなんとかする

パッケージ変更

  • org.apache.commons.httpclient.Cookie; →org.apache.http.cookie.Cookie(interfaceなのでインスタンス作るときはBasicClientCookieを使う?)
  • org.apache.commons.httpclient.util.DateUtil; →org.apache.http.client.utils.DateUtils
  • org.apache.commons.httpclient.Header →org.apache.http.Header(interfaceなのでインスタンス作るときはBasicHeaderを使う)
  • org.apache.commons.httpclient.HeaderGroup →org.apache.http.message.HeaderGroup

標準APIに変更

  • org.apache.commons.httpclient.URI; →java.net.URI
  • org.apache.commons.httpclient.URIException; →java.net.URISyntaxException

ごっそり変わってる系

  • org.apache.commons.httpclient.HostConfiguration →org.apache.http.HttpHost
  • org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory →org.apache.http.conn.ssl.SSLConnectionSocketFactory この辺に変えるのが良さそう

インスタンスの生成の仕方がそもそも変わってる

before.java
HttpClient client = new HttpClient();

Builder使おうぜ!ってことらしいです。
なにも考えなくてよければ、以下で。

after.java
HttpClient client = HttpClients.createDefault();

ただし、生成するタイミングで接続設定とかは渡さないといけないので、あまり実用的ではないかも。

methodのクラス変更

GetMethod→HttpGet
PostMethod→HttpPost
親クラスHttpMethod→HttpRequestBase
にそれぞれ変更

実行&レスポンスの取得

以前はこんな流れだったが

before.java
PostMethod post = new PostMethod(url);
post.setRequestEntity(なんかセットする);
int responseCode = client.executeMethod(post);
if (responseCode == 200) {
    Reader reader = new InputStreamReader(
            httpMethod.getResponseBodyAsStream(), httpMethod.getResponseCharSet());
    // 後続処理
} else {
  // エラー処理とか
}

だいぶ変わってこんな感じに。
でも明確になったので、わかりやすくなったかも。

after.java
HttpPost post = new HttpPost(url);
// リクエストbodyにあたるやつをセット
post.setEntity(なんかセットする);
HttpResponse response = client.execute(post);
StatusLine statusLine = response.getStatusLine();
int responseCode = statusLine.getStatusCode();
if (responseCode == HttpStatus.SC_OK) {
    Reader reader = new InputStreamReader(
            response.getEntity().getContent(), Charset.forName("UTF-8"));
    // 後続処理
} else {
    // エラー処理とか
}

ベーシック認証接続周り

今までこんな感じにしてた。

before.java
Credentials credentials = new UsernamePasswordCredentials(this.username, this.password);
AuthScope scope = new AuthScope(host, port, this.realm);
client.getState().setCredentials(scope, credentials);

CredentialsProvider使えば良いらしいです。(@sh_kawakami さんありがとうございます!)

after.java
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(baseUrl.getHost(),baseUrl.getPort()),
        new UsernamePasswordCredentials(username, password));
// インスタンス生成時にセットしてあげる
HttpClient client = HttpClients.custom().setDefaultCredentialsProvider(credentialsProvider).build();

NameValuePairのインターフェイス化

実体クラスはBasicNameValuePairになったみたい。
『=』で分割するだけっぽいから、今までと挙動が変わらないことを祈りつつ。
が、setterがなくなってる!!!
見た感じ、コンストラクタでしかsetできないようになったみたい。
それでもsetterを使いたい場合はNameValuePairを継承して独自クラスを作るしかない。

実行時にエラーになったあれこれ

デフォルトのHttpClientはHostヘッダを送らなくなった

独自に追加してあげましょう

// hostヘッダー
httpMethod.addHeader("Host", uri.getHost());

Content-LengthヘッダとResponseBodyのサイズが一致しないとConnectionClosedExceptionが発生する

レスポンスのContent-Lengthヘッダがある場合、Bodyとサイズが異なる場合に発生する。Content-Lengthヘッダがない場合は発生しない(それはそれでRFC違反な気がするけど)
例えばこんな感じのレスポンスだった時に発生。
まあ、今日日Content-Lengthは自動計算してサーバが勝手に返すと思うので、発生することは少ないと思いますけど、諸事情によりレスポンスヘッダは信用しないことにしてるので:sweat:

HTTP/1.1 200 OK
Content-Type: text/html; charset=Shift_JIS
Content-Length: 3000
<html>
<head><title>ほげ</title>
</head>
<body>test
</body>
</html>
org.apache.http.ConnectionClosedException: Premature end of Content-Length delimited message body (expected: 3000; received: 124
    at org.apache.http.impl.io.ContentLengthInputStream.read(ContentLengthInputStream.java:178)
    at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:135)
    at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:148)

原因はここ

org.apache.http.impl.io.ContentLengthInputStream.java
    /**
     * Does standard {@link InputStream#read(byte[], int, int)} behavior, but
     * also notifies the watcher when the contents have been consumed.
     *
     * @param b     The byte array to fill.
     * @param off   Start filling at this position.
     * @param len   The number of bytes to attempt to read.
     * @return The number of bytes read, or -1 if the end of content has been
     *  reached.
     *
     * @throws java.io.IOException Should an error occur on the wrapped stream.
     */
    @Override
    public int read (final byte[] b, final int off, final int len) throws java.io.IOException {
        if (closed) {
            throw new IOException("Attempted read from closed stream.");
        }

        if (pos >= contentLength) {
            return -1;
        }

        int chunk = len;
        if (pos + len > contentLength) {
            chunk = (int) (contentLength - pos);
        }
        final int count = this.in.read(b, off, chunk);
        if (count == -1 && pos < contentLength) {
            throw new ConnectionClosedException(
                    "Premature end of Content-Length delimited message body (expected: "
                    + contentLength + "; received: " + pos);
        }
        if (count > 0) {
            pos += count;
        }
        return count;
    }

対処方法は今のところ見つからず。

SSL通信時(自己証明書含む)に証明書とhostnameがマッチしないよエラー

javax.net.ssl.SSLPeerUnverifiedException: Certificate for <hostname> doesn't match any of the subject alternative names:

ホスト名の検証をデフォルトではしているみたい。
該当箇所はここ

org.apache.http.conn.ssl.SSLConnectionSocketFactory.java#verifyHostname
            if (!this.hostnameVerifier.verify(hostname, session)) {
                final Certificate[] certs = session.getPeerCertificates();
                final X509Certificate x509 = (X509Certificate) certs[0];
                final List<SubjectName> subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509);
                throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " +
                        "of the subject alternative names: " + subjectAlts);
            }

verifyがtrueを返せば良いので、NoopHostnameVerifierクラスのインスタンスを使えば良いらしい。
Apache HttpComponents/Clientで、SSL証明書の検証、ホスト名の検証を無効化する
Chapter 2. Connection management 2.7.4. Hostname verification
私の場合は、SSLContextやSSLConnectionSocketFactoryをhttpclientに直接セットするわけではなく、ConnectionManagerにセットするやり方で実装してます。今のところ。なんので以下みたいな感じで。

public static PoolingHttpClientConnectionManager createConnectionManager() {
    SSLContext sslContext = null;
    try {
        sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy(){
            @Override
            public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                return true;
            }
        }
        ).build();
    } catch (Exception e) {
        e.printStackTrace();
    }
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    Registry<ConnectionSocketFactory> registry =
            RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", sslsf)
                    .build();
    return new PoolingHttpClientConnectionManager(registry);
}

その他参考にさせていただいたサイト様

Apache HttpClient の DefaultHttpRequestRetryHandler は ConnectTimeoutException のときはリトライしない
http://kntmr.hatenablog.com/entry/2016/12/09/150615
Use of non-ascii credentials not working in httpclient 4.3.x
https://stackoverflow.com/questions/27955067/use-of-non-ascii-credentials-not-working-in-httpclient-4-3-x

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8