1. はじめに
Feign
のデフォルトの機能ではファイルダウンロードやアップロードといったバイナリを扱うHTTPリクエストの操作には対応していません。
今回はFeign
でファイルダウンロードを実現する方法について説明したいと思います。
Feign
の基本的な使い方については以下の記事で説明しているのでそちらを参照してください。
2. モデルクラスの定義
feign.Response
から取得可能なヘッダ、ステータス、レスポンスリーズンのデータを保持するクラスです。
HTTPレスポンスのBODYに1つのファイルのバイナリデータが格納されている前提とし、ファイルを1つだけ保持する仕様としました。
あまり一般的ではありませんが、マルチパートのHTTPレスポンスを扱いたい場合は適宜修正してください。
package com.example.feign.demo.download;
import java.io.File;
import java.io.Serializable;
import java.util.Collection;
import java.util.Map;
public class FileEntity implements Serializable {
private static final long serialVersionUID = 1L;
private int status;
private Map<String, Collection<String>> headers;
private String reason;
private File file;
public FileEntity() {
}
public FileEntity(int status, Map<String, Collection<String>> headers,
String reason, File file) {
super();
this.status = status;
this.headers = headers;
this.reason = reason;
this.file = file;
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("FileEntity [status=");
builder.append(status);
builder.append(", headers=");
builder.append(headers);
builder.append(", reason=");
builder.append(reason);
builder.append(", file=");
builder.append(file);
builder.append("]");
return builder.toString();
}
// setter, getter omitted
}
3. APIのインターフェースの定義
APIは指定されたパスのファイルをダウンロードするというシンプルなものです。
戻り値は先ほど定義したモデルクラスのFileEntity
です。
java.io.File
やjava.nio.file.Path
を戻り値にしてしまうと、ファイルデータ以外の情報(ヘッダ、ステータスコード等)が取得できないためFileEntity
を用意しました。
package com.example.feign.demo.download;
import feign.Param;
import feign.RequestLine;
public interface DownloadApi {
@RequestLine("GET {path}")
FileEntity download(@Param("path") String path);
}
4. ファイルダウンロードのDecoderを定義
今回の記事のポイントになります。HTTPレスポンスのデータ変換処理(デシリアライズ)はDecoder
が担当します。
feign.codec.Decoder
インターフェースをimplements
したファイルダウンロード用のDecoder
を実装します。
目的の処理をdecode
メソッドに実装します。このメソッドの戻り値がAPIインターフェースの戻り値になります。
なお、今回は1ファイルの前提のためレスポンスのBODYのデータをそのままファイルに保存していますが、マルチパートのHTTPレスポンスの場合はパースして複数ファイルとして保存する必要があるので注意してください。
package com.example.feign.demo.download;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import feign.FeignException;
import feign.Response;
import feign.Util;
import feign.codec.DecodeException;
import feign.codec.Decoder;
public class FileEntityDecoder implements Decoder {
private static final int BUFFER_SIZE = 1 * 1024 * 1024;
private String filePath;
public FileEntityDecoder() {
}
public FileEntityDecoder(String filePath) {
this.filePath = filePath;
}
/**
* @see feign.codec.Decoder#decode(feign.Response, java.lang.reflect.Type)
*/
@Override
public Object decode(Response response,
Type type) throws IOException, DecodeException, FeignException {
if (response.status() == 404) {
return Util.emptyValueOf(type);
}
if (response.body() == null) {
return null;
}
return createFileEntity(response, filePath);
}
/**
* Save download file on instructed filePath.
* @param response feign's response
* @param filePath download file path
* @return FileEntity instance
* @throws IOException
*/
private FileEntity createFileEntity(Response response,
String filePath) 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
try (InputStream input = new BufferedInputStream(
response.body().asInputStream());
OutputStream out = new BufferedOutputStream(
new FileOutputStream(downloadFile));) {
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;
// System.out.println("writed : " + total);
}
}
// 3. create FileEntity instance
return new FileEntity(response.status(), response.headers(),
response.reason(), downloadFile);
}
}
5. お試し実行
APIインターフェースのインスタンスを生成する際、decoder
メソッドの引数に今回実装したFileEntityDecoder
のインスタンスを指定します。
認証が必要なProxy
を経由して、インターネットからPDFファイルをダウンロードするサンプルとしました。
Proxy
対応のポイントとなるcreateOkHttpClientCorrespondedProxy
メソッドの実装については前回の記事を参照してください。
エンドポイントのURLであるhttp://www.example.com
とパスの/document/2018/pdf/sample.pdf
は各自で置き換えてください。
package com.example.feign.demo.download;
import feign.Feign;
import feign.Logger;
import feign.slf4j.Slf4jLogger;
import okhttp3.OkHttpClient;
public class DownloadDemo {
public static void main(String[] args) {
// create instance of okhttp3.OkHttpClient corresponded proxy
OkHttpClient client = createOkHttpClientCorrespondedProxy("yourProxyHost",
8080, "proxyUserId", "proxyPass");
// feign use proxy with authentication
DownloadApi downloadApi = Feign.builder()
// set instance of feign.Client.OkHttpClient
.client(new feign.okhttp.OkHttpClient(client))
.decoder(new FileEntityDecoder()) // use FileEntityDecoder
.logger(new Slf4jLogger()) // use Slf4j
.logLevel(Logger.Level.FULL) // setting log level to most detail
.target(DownloadApi.class, "http://www.example.com");
// call api [GET /documents/2018/pdf/sample.pdf]
FileEntity fileEntity = downloadApi.download(
"/document/2018/pdf/sample.pdf");
System.out.println(fileEntity);
}
// omitted createOkHttpClientCorrespondedProxy method
// see Corresponded Proxy
}
22:32:58.703 [main] DEBUG feign.Logger - [DownloadApi#download] ---> GET http://www.example.com/documents/2018/pdf/sample.pdf HTTP/1.1
22:32:58.705 [main] DEBUG feign.Logger - [DownloadApi#download] ---> END HTTP (0-byte body)
22:32:58.819 [main] DEBUG feign.Logger - [DownloadApi#download] <--- HTTP/1.1 200 OK (113ms)
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] accept-ranges: bytes
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] content-length: 1656363
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] content-type: application/pdf
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] date: Thu, 05 Apr 2018 13:32:56 GMT
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] proxy-connection: Keep-Alive
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] server: Apache
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download] set-cookie: server-20480-%3Fmntdlb01%3Fsfarm-web_ap=ECBBGDAKFAAA; Path=/
22:32:58.820 [main] DEBUG feign.Logger - [DownloadApi#download]
22:32:59.678 [main] DEBUG feign.Logger - [DownloadApi#download] Binary data
22:32:59.678 [main] DEBUG feign.Logger - [DownloadApi#download] <--- END HTTP (1656363-byte body)
FileEntity [status=200, headers={accept-ranges=[bytes], content-length=[1656363], content-type=[application/pdf], date=[Thu, 05 Apr 2018 13:32:56 GMT], proxy-connection=[Keep-Alive], server=[Apache], set-cookie=[server-20480-%3Fmntdlb01%3Fsfarm-web_ap=ECBBGDAKFAAA; Path=/]}, reason=OK, file=C:\Users\feign\AppData\Local\Temp\download4121273495791641378.tmp]
6. さいごに
今回はFeign
でファイルダウンロードを実現するため、独自のDecoder
を利用する方法について説明しました。
次回はFeign
でファイルアップロードを実現する方法について説明したいと思います。