LoginSignup
3
5

More than 5 years have passed since last update.

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

Posted at

1. はじめに

今回はSpringFrameworkRestTemplateを利用して巨大ファイルをアップロードする方法について説明します。ポイントはRequestCallbackを利用することです。
なお、巨大ファイルアップロードのサーバ側の実装については「TERASOLUNA5.x(=SpringMVC)で巨大ファイルアップロードを実現する方法」を参照ください。

(ポイント)

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

2. ソースコード

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メソッドを呼び出すことで出力ストリームとしてアクセスします。

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を利用するメソッドは3つのexecuteメソッドしか存在せず、その場合はResponseExtractorも併せて指定する必要があります。
RequestCallbackは実装しましたがResponseExtractorはデフォルトの機能を利用したいため、RestTemplateを少しだけ拡張することにします。

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

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
今回はPOSTRequestCallbackを引数に指定可能なpostForEntityメソッドが欲しかったのでpostForEntityWithCallbackメソッドを追加しました。
protectedresponseEntityExtractorメソッドを呼び出してデフォルトのResponseExtractorを利用し、RequestCallbackは引数で指定されたものを利用してexecuteメソッドを実行します。

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で巨大ファイルのハッシュ値(チェックサム)を求める方法」で求めました。

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

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

  • 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. さいごに

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

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5