Java標準機能でHttpClient

普段はSpringBootを使った開発をしているため[RestTemplate](https://spring.pleiades.io/guides/gs/consuming-rest/)を使うのだが、いざSpringBootに依存しない実装が必要となったときに、[Apache HttpClient](https://hc.apache.org/httpcomponents-client-4.5.x/) を使わなくてもJavaの標準機能だけで簡単に実装できるようになったことを恥ずかしながら最近知ったので、昨年Java17がリリースされた今となってはもう古い記事になってしまうが、今更ながらJava標準機能でHttp通信するクライアントプログラムについて簡単にまとめておくことにした。

# 本記事の前提
- Java11以上であること(Java8ではサポートされてない機能)
- サーバ側のコードは割愛
- Jsonとのマッピング機能はJacksonを想定

# 受信するデータ型
```java
public class ResponseDto {
    private String message;
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    @Override
    public String toString() {
        return "ResponseDto{" +
                "message='" + message + '\'' +
                '}';
    }
}
```


# サンプル実装
```java
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class Java11HttpSample {
    private final HttpClient httpClient;
    Java11HttpSample() {
        this.httpClient = HttpClient.newHttpClient();
    }
    Java11HttpSample(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    /**
     * JSON文字列をGetするサンプル
     */
    public String getJsonString() throws IOException, InterruptedException {
        // リクエストを作成
        HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:8080/sample/get"))
                .build();
        // 同期API呼び出し
        HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body()); // 受信したJSON文字列を確認
        return response.body();
    }

    /**
     * JSON文字列を受信してオブジェクトにマッピングする。マッピング自体はJacksonで実行する
     */
    public ResponseDto getJsonData() throws IOException, InterruptedException {
        // リクエストを作成
        HttpRequest request = HttpRequest
                .newBuilder(URI.create("http://localhost:8080/sample/get"))
                .build();
        // 同期API呼び出し
        HttpResponse<ResponseDto> response = this.httpClient.send(request, responseInfo ->
                // 受信データをマッピングするロジック
                HttpResponse.BodySubscribers.mapping(
                        HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), // JSONを文字列で受信して
                        (String body) -> { // 受信した文字列をラムダの引数bodyで受け取り
                            try {
                                return new ObjectMapper().readValue(body, ResponseDto.class); // Jacksonでオブジェクトに変換
                            } catch (IOException e) {
                                throw new UncheckedIOException(e);
                            }
                        })
        );
        System.out.println(response.body()); // body is ResponseDto
        return response.body();
    }

    /**
     * Postのサンプル
     */
    public void postJson() throws IOException, InterruptedException {
        HttpRequest httpRequest = HttpRequest.newBuilder(URI.create("http://localhost:8080/sample/post"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                        new ObjectMapper().writeValueAsString(Map.of("key", "value"))))
                .build();

        HttpResponse<String> httpResponse = this.httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
        System.out.println(httpResponse.body());
    }
}
```

# テストコード
一般的に、単体テストではHttp通信する処理をモックにすると思うので、上記サンプルを[Mockito](https://github.com/mockito/mockito)でモックして(サーバ不要で)単体テストするコードも載せておく。
そのために`java.net.http.HttpClient`はコンストラクタ等で外部から設定できるように実装しておいたほうがいい。

```java
@ExtendWith(MockitoExtension.class)
public class Java11HttpTest {

    @Mock
    HttpResponse<String> httpResponseString;

    @Mock
    HttpResponse<ResponseDto> httpResponseJson;

    @Spy
    HttpClient httpClient;

    @Test
    public void getJsonStringTest() throws IOException, InterruptedException {
        Mockito.when(this.httpResponseString.body()).thenReturn("hello");
        Mockito.when(this.httpClient.send(Mockito.any(), Mockito.any(HttpResponse.BodyHandlers.ofString().getClass())))
                .thenReturn(this.httpResponseString);
        Assertions.assertEquals("hello", new Java11HttpSample(this.httpClient).getJsonString());
    }

    @Test
    public void getJsonDataTest() throws IOException, InterruptedException {
        final ResponseDto expected = new ResponseDto();
        Mockito.when(this.httpResponseJson.body()).thenReturn(expected);
        Mockito.when(this.httpClient.send(Mockito.any(), Mockito.any(HttpResponse.BodyHandler.class)))
                .thenReturn(this.httpResponseJson);
        Assertions.assertEquals(expected, new Java11HttpSample(this.httpClient).getJsonData());
    }
}
```

# さいごに
Java8など古いJavaでは`java.net.http`パッケージが提供されていないため、[Apache HttpClient](https://hc.apache.org/httpcomponents-client-4.5.x/)などの外部ライブラリを利用したほうが簡単に実装できるが、Java11以上であれば標準機能だけで、かなりシンプルに実装できる。
また、今回は基本的なGET/POSTのサンプルのみだが、WebSocketやHTTP/2対応した通信も実装できる（詳細は以下Javadoc等を確認）ため、特にこだわりがなければ今後は標準機能の実装方法を覚えていったほうが汎用性が高いのではないかと感じた。

https://docs.oracle.com/javase/jp/11/docs/api/java.net.http/java/net/http/package-summary.html