OkHttp3を試す

OkHttp3を試してみたメモです。

javaのhttp clientは今までずっとapache commonsのhttpClientを使ってきたので、
okhttp3を試してみました。

OkHttp

OkHttpはsquare社が開発したhttp clientです。
apache commons httpClientよりも簡潔に記述でき、

  • HTTP/2を喋れる
  • connection poolを簡単に設定できる
  • Interceptorでrequest, responseに処理を挟める
  • 同期通信、非同期通信サポート

などの機能で最近人気?のようです

square.github.io

下記のバージョンで試してみます。

  • 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に分かれています。

f:id:pppurple:20180531223745p:plain:w400

 

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"`
}