Feignでファイルアップロードを実現する方法

## 1. はじめに

[前回](https://qiita.com/5zm/items/940195d776497f70f683)は`Feign`でファイルダウンロードを実現する方法について説明しました。
今回は`Feign`でファイルアップロードを実現する方法について説明したいと思います。
ダウンロードでは独自の`Decoder`を実装しましたが、アップロードは[feign-form](https://github.com/OpenFeign/feign-form)という専用のモジュールが用意されているためこれを利用します。なお、[GitHubの公式ページ](https://github.com/OpenFeign/feign-form)には以下のように説明されています。

> This module adds support for encoding application/x-www-form-urlencoded and multipart/form-data forms.

## 2. ライブラリの用意

`feign-form`は別モジュールのため依存関係に追加します。`Feign`を利用する際の基本的な`pom.xml`については[別の記事](https://qiita.com/5zm/items/f7a48e80ad5ba4316748)を参照してください。

```xml:pom.xml
    <!-- add feign-form for multipart : start -->
    <dependency>
        <groupId>io.github.openfeign.form</groupId>
        <artifactId>feign-form</artifactId>
        <version>3.3.0</version>
    </dependency>
    <!-- add feign-form for multipart : end -->
```

## 3. APIのインターフェースの定義

APIのインターフェースを定義します。ファイルアップロードのデータ変換処理（シリアライズ）が可能な引数のデータ型として以下に示す３種類があります。

* `java.io.File`　：　アップロードファイルを`File`として設定する
* `byte[]`　：　アップロードファイルのファイルデータを`byte[]`として設定する
* `feign.form.FormData`　：　アップロードファイルを専用のクラスで設定する

サンプルのため、今回は３種類のデータ型を利用してみたいと思います。

```java:UploadApi.java
package com.example.feign.demo.upload;

import java.io.File;

import feign.Headers;
import feign.Param;
import feign.RequestLine;
import feign.form.FormData;

public interface UploadApi {

    @RequestLine("POST /data/upload")
    @Headers("Content-Type: multipart/form-data")
    String upload(@Param("remarks") String remarks, @Param("uploadFile") File uploadFile);

    @RequestLine("POST /data/upload")
    @Headers("Content-Type: multipart/form-data")
    String upload(@Param("remarks") String remarks, @Param("uploadFile") byte[] uploadFile);

    @RequestLine("POST /data/upload")
    @Headers("Content-Type: multipart/form-data")
    String upload(@Param("remarks") String remarks, @Param("uploadFile") FormData uploadFile);
}
```

**注意点になりますが、３種類で全て同じ動作にはなりません。APIの提供（サーバ）側の仕様を確認し、適切なものを利用してください。**

| データ型 | Content-Disposition:のfilename | Content-Type: |
|:----:|:----------|:-------------|
| `java.io.File` | ○：設定される | ○：設定される（拡張子より自動判定） |
| `byte[]` | ×：設定されない | ×：設定されない<br>一律`application/octet-stream`となる |
| `feign.form.FormData` | ×：設定されない | ○：設定される（プログラムで明示的に設定） |

## 4. 使い方

ポイントとしては`encoder`メソッドに`feign.form.FormEncoder`クラスのインスタンスを設定するくらいです。他は`Feign`の基本的な使い方と同じです。

```java:DownloadDemo.java
package com.example.feign.demo.upload;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import feign.Feign;
import feign.Logger;
import feign.form.FormData;
import feign.form.FormEncoder;
import feign.jackson.JacksonEncoder;
import feign.okhttp.OkHttpClient;
import feign.slf4j.Slf4jLogger;

public class UploadDemo {

    public static void main(String[] args) {
        // 1. create instance of api interface with feign
        UploadApi uploadApi = Feign.builder()
                .client(new OkHttpClient())
                // use FormEncoder with JacksonEncoder
                .encoder(new FormEncoder(new JacksonEncoder()))
                // .encoder(new FormEncoder())
                .logger(new Slf4jLogger())
                .logLevel(Logger.Level.FULL)
                .target(UploadApi.class, "http://localhost:3000");

        // 2. call api
        // File
        File uploadFile = new File("C:/tmp/test.png");
        String remarks = "I send a image file.";
        String result = uploadApi.upload(remarks, uploadFile);
        System.out.println(result);

        try (FileInputStream iStream = new FileInputStream(uploadFile);) {
            // byte[]
            byte[] uploadFileByte = new byte[(int) uploadFile.length()];
            iStream.read(uploadFileByte);
            String result2 = uploadApi.upload(remarks, uploadFileByte);
            System.out.println(result2);
            // FormData
            FormData formData = new FormData("image/png", uploadFileByte);
            String result3 = uploadApi.upload(remarks, formData);
            System.out.println(result3);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

## 5. ３種類の違いを実装コードで説明（おまけ）

[feign-form](https://github.com/OpenFeign/feign-form)の実装コードは公開されているので、これを見れば３種類の動作の違いが分かります。
`FormData`は`filename`のフィールドを持たせてあげれば解決するのですが、なぜ保持していないのか謎です。（`contentType`は保持しているのに）

### 5.1. File

https://github.com/OpenFeign/feign-form/blob/master/feign-form/src/main/java/feign/form/multipart/SingleFileWriter.java

```java:SingleFileWriter.javaで親クラス（AbstractWriter）のwriteFileMetadataメソッドを呼び出している箇所
writeFileMetadata(output, key, file.getName(), null);
```
https://github.com/OpenFeign/feign-form/blob/master/feign-form/src/main/java/feign/form/multipart/AbstractWriter.java

```java:AbstractWriter.javaのwriteFileMetadataメソッド
  @SneakyThrows
  protected void writeFileMetadata (Output output, String name, String fileName, String contentType) {
    val contentDesposition = new StringBuilder()
        .append("Content-Disposition: form-data; name=\"").append(name).append("\"; ")
        .append("filename=\"").append(fileName).append("\"")
        .toString();

    String fileContentType = contentType;
    if (fileContentType == null) {
      if (fileName != null) {
        fileContentType = URLConnection.guessContentTypeFromName(fileName);
      }
      if (fileContentType == null) {
        fileContentType = "application/octet-stream";
      }
    }

    val string = new StringBuilder()
        .append(contentDesposition).append(CRLF)
        .append("Content-Type: ").append(fileContentType).append(CRLF)
        .append("Content-Transfer-Encoding: binary").append(CRLF)
        .append(CRLF)
        .toString();

    output.write(string);
  }
```

つまり`File`の場合は以下のようになります。

* `Content-Disposition:`の`filename`に`file.getName()`が設定される
* `Content-Type:`に`URLConnection.guessContentTypeFromName(fileName)`が設定される

### 5.2. byte[]

https://github.com/OpenFeign/feign-form/blob/master/feign-form/src/main/java/feign/form/multipart/ByteArrayWriter.java

```java:ByteArrayWriter.javaで親クラス（AbstractWriter）のwriteFileMetadataメソッドを呼び出している箇所
writeFileMetadata(output, key, null, null);
```

つまり`byte[]`の場合は以下のようになります。

* `Content-Disposition:`の`filename`に`null`が設定される
* `Content-Type:`に`application/octet-stream`が設定される

### 5.3. FormData

https://github.com/OpenFeign/feign-form/blob/master/feign-form/src/main/java/feign/form/multipart/FormDataWriter.java

```java:FormDataWriter.javaで親クラス（AbstractWriter）のwriteFileMetadataメソッドを呼び出している箇所
writeFileMetadata(output, key, null, formData.getContentType());
```

つまり`FormData`の場合は以下のようになります。

* `Content-Disposition:`の`filename`に`null`が設定される
* `Content-Type:`に`formData.getContentType()`が設定される

## 6. さいごに

今回は`Feign`でファイルアップロードを実現する方法について説明しました。モジュールが用意されているため、ダウンロードよりも簡単だったかと思います。