今携わっている案件で、複数ファイル(実際はDBに保持しているBLOBデータ)を1回のAPI呼び出しでダウンロードしたい!!という要望があって、思いついた案は・・・マルチパートとJDK標準のZIP APIでした。マルチパートレスポンスはクライアント系のライブラリがほとんでサポートしていない感じだったのでボツにし、JDK標準のZip API(ZipOutputStream
/ ZipInputStream
)を使うことにしました。
そんなもんで、本日はJDK標準のZip APIを使った複数ファイルのダウンロードの実装例を紹介したいと思います。
ちなみに・・・Spring Boot使っているので、サンプルもSpring Bootアプリケーションです。
動作検証バージョン
- Spring Boot 1.3.5.RELEASE
- Java SE 8
- Mac
検証コード
プロジェクト作成
SPRING INITIALIZRにて、Artifactを「zip-demo」、Dependenciesに「Web」を指定してプロジェクトをダウンロードし、任意の場所に解凍します。今回は、Apache Http Clientを使ってダウンロードを行うので・・・Apache Http Clientを依存リブラリに追加します。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
今回紹介するサンプルでは、アップロードしたファイルをZipファイルとしてダウンロードする構成にするため、org.apache.httpcomponents:httpmime
を依存ライブラリに追加しています。なお、org.apache.httpcomponents:httpclient
は推移的依存性によって解決されるので、ここでは明示的に指定しません。
Handlerメソッドの作成
本来はControllerクラスを作成すべきですが、ここでは起動クラスに直接Handlerメソッドを追加しちゃいます。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Controller // @Controllerを追加する
@SpringBootApplication
public class ZipDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZipDemoApplication.class, args);
}
@Bean // ファイルアップロード機能を有効化する
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
@RequestMapping(path = "/files") // Handlerメソッドを追加する
public void zipDownload(@RequestParam List<MultipartFile> files, HttpServletResponse response) throws IOException {
response.setHeader(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=download.zip");
try (ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream())) {
for (MultipartFile file : files) {
String fileName = Paths.get(file.getOriginalFilename()).getFileName().toString();
try (InputStream input = file.getInputStream()) {
zipOutputStream.putNextEntry(new ZipEntry(fileName));
StreamUtils.copy(input, zipOutputStream); // write per 4KB
}
}
}
}
}
上記のAPIを呼び出す際に指定するファイルを作成します。
aaaaa
bbbbb
curlを利用したAPIの呼び出し
APIを呼び出す前にSpring Bootアプリケーションを起動します。
./mvnw spring-boot:run
...
2016-06-23 06:03:24.080 INFO 75579 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2016-06-23 06:03:24.135 INFO 75579 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-06-23 06:03:24.141 INFO 75579 --- [ main] com.example.ZipDemoApplication : Started ZipDemoApplication in 1.699 seconds (JVM running for 3.812)
Spring Bootアプリケーションが正常に起動したら、curlコマンドを使ってアップロードしたファイルをZipファイルとしてダウンロードします。
$ curl -X POST -F files=@data/a.txt -F files=@data/b.txt -O http://localhost:8080/files
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 568 0 236 100 332 7772 10934 --:--:-- --:--:-- --:--:-- 11066
filesという名前のファイルがダウンロードされているので、Zipファイル内のエントリーを確認します。
$ jar -tvf files
5 Thu Jun 23 05:59:22 JST 2016 a.txt
5 Thu Jun 23 05:59:22 JST 2016 b.txt
アップロードしたファイルがダウンンロードできていそうですね
クライアントライブラリを利用したAPIの呼び出し
ここでは、SpringのRestTemplate
とApache Http Clientを使用してAPIにアクセスしてみます。今回のサンプルでは、SPRING INITIALIZRからダウンロードしたプロジェクトにあらかじめ同封されているテストケースクラスを書き換えます。
package com.example;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ZipDemoApplication.class)
@WebIntegrationTest(randomPort = true) // Junit内で空きポートを使って組み込みTomcatを起動
public class ZipDemoApplicationTests {
@Value("http://localhost:${local.server.port}/files") // 組み込みTomcat上のURLをインジェクション
private String url;
/**
* Springが提供するRestTemplateを使用した時のダウンロード例
*/
@Test
public void zipDownloadUsingSpringRestTemplate() throws IOException {
RestTemplate restOperations = new RestTemplate();
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("files", new FileSystemResource("data/a.txt"));
form.add("files", new FileSystemResource("data/b.txt"));
RequestEntity<MultiValueMap<String, Object>> requestEntity =
RequestEntity.post(URI.create(url)).body(form);
// ★★★ この実装だとByteArrayInputStreamとして扱われるので大量データを扱う場合は注意が必要!! ★★★
ResponseEntity<Resource> responseEntity = restOperations.exchange(requestEntity, Resource.class);
try (ZipInputStream zipInputStream = new ZipInputStream(responseEntity.getBody().getInputStream())) {
{
ZipEntry entry = zipInputStream.getNextEntry();
assertThat(entry.getName(), is("a.txt"));
assertThat(StreamUtils.copyToString(zipInputStream, StandardCharsets.UTF_8), is("aaaaa"));
}
{
ZipEntry entry = zipInputStream.getNextEntry();
assertThat(entry.getName(), is("b.txt"));
assertThat(StreamUtils.copyToString(zipInputStream, StandardCharsets.UTF_8), is("bbbbb"));
}
assertNull(zipInputStream.getNextEntry());
}
}
/**
* Apache Http Clientが提供するJava APIを使用した時のダウンロード例
*/
@Test
public void zipDownloadUsingApacheHttpClient() throws IOException {
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
HttpPost request = new HttpPost(url);
request.setEntity(MultipartEntityBuilder.create()
.addPart("files", new FileBody(new File("data/a.txt")))
.addPart("files", new FileBody(new File("data/b.txt")))
.build());
HttpResponse response = httpClient.execute(request);
try (ZipInputStream zipInputStream = new ZipInputStream(response.getEntity().getContent())) {
{
ZipEntry entry = zipInputStream.getNextEntry();
assertThat(entry.getName(), is("a.txt"));
assertThat(StreamUtils.copyToString(zipInputStream, StandardCharsets.UTF_8), is("aaaaa"));
}
{
ZipEntry entry = zipInputStream.getNextEntry();
assertThat(entry.getName(), is("b.txt"));
assertThat(StreamUtils.copyToString(zipInputStream, StandardCharsets.UTF_8), is("bbbbb"));
}
assertNull(zipInputStream.getNextEntry());
}
}
}
}
このテストケースを実行すると、組み込みTomcat上のAPIを呼び出し、アップロードしたファイルをZipファイルとしてダウンロードできます。
まとめ
サーバーもクライアントも簡単に実装できることがわかったかと思います。サンプルコード内にも記載しましたが、RestTemplate
を利用して容量が大きいデータを扱う場合、サンプルコードで紹介している実装を使うとメモリがやばいことになる可能性が高いので注意してください。容量が大きいデータをダウンロードする場合は、こちらのサンプルを参考にアプリケーション要件にあった実装にしましょう。