SpringのRestTemplateで巨大ファイルダウンロードを実現する方法

## 1. はじめに

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

## 2. ソースコード

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

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.client.ResponseExtractor;

/**
 * ★ポイント1
 * Implements ResponseExtractor class for large file
 */
public class LargeFileResponseExtractor implements
                                        ResponseExtractor<ResponseEntity<File>> {

    /**
     * ファイルパス
     */
    private String filePath;

    /**
     * 空のコンストラクタ
     */
    public LargeFileResponseExtractor() {

    }

    /**
     * コンストラクタ
     * @param filePath ファイルパス
     */
    public LargeFileResponseExtractor(String filePath) {
        this.filePath = filePath;
    }

    /**
     * ★ポイント2
     * @see org.springframework.web.client.ResponseExtractor#extractData(org.springframework.http.client.ClientHttpResponse)
     */
    @Override
    public ResponseEntity<File> extractData(
            ClientHttpResponse response) throws IOException {

        // 1. create File object on instructed file path or temporary
        File downloadFile = null;
        if (filePath == null) {
            downloadFile = File.createTempFile("download", null);
        } else {
            downloadFile = new File(filePath);
        }

        // 2. copy contents with buffering
        FileCopyUtils.copy(response.getBody(),
                new FileOutputStream(downloadFile));

        // ★ポイント3
        // 3. create ResponseEntity object with status, headers, body
        return ResponseEntity.status(response.getStatusCode())
                .headers(response.getHeaders()).body(downloadFile);
    }
}
```

**★ポイント1**
`org.springframework.web.client.ResponseExtractor`インターフェースを実装したクラスを定義します。
**`<>`には`ResponseExtractor`の戻り値となるデータ型を定義しますが、`ResponseEntity<File>`とするのがポイントです。**
今回は他でも利用できるように別クラスとして定義しましたが、利用する箇所に①アロー演算子（ラムダ式）、②インナークラス（匿名クラス）として書くことも可能です。

**★ポイント2**
HTTPレスポンスのBODYからデータをバッファリングしつつ、ファイルとして保存する処理を`extractData`メソッドに実装します。この処理により巨大なファイルでも`OutOfMemory`にならずにファイルをダウンロードすることが可能になります。

**★ポイント3**
戻り値となる`ResponseEntity`クラスのオブジェクトを生成します。この際、HTTPレスポンスのステータスコード、レスポンスヘッダも含めるようにします。

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

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

import java.io.File;
import java.util.List;

import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;

public class FileDownloadControllerTest {

    @Test
    public void test01() {
        String targetUrl = "http://localhost:8090/todo-rest/download/chunked";

        RestTemplate restTemplate = new RestTemplate();

        // ★ポイント4
        ResponseExtractor<ResponseEntity<File>> responseExtractor = new LargeFileResponseExtractor();

        // ★ポイント5
        ResponseEntity<File> responseEntity = restTemplate.execute(targetUrl,
                HttpMethod.GET, null, responseExtractor);

        // ★ポイント6
        File downloadFile = responseEntity.getBody();
        
        // assert
        HttpHeaders httpHeaders = responseEntity.getHeaders();
        long contentLength = httpHeaders.getContentLength();
        MediaType contentType = httpHeaders.getContentType();
        List<String> checkSum = httpHeaders.get("X-SHA1-CheckSum");
        // ★ポイント7
        assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
        assertThat(contentLength, is(downloadFile.length()));
        // omitted

    }
}
```

**★ポイント4**
`LargeFileResponseExtractor`クラスのオブジェクトを生成します。

**★ポイント5**
`RestTemplate`の`execute`メソッドでHTTPリクエストを発行します。この際、第４引数の`ResponseExtractor`に★ポイント4で生成した`LargeFileResponseExtractor`のオブジェクトを指定します。

**★ポイント6**
`ResponseEntity`の`getBody`メソッドを実行すると`LargeFileResponseExtractor`で保存したダウンロードファイルが取得できます。

**★ポイント7**
今回のサンプルでは`Content-Length`ヘッダのデータサイズとダウンロードファイルのデータサイズが等しいことを確認しています。
「[TERASOLUNA5.x(=SpringMVC)で巨大ファイルダウンロードを実現する方法](https://qiita.com/5zm/items/0dd239bde353cf6dddae)」のサーバ処理ではダウンロードファイルのチェックサム（ハッシュ値）を`X-SHA1-CheckSum`ヘッダに設定しています。今回は省略しましたが、このチェックサムを比較して正常にダウンロードできたか確認する処理を追加してもいいでしょう。

## 3. さいごに
今回は`RestTemplate`と`ResponseExtractor`を利用して巨大ファイルをダウンロードする方法について説明しました。
今回の方法は`JdbcTemplate`で`ResultSetExtractor`を利用する方法とパターンが似ているかと思います。一般的にフレームワーク内ではアーキテクチャを揃えるため、APIの使い方は似たような感じになります。一つのAPIの使い方を覚えたら他も似たような感じになると思って調べてみるとよいかと思います。