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

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

#参考にさせていただきました。
**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
```xml:pom.xml
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.4</version>
</dependency>
```

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

- 3.x系
https://hc.apache.org/httpclient-3.x/apidocs/deprecated-list.html
- 4.x系
http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/deprecated-list.html

## 素敵なチュートリアル
http://hc.apache.org/httpcomponents-client-ga/tutorial/html/index.html

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

## 便利な定数一覧
- 3.x系
https://hc.apache.org/httpclient-3.x/apidocs/constant-values.html
- 4.x系
http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/constant-values.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 この辺に変えるのが良さそう

## インスタンスの生成の仕方がそもそも変わってる
```java:before.java
HttpClient client = new HttpClient();
```
Builder使おうぜ！ってことらしいです。
なにも考えなくてよければ、以下で。

```java:after.java
HttpClient client = HttpClients.createDefault();
```
ただし、生成するタイミングで接続設定とかは渡さないといけないので、あまり実用的ではないかも。

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

## 実行&レスポンスの取得
以前はこんな流れだったが

```java: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 {
  // エラー処理とか
}
```

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

```java: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 {
	// エラー処理とか
}
```

## ベーシック認証接続周り
今までこんな感じにしてた。

```java: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 さんありがとうございます！)

```java: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ヘッダを送らなくなった
独自に追加してあげましょう

```java
// 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
<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)
```
原因はここ

```java: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:
```
ホスト名の検証をデフォルトではしているみたい。
該当箇所はここ

```java: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証明書の検証、ホスト名の検証を無効化する](http://d.hatena.ne.jp/Kazuhira/20151213/1450024135)
[Chapter 2. Connection management 2.7.4. Hostname verification](http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e439)
私の場合は、SSLContextやSSLConnectionSocketFactoryをhttpclientに直接セットするわけではなく、ConnectionManagerにセットするやり方で実装してます。今のところ。なんので以下みたいな感じで。

```java
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