RestTemplateによるファイルストリームの処理と応答設計のベストプラクティス

RestTemplateの基本的な使用方法

Spring FrameworkにおけるRestTemplateは、外部HTTPエンドポイントを呼び出すための中心的なクラスです。特にマイクロサービスアーキテクチャにおいて、サービス間通信を簡素化する役割を果たします。

主なメソッドとして以下が存在します:

getForEntity(String url, Class<T> responseType, Object... uriVariables)
postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables)

getForObject(String url, Class<T> responseType, Object... uriVariables)
postForObject(String url, Object request, Class<T> responseType, Object... uriVariables)

両者の違いは戻り値にあります。ForEntity系はResponseEntity<T>を返し、HTTPステータスコード、ヘッダー、ボディをすべて取得可能です。一方ForObjectはボディのみを指定型で返します。

カスタムヘッダーを含むリクエストの送信

getForEntitypostForEntityでは直接ヘッダーを設定できません。この場合、汎用的なexchangeメソッドを使用します。

HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer token");
headers.setContentType(MediaType.MULTIPART_FORM_DATA);

HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(formData, headers);

ResponseEntity<byte[]> response = restTemplate.exchange(
    "https://api.example.com/upload",
    HttpMethod.POST,
    entity,
    byte[].class
);

ファイルストリームの受信と処理

外部APIから画像やPDFなどのバイナリデータを受け取る場合、戻り値型をbyte[]またはResourceとすることが有効です。

ResponseEntity<Resource> result = restTemplate.getForEntity(
    "https://api.example.com/export/report.pdf", 
    Resource.class
);

Resource resource = result.getBody();
InputStream inputStream = resource.getInputStream();
// ストリームを処理後、必要に応じてローカルに保存または変換

ファイルダウンロード用エンドポイントの実装パターン

自サービスがファイルを提供する際の代表的な2つのアプローチがあります。

パターンA:FileSystemResourceを直接返却

@GetMapping("/download/{id}")
public ResponseEntity<FileSystemResource> downloadFile(@PathVariable String id) {
    Path path = Paths.get("/tmp/uploads", id + ".pdf");
    FileSystemResource resource = new FileSystemResource(path);

    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"report.pdf\"")
        .contentType(MediaType.APPLICATION_PDF)
        .body(resource);
}

この方式の欠点は、レスポンス完了までファイルハンドルが解放されず、メソッド内でファイル削除ができない点です。

パターンB:HttpServletResponseを使用した手動出力

@GetMapping("/stream/{id}")
public void streamFile(@PathVariable String id, HttpServletResponse response) throws IOException {
    Path filePath = Paths.get("/tmp/uploads", id + ".pdf");
    
    if (!Files.exists(filePath)) {
        response.sendError(404, "File not found");
        return;
    }

    response.setContentType("application/pdf");
    response.setHeader("Content-Disposition", "attachment; filename=\"report.pdf\"");

    try (InputStream in = Files.newInputStream(filePath);
         OutputStream out = response.getOutputStream()) {
        in.transferTo(out);
    }

    // ストリーム送信後にファイル削除可能
    Files.deleteIfExists(filePath);
}

この方法なら、ネットワーク送信後にローカルファイルのクリーンアップが可能です。

エラーハンドリングの課題と対策

ファイルストリームを返すエンドポイントでは、HTTPステータスを200以外に設定しても、OutputStreamが開始された時点で変更できなくなります。従って、正常時も異常時もHTTP 200となることが多く、クライアント側での判別が困難になります。

これを回避するには、次の2段階方式が推奨されます:

  1. ステータス確認APIGET /files/{id}/statusでファイルの存在・準備状態をJSONで返す
  2. ファイル取得APIGET /files/{id}/contentで実際のバイナリを返す

これにより、クライアントは事前にステータスを確認した上でダウンロードを実行できるようになります。

複合リクエスト(JSON + ファイル)の処理

フロントエンドからフォームデータ(文字列フィールド+ファイル)を送信するケースでは、multipart/form-data形式を利用します。

MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("metadata", "{\"name\": \"document1\"}");
form.add("file", new FileSystemResource("/tmp/upload/temp.pdf"));

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);

HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(form, headers);

restTemplate.postForObject("https://api.example.com/submit", request, String.class);

代替案として、ファイルアップロードを独立させ、そのUUIDをフォームデータに含める方式もあります。これにより、処理の分離と再試行の容易さが向上します。

タグ: Spring RestTemplate ファイルストリーム HttpServletResponse multipart/form-data

5月28日 18:39 投稿