SpringのRestTemplateで巨大ファイルアップロードを実現する方法

## 1. はじめに

今回は`SpringFramework`の`RestTemplate`を利用して巨大ファイルをアップロードする方法について説明します。ポイントは`RequestCallback`を利用することです。
なお、巨大ファイルアップロードのサーバ側の実装については「[TERASOLUNA5.x(=SpringMVC)で巨大ファイルアップロードを実現する方法](https://qiita.com/5zm/items/4319782745830491bd9b)」を参照ください。

**（ポイント）**

* `RequestCallback`インターフェースを実装した独自クラスを定義し、リクエストBODYにアップロードデータをバッファリングしながら書き込む
* `RestTemplate`を拡張し、デフォルトのResponseExtractorを利用して`RequestCallback`を指定できるメソッドを追加する
* `RequestFactory`でタイムアウトやバッファデータサイズ等、巨大ファイルアップロードのためのコンフィグレーションを行う

## 2. ソースコード

```java:LargeFileRequestCallback.java
package todo.app.largefile;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.web.client.RequestCallback;

// ★ポイント1
public class LargeFileRequestCallback implements RequestCallback {

    /**
     * LOGGER
     */
    private static final Logger LOGGER = LoggerFactory
            .getLogger(LargeFileRequestCallback.class);

    /**
     * ★ポイント2
     * buffer size 1MB
     */
    private static final int BUFFER_SIZE = 1 * 1024 * 1024;

    /**
     * ★ポイント3
     * ファイル情報
     */
    private final FileInfo fileInfo;

    /**
     * ★ポイント3
     * コンストラクタ
     * @param fileInfo ファイル情報
     */
    public LargeFileRequestCallback(FileInfo fileInfo) {
        this.fileInfo = fileInfo;
    }

    /**
     * ★ポイント4
     * @see org.springframework.web.client.RequestCallback#doWithRequest(org.springframework.http.client.ClientHttpRequest)
     */
    @Override
    public void doWithRequest(ClientHttpRequest request) throws IOException {

        LOGGER.debug("doWithRequest : start");
        // ★ポイント5
        // 1. set Content-type
        request.getHeaders().add("Content-type", fileInfo.getContentType());
        request.getHeaders().add("Content-Length",
                Long.toString(fileInfo.getContentLength()));
        request.getHeaders().add("X-SHA1-CheckSum", fileInfo.getCheckSum());
        request.getHeaders().add("X-FILE-NAME", fileInfo.getDownloadFileName());

        // ★ポイント6
        // 2. copy contents with buffering
        try (InputStream input = new BufferedInputStream(
                new FileInputStream(fileInfo.getFilePath()));
                OutputStream out = request.getBody();) {
            byte[] buffer = new byte[BUFFER_SIZE];
            long total = 0;
            int len = 0;
            while ((len = input.read(buffer)) != -1) {
                out.write(buffer, 0, len);
                out.flush();
                total = total + len;
                // LOGGER.debug("writed : " + total);
            }
        }

        LOGGER.debug("doWithRequest : end");
    }

}
```

**★ポイント1**
`org.springframework.web.client.RequestCallback`インターフェースを実装した`RequestCallback`クラスを定義します。
このクラスは`RestTemplate`のHTTPリクエスト発行時にリクエストヘッダを操作したり、リクエストBODYにデータを書き込んだりすることができます。今回はファイルデータをリクエストBODYに書き込むために利用します。
なお、今回は他でも利用できるように別クラスとして定義しましたが、利用する箇所に①アロー演算子（ラムダ式）、②インナークラス（匿名クラス）として書くことも可能です。

**★ポイント2**
ファイルデータを書き込む際のバッファサイズを定義します。今回は1MBとしました。

**★ポイント3**
アップロード対象のファイル情報はコンストラクタ引数として`RequestCallback`クラスに連携します。

**★ポイント4**
今回の記事のポイントです。`doWithRequest`メソッドをオーバーライドしてアップロードファイルのデータをHTTPリクエストに書き込む処理を実装します。
引数として渡される`ClientHttpRequest`オブジェクトが`RestTemplate`が発行するHTTPリクエストです。これを直接操作してリクエストヘッダを設定したり、リクエストBODYにデータを書き込みます。

**★ポイント5**
リクエストヘッダを設定する場合、`getHeaders`メソッドで`HttpHeaders`オブジェクトを取得し、`add`メソッドで追加していきます。

**★ポイント6**
ファイルデータをバッファリングしながらリクエストBODYに書き込んでいきます。
リクエストBODYは`ClientHttpRequest`オブジェクトの`getBody`メソッドを呼び出すことで出力ストリームとしてアクセスします。

```java:RestTemplateCallbackExtend.java
package todo.app.largefile;

import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;

// ★ポイント7
public class RestTemplateCallbackExtend extends RestTemplate {

    public RestTemplateCallbackExtend() {
        super();
    }

    /**
     * ★ポイント8
     * Extend the postForEntity method so that callback can be used
     * @param url the URL
     * @param responseType the type of the return value
     * @param requestCallback object that prepares the request
     * @return the converted object
     */
    public <T> ResponseEntity<T> postForEntityWithCallback(String url,
            Class<T> responseType, RequestCallback requestCallback) {
        
        // 1. create ResponseExtractor object
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(
                responseType);
        
        // 2. send request using RequestCallback and default ResponseExtractor
        return super.execute(url, HttpMethod.POST, requestCallback,
                responseExtractor);
    }
}
```

**★ポイント7**
`RestTemplate`のデフォルトでは`RequestCallback`を利用するメソッドは３つの`execute`メソッドしか存在せず、その場合は`ResponseExtractor`も併せて指定する必要があります。
`RequestCallback`は実装しましたが`ResponseExtractor`はデフォルトの機能を利用したいため、`RestTemplate`を少しだけ拡張することにします。

**(参考)**
`ResponseEntity<String>`の`ResponseExtractor<ResponseEntity<String>>`を作るのが面倒だったため、今回は`RestTemplate`を拡張しました。
この`ResponseExtractor`のオブジェクトを簡単に作れるのであれば`RestTemplate`の`execute`メソッドを直接呼び出した方がいいでしょう。

```java:RestTemplateでRequestCallbackを引数に取るメソッドの一覧
public <T> T execute(String url,
                     HttpMethod method,
                     RequestCallback requestCallback,
                     ResponseExtractor<T> responseExtractor,
                     Object... urlVariables)
              throws RestClientException

public <T> T execute(String url,
                     HttpMethod method,
                     RequestCallback requestCallback,
                     ResponseExtractor<T> responseExtractor,
                     Map<String,?> urlVariables)
              throws RestClientException

public <T> T execute(URI url,
                     HttpMethod method,
                     RequestCallback requestCallback,
                     ResponseExtractor<T> responseExtractor)
              throws RestClientException
```

**★ポイント8**
今回は`POST`で`RequestCallback`を引数に指定可能な`postForEntity`メソッドが欲しかったので`postForEntityWithCallback`メソッドを追加しました。
`protected`な`responseEntityExtractor`メソッドを呼び出してデフォルトの`ResponseExtractor`を利用し、`RequestCallback`は引数で指定されたものを利用して`execute`メソッドを実行します。


```java:FileUploadControllerTest.java
package todo.app.largefile;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.HttpClientErrorException;

public class FileUploadControllerTest {

    /**
     * LOGGER
     */
    private static final Logger LOGGER = LoggerFactory
            .getLogger(FileUploadControllerTest.class);

    @Test
    public void test01() {
        // ★ポイント9
        // 1. prepared data
        String targetUrl = "http://localhost:8090/todo-rest/upload/chunked";
        FileInfo fileInfo = getFileInfo();

        // ★ポイント10
        // 2. create RestTemplate object
        RestTemplateCallbackExtend restTemplate = new RestTemplateCallbackExtend();

        // ★ポイント11
        // 3. setting SimpleClientHttpRequestFactory for timeout, streaming
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setBufferRequestBody(false);
        requestFactory.setConnectTimeout(1000 * 120);
        requestFactory.setReadTimeout(1000 * 120);
        requestFactory.setChunkSize(1 * 1024 * 1024);
        restTemplate.setRequestFactory(requestFactory);

        try {
            // ★ポイント12
            // 4. send request using RequestCallback
            ResponseEntity<String> responseEntity = restTemplate
                    .postForEntityWithCallback(targetUrl, String.class,
                            new LargeFileRequestCallback(fileInfo));

            // ★ポイント13
            // 5. assert
            assertThat(responseEntity.getStatusCode(), is(HttpStatus.CREATED));
            assertThat(responseEntity.getBody(), is("success!"));

        } catch (HttpClientErrorException e) {
            LOGGER.error(e.getMessage()); // 「403 Forbidden」しか入っていない。
            LOGGER.error(e.getResponseBodyAsString());
            throw e;
        }

    }

    /**
     * ★ポイント9
     * テスト用のファイル情報を取得する
     * @return ファイル情報
     */
    private FileInfo getFileInfo() {
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFilePath("C:/tmp/spring-tool-suite-3.8.2.RELEASE-e4.6.1-win32-x86_64.zip");
        fileInfo.setContentType("application/zip");
        fileInfo.setCheckSum("ff1a62872fb62edb52c1e040fdf9c720e051f9e8");
        fileInfo.setContentLength(418945672);
        fileInfo.setDownloadFileName("STS3.8.2.zip");
        return fileInfo;
    }
}
```

**★ポイン9**
送信するデータを用意します。今回は手元にあったSTSのZipファイル（400MB弱）をアップロードすることにしました。
チェックサム（ハッシュ値）は「[Pythonで巨大ファイルのハッシュ値（チェックサム）を求める方法](https://qiita.com/5zm/items/4592f1b0e036bd9505c8)」で求めました。

**★ポイン10**
`RestTemplate`ではなく★ポイント7で定義した`RestTemplateCallbackExtend`クラスのインスタンスを生成します。

**★ポイン11**
巨大なファイルをアップロードする場合、タイムアウトやバッファデータサイズ等の影響を考慮する必要があります。
`org.springframework.http.client.SimpleClientHttpRequestFactory`クラスでコンフィグレーションを行い、`RestTemplate`の`setRequestFactory`メソッドで有効にします。

* `setConnectTimeout`　：　URLコネクションのコネクションタイムアウト（単位はミリ秒）を設定
* `setReadTimeout`　：　URLコネクションの読み取りタイムアウト（単位はミリ秒）を設定
* `setChunkSize`　：　HTTPリクエストBODYにデータを書き込む際のチャンクサイズ（単位はバイト）を設定
* `setBufferRequestBody`　：　リクエストファクトリが内部的にバッファするかどうか（`true：デフォルト` or `false`）を設定

以下の説明の通り、今回はデータサイズの大きなリクエストを送信するので`false`を設定します。

> Default is true. When sending large amounts of data via POST or PUT, it is recommended to change this property to false, so as not to run out of memory. 

**★ポイン12**
★ポイント8で実装した`postForEntityWithCallback`メソッドを呼び出します。その際、引数として`LargeFileRequestCallback`オブジェクトを指定します。
これで今回実装した`RequestCallback`を利用してファイルデータがリクエストBODYに書き込まれます。

**★ポイン13**
HTTPリクエストの実行後は通常の`RestTemplate`と同様になります。
`getStatusCode`メソッドでHTTPレスポンスステータスコード、`getBody`メソッドでHTTPレスポンスBODYにアクセスします。
なお、`HttpClientErrorException`が発生した場合、`getMessage`メソッドでは例外の概要を取得します。詳細が必要な場合、`getResponseBodyAsString`メソッドで例外時のレスポンスBODYを確認します。

## 3. さいごに

今回は`RestTemplate`と`RequestCallback`を利用して巨大ファイルをアップロードする方法について説明しました。
今回試して分かりましたが`RestTemplate`には`RequestCallback`を単独で指定可能なメソッドが存在しない（`ResponseExtractor`とセットのものはある）のは少し不便でした。