1. はじめに
今回はSpringFramework
のRestTemplate
を利用して巨大ファイルをアップロードする方法について説明します。ポイントはRequestCallback
を利用することです。
なお、巨大ファイルアップロードのサーバ側の実装については「TERASOLUNA5.x(=SpringMVC)で巨大ファイルアップロードを実現する方法」を参照ください。
(ポイント)
-
RequestCallback
インターフェースを実装した独自クラスを定義し、リクエストBODYにアップロードデータをバッファリングしながら書き込む -
RestTemplate
を拡張し、デフォルトのResponseExtractorを利用してRequestCallback
を指定できるメソッドを追加する -
RequestFactory
でタイムアウトやバッファデータサイズ等、巨大ファイルアップロードのためのコンフィグレーションを行う
2. ソースコード
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
メソッドを呼び出すことで出力ストリームとしてアクセスします。
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
のオブジェクトを簡単に作れるのであればRestTemplate
のexecute
メソッドを直接呼び出した方がいいでしょう。
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
メソッドを実行します。
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
クラスでコンフィグレーションを行い、RestTemplate
のsetRequestFactory
メソッドで有効にします。
-
setConnectTimeout
: URLコネクションのコネクションタイムアウト(単位はミリ秒)を設定 -
setReadTimeout
: URLコネクションの読み取りタイムアウト(単位はミリ秒)を設定 -
setChunkSize
: HTTPリクエストBODYにデータを書き込む際のチャンクサイズ(単位はバイト)を設定 -
setBufferRequestBody
: リクエストファクトリが内部的にバッファするかどうか(true:デフォルト
orfalse
)を設定
以下の説明の通り、今回はデータサイズの大きなリクエストを送信するので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
とセットのものはある)のは少し不便でした。