なにやったか自分でもわからなくなってくるので、だらだらと変更したことを書いていきます。
#参考にさせていただきました。
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
<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
素敵なチュートリアル
素敵な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 この辺に変えるのが良さそう
インスタンスの生成の仕方がそもそも変わってる
HttpClient client = new HttpClient();
Builder使おうぜ!ってことらしいです。
なにも考えなくてよければ、以下で。
HttpClient client = HttpClients.createDefault();
ただし、生成するタイミングで接続設定とかは渡さないといけないので、あまり実用的ではないかも。
methodのクラス変更
GetMethod→HttpGet
PostMethod→HttpPost
親クラスHttpMethod→HttpRequestBase
にそれぞれ変更
実行&レスポンスの取得
以前はこんな流れだったが
PostMethod post = new PostMethod(url);
post.setRequestEntity(なんかセットする);
int responseCode = client.executeMethod(post);
if (responseCode == 200) {
Reader reader = new InputStreamReader(
httpMethod.getResponseBodyAsStream(), httpMethod.getResponseCharSet());
// 後続処理
} else {
// エラー処理とか
}
だいぶ変わってこんな感じに。
でも明確になったので、わかりやすくなったかも。
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 {
// エラー処理とか
}
ベーシック認証接続周り
今までこんな感じにしてた。
Credentials credentials = new UsernamePasswordCredentials(this.username, this.password);
AuthScope scope = new AuthScope(host, port, this.realm);
client.getState().setCredentials(scope, credentials);
CredentialsProvider使えば良いらしいです。(@sh_kawakami さんありがとうございます!)
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は自動計算してサーバが勝手に返すと思うので、発生することは少ないと思いますけど、諸事情によりレスポンスヘッダは信用しないことにしてるので
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)
原因はここ
/**
* 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:
ホスト名の検証をデフォルトではしているみたい。
該当箇所はここ
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