OkHttp3を試す
OkHttp3を試してみたメモです。
javaのhttp clientは今までずっとapache commonsのhttpClientを使ってきたので、
okhttp3を試してみました。
OkHttp
OkHttpはsquare社が開発したhttp clientです。
apache commons httpClientよりも簡潔に記述でき、
- HTTP/2を喋れる
- connection poolを簡単に設定できる
- Interceptorでrequest, responseに処理を挟める
- 同期通信、非同期通信サポート
などの機能で最近人気?のようです
下記のバージョンで試してみます。
- okhttp 3.10.0
- java 1.8
- jackson-databind 2.9.5
gradle
依存関係としてokhttpとlogging-interceptorを追加しました。
logging-interceptorはインターセプターの確認で使用します。
シリアライズ用にjacksonとlombokも追加しています。
build.gradle
dependencies { compile 'com.squareup.okhttp3:okhttp:3.10.0' compile 'com.squareup.okhttp3:logging-interceptor:3.10.0' compile 'org.projectlombok:lombok:1.16.20' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.5' testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:3.10.0' }
テスト用の簡易サーバをGoで立ち上げて試してみます。
GET
単純にGETしてみます。
Request.Builderクラスを利用して、Requestを生成します。
OkHttpClientもOkHttpClient.Builderを利用して生成します。
あとは、newCall()で同期リクエストを発行します。
@Test public void get() throws Exception { String url = "http://localhost:8080/hello"; Request request = new Request.Builder() .url(url) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { String body = response.body().string(); System.out.println("body: " + body); Dog dog = mapper.readValue(body, Dog.class); System.out.println("deserialized: " + dog); } } }
クライントログ
responseCode: 200 body: {"id":1,"name":"pochi"} deserialized: MainTest.Dog(id=1, name=pochi)
サーバログ
[method] GET [header] Connection: Keep-Alive [header] Accept-Encoding: gzip [header] User-Agent: okhttp/3.10.0
GET(リクエストヘッダ追加)
リクエストヘッダの追加は、
Request.BuilderクラスのaddHeader(key, value)で追加出来ます。
@Test public void getAddHeaders() throws Exception { String url = "http://localhost:8080/hello"; final Request.Builder builder = new Request.Builder(); // set headers Map<String, String> httpHeaderMap = new HashMap<>(); httpHeaderMap.put("User-Agent", "hello-agent"); httpHeaderMap.put("x-aaa-header", "bbb"); httpHeaderMap.forEach(builder::addHeader); Request request = builder .url(url) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
クライントログ
responseCode: 200 body: {"id":1,"name":"pochi"}
サーバログ
[method] GET [header] User-Agent: hello-agent [header] Connection: Keep-Alive [header] Accept-Encoding: gzip [header] X-Aaa-Header: bbb
GET(リクエストパラメータ追加)
リクエストパラメータの追加は、
HttpUrl.BuilderクラスのaddQueryParameter(key, value)で追加出来ます。
@Test public void getAddRequestParams() throws Exception { String url = "http://localhost:8080/hello"; HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); // set request parameters Map<String, String> params = new HashMap<>(); params.put("name", "abc"); params.put("code", "123"); params.forEach(urlBuilder::addQueryParameter); Request request = new Request.Builder() .url(urlBuilder.build()) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
クライントログ
responseCode: 200 body: {"id":1,"name":"pochi"}
サーバログ
[method] GET [header] Connection: Keep-Alive [header] Accept-Encoding: gzip [header] User-Agent: okhttp/3.10.0 [param] code: 123 [param] name: abc
POST(form)
POSTでform送信してみます。
FormBody.Builderクラスのadd(name, value)で追加し、build()でRequestBodyを生成します。
nameとvalueはURLエンコードされます。
Request.post()でリクエストボディを指定します。
@Test public void postForm() throws Exception { String url = "http://localhost:8080/hello"; Map<String, String> formParamMap = new HashMap<>(); formParamMap.put("name", "abc"); formParamMap.put("code", "123"); // Names and values will be url encoded final FormBody.Builder formBuilder = new FormBody.Builder(); formParamMap.forEach(formBuilder::add); RequestBody requestBody = formBuilder.build(); Request request = new Request.Builder() .url(url) .post(requestBody) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
クライントログ
responseCode: 200 body: Recieved POST(form) request!!
サーバログ
[method] POST [header] Connection: Keep-Alive [header] Accept-Encoding: gzip [header] User-Agent: okhttp/3.10.0 [header] Content-Type: application/x-www-form-urlencoded [header] Content-Length: 17 [request body row] code=123&name=abc [request body decoded] code=123&name=abc
POST(json)
POSTでjsonを送信してみます。
request bodyはjacksonでシリアライズします。
MediaTypeを"application/json; charset=utf-8"の文字列からparseして取得します。
RequestBody.create()でMediaTypeとjsonを指定してRequestBodyを取得します。
Request.post()でリクエストボディを指定します。
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); @Test public void postJson() throws Exception { String url = "http://localhost:8080/dog_json"; Dog dog = new Dog(100, "pome"); RequestBody requestBody = RequestBody.create(JSON, mapper.writeValueAsString(dog)); Request request = new Request.Builder() .url(url) .post(requestBody) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
クライントログ
responseCode: 200 body: Recieved POST/PUT(json) request!!
サーバログ
[method] POST [header] Connection: Keep-Alive [header] Accept-Encoding: gzip [header] User-Agent: okhttp/3.10.0 [header] Content-Type: application/json; charset=utf-8 [header] Content-Length: 24 [request body row] {"id":100,"name":"pome"} [request body decoded] {Id:100 Name:pome}
PUT
PUTはPOST(json)の場合と方法は同じです。
Request.put()でPUTリクエストを指定します。
@Test public void put() throws Exception { String url = "http://localhost:8080/dog_json"; Dog dog = new Dog(100, "pome"); RequestBody requestBody = RequestBody.create(JSON, mapper.writeValueAsString(dog)); Request request = new Request.Builder() .url(url) .put(requestBody) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
クライントログ
responseCode: 200 body: Recieved POST/PUT(json) request!!
サーバログ
[method] PUT [header] Content-Length: 24 [header] Connection: Keep-Alive [header] Accept-Encoding: gzip [header] User-Agent: okhttp/3.10.0 [header] Content-Type: application/json; charset=utf-8 [request body row] {"id":100,"name":"pome"} [request body decoded] {Id:100 Name:pome}
DELETE
DELETEはGETの場合と方法は同じです。
GETの場合と同様に必要に応じてリクエストパラメータなどを付与します。
Request.delete()でDELETEリクエストを指定します。
@Test public void delete() throws Exception { String url = "http://localhost:8080/hello"; Request request = new Request.Builder() .url(url) .delete() .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
クライントログ
responseCode: 200 body: {"id":1,"name":"pochi"}
サーバログ
[method] DELETE [header] Accept-Encoding: gzip [header] User-Agent: okhttp/3.10.0 [header] Content-Length: 0 [header] Connection: Keep-Alive
connection pool
connection poolのidle connectionの最大数と、keep aliveのdurationが設定出来ます。
ConnectionPoolクラスのコンストラクタで指定します。
生成したConnectionPoolをOkHttpClient.connectionPool()で指定します。
デフォルトは
max idle connection: 5
keep alive duration: 5分
です。
@Test public void connectionPool() throws Exception { String url = "http://localhost:8080/hello"; Request request = new Request.Builder() .url(url) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)) .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
interceptor
okhttpではインターセプターでリクエスト/レスポンスに処理を挟む事が出来ます。
Interceptors · square/okhttp Wiki · GitHub
公式の図を見ると、処理を挟む箇所によって
application ⇔ okhttp間のapplication interceptorと
okhttp ⇔ network間のnetwork interceptorに分かれています。
application interceptor
application interceptorを設定してみます。
Interceptorインターフェースを実装して、全リクエストで固定のヘッダを付与するインターセプターを作成してみます。
(UserAgentの設定の場合などに便利そうです)
Interceptorインターフェースはintercept(Interceptor.Chain)を実装する必要があります。
chainからrequestを取得し、リクエストヘッダを設定します。
Interceptor.Chainのproceed()を呼んでResponseを返す必要があります。
private Interceptor headerInterceptor() { return chain -> { Request request = chain.request() .newBuilder() .header("my-header", "abcde") .build(); return chain.proceed(request); }; }
インターセプターはOkHttpClient.addInterceptor()で設定します。
インターセプターは複数設定することが出来ます。
@Test public void interceptor() throws Exception { String url = "http://localhost:8080/hello"; Request request = new Request.Builder() .url(url) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .addInterceptor(headerInterceptor()) .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
クライントログ
responseCode: 200 body: {"id":1,"name":"pochi"}
サーバログ
[method] GET [header] Connection: Keep-Alive [header] Accept-Encoding: gzip [header] User-Agent: okhttp/3.10.0 [header] My-Header: abcde
network interceptor
network interceptorはOkHttpClient.addNetworkInterceptor()で設定します。
これも複数設定することが出来ます。
リダイレクトの場合、okhttp⇔network間で処理が行われるので、
リダイレクトの通信の様子を見てみます。
okhttpのHttpLoggingInterceptorを使用すると、Http通信をログ出力出来ます。
HttpLoggingInterceptorをapplication interceptorに設定した場合と、
network interceptorに設定した場合で違いを確認してみます。
@Test public void networkInterceptor() throws Exception { String url = "http://localhost:8080/redirect"; Request request = new Request.Builder() .url(url) .build(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)) // .addInterceptor(new HttpLoggingInterceptor().setLevel(Level.BODY)) .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(Level.BODY)) .build(); try (Response response = okHttpClient.newCall(request).execute()) { int responseCode = response.code(); System.out.println("responseCode: " + responseCode); if (!response.isSuccessful()) { System.out.println("error!!"); } if (response.body() != null) { System.out.println("body: " + response.body().string()); } } }
application interceptorの場合のサーバログ。
直接200でgoogleのレスポンスが来ています。
情報: --> GET http://localhost:8080/redirect 情報: --> END GET 情報: <-- 200 OK http://www.google.com/ (136ms) 情報: Date: Thu, 31 May 2018 10:35:00 GMT 情報: Expires: -1 情報: Cache-Control: private, max-age=0 情報: Content-Type: text/html; charset=ISO-8859-1 情報: P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info." 情報: Server: gws 情報: X-XSS-Protection: 1; mode=block 情報: X-Frame-Options: SAMEORIGIN 情報: Set-Cookie: 1P_JAR=2018-05-31-10; expires=Sat, 30-Jun-2018 10:35:00 GMT; path=/; domain=.google.com 情報: Set-Cookie: NID=131=tpvhKfOZGsgAdl-UuGKsHXYfYlF5Xrmok2wh0rnHKPbpH7RQ-VcIc9qQByXc3k4kO-FSnFLtaaJd-enNE-0Wt3wE3deI54R0W4ihJgOKw-ZE0cbq21S8PZ9GIfHzNwxU; expires=Fri, 30-Nov-2018 10:35:00 GMT; path=/; domain=.google.com; HttpOnly 情報: <-- END HTTP (11881-byte body) responseCode: 200 body: <!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head>……………
network interceptorの場合のサーバログ。
301が返されて、googleにリダイレクトされているのが分かります。
情報: --> GET http://localhost:8080/redirect http/1.1 情報: Host: localhost:8080 情報: Connection: Keep-Alive 情報: Accept-Encoding: gzip 情報: User-Agent: okhttp/3.10.0 情報: --> END GET 情報: <-- 301 Moved Permanently http://localhost:8080/redirect (6ms) 情報: Location: http://www.google.com 情報: Date: Thu, 31 May 2018 10:39:02 GMT 情報: Content-Length: 56 情報: Content-Type: text/html; charset=utf-8 情報: 情報: <a href="http://www.google.com">Moved Permanently</a>. 情報: <-- END HTTP (56-byte body) 情報: --> GET http://www.google.com/ http/1.1 情報: Host: www.google.com 情報: Connection: Keep-Alive 情報: Accept-Encoding: gzip 情報: User-Agent: okhttp/3.10.0 情報: --> END GET 情報: <-- 200 OK http://www.google.com/ (81ms) 情報: Date: Thu, 31 May 2018 10:39:02 GMT 情報: Expires: -1 情報: Cache-Control: private, max-age=0 情報: Content-Type: text/html; charset=ISO-8859-1 情報: P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info." 情報: Content-Encoding: gzip 情報: Server: gws 情報: Content-Length: 4937 情報: X-XSS-Protection: 1; mode=block 情報: X-Frame-Options: SAMEORIGIN 情報: Set-Cookie: 1P_JAR=2018-05-31-10; expires=Sat, 30-Jun-2018 10:39:02 GMT; path=/; domain=.google.com 情報: Set-Cookie: NID=131=T6FiEwAuE1_TWcZ6bYQO9qw-o7gmi4jioN4zzml1972uCjQfe2kKaExpwxgwkbW_jq1B-w3ZmvlaVlRovr20D2czbayoxAHMFBk8w4vt-AfMQ8OQhsW7Lq2WdXmKc85Q; expires=Fri, 30-Nov-2018 10:39:02 GMT; path=/; domain=.google.com; HttpOnly 情報: <-- END HTTP (11873-byte, 4937-gzipped-byte body) responseCode: 200 body: <!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head>…………
テストコードは下記にあげました。
github.com
参考
http://yuki312.blogspot.jp/2016/03/okhttp-interceptor.html
http://fushiroyama.hatenablog.com/entry/2017/11/28/122524
終わり。
一応テストで使った簡易的なサーバを載せておきます。
main.go
package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "strings" ) func main() { http.HandleFunc("/hello", hello) http.HandleFunc("/dog_json", handleDogJson) http.HandleFunc("/redirect", redirect) http.ListenAndServe(":8080", nil) } func hello(w http.ResponseWriter, req *http.Request) { // header method := req.Method fmt.Println("[method] " + method) for k, v := range req.Header { fmt.Print("[header] " + k) fmt.Println(": " + strings.Join(v, ",")) } // GET/DELETE if method == "GET" || method == "DELETE" { req.ParseForm() for k, v := range req.Form { fmt.Print("[param] " + k) fmt.Println(": " + strings.Join(v, ",")) } dog, _ := json.Marshal(Dog{1, "pochi"}) fmt.Fprint(w, string(dog)) } // POST (form) if method == "POST" { defer req.Body.Close() body, err := ioutil.ReadAll(req.Body) if err != nil { log.Fatal(err) } fmt.Println("[request body row] " + string(body)) decoded, error := url.QueryUnescape(string(body)) if error != nil { log.Fatal(error) } fmt.Println("[request body decoded] ", decoded) fmt.Fprint(w, "Recieved POST(form) request!!") } } func handleDogJson(w http.ResponseWriter, req *http.Request) { // header method := req.Method fmt.Println("[method] " + method) for k, v := range req.Header { fmt.Print("[header] " + k) fmt.Println(": " + strings.Join(v, ",")) } // POST/PUT (json) if method == "POST" || method == "PUT" { defer req.Body.Close() body, err := ioutil.ReadAll(req.Body) if err != nil { log.Fatal(err) } fmt.Println("[request body row] " + string(body)) // Unmarshal var dog Dog error := json.Unmarshal(body, &dog) if error != nil { log.Fatal(error) } fmt.Printf("[request body decoded] %+v\n", dog) fmt.Fprint(w, "Recieved POST/PUT(json) request!!") } } func redirect(w http.ResponseWriter, req *http.Request) { // header method := req.Method fmt.Println("[method] " + method) for k, v := range req.Header { fmt.Print("[header] " + k) fmt.Println(": " + strings.Join(v, ",")) } // GET if method == "GET" { req.ParseForm() for k, v := range req.Form { fmt.Print("[param] " + k) fmt.Println(": " + strings.Join(v, ",")) } http.Redirect(w, req, "http://www.google.com", 301) } } type Dog struct { Id int `json:"id"` Name string `json:"name"` }