はじめに
multipart/form-dataなAPIを呼び出す実装が一番面倒だと思っているのですが、自前でゴリゴリと実装している人が意外と多い印象です。
Retrofit+OkHttpを使ったらどれぐらい簡単なのか試してみたのですがハマり所もあったので記事していきます。
ハマりどころ
@retrofit2.http.Multipart
@retrofit2.http.Part
@retrofit2.http.Body
okhttp3.MultipartBody
okhttp3.MultipartBody.Part
okhttp3.RequestBody
あたりを組み合わせて実装するのですが、まずい組み合わせでハマったり、必要なリクエストヘッダーが指定できそうで出来なかったりと使い方が分かるまで結構大変でした。
StackOverFlowでも反応が多いのでハマっている人の多さが伺い知れます。
まずやっておいた方が良いこと
どんなリクエストデータが作成されたのか見れないとデバッグもままならないのでリクエストボディをログに出すようにInterceptorを仕込んでおくことをお勧めします。
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build();
これによって、生のリクエスト内容がログに出るので何が起きているか把握できるようになります。
ちなみにmultipart/form-dataのバウンダリーや改行がどう入るのかを見れるので勉強にもなるかと思います。
2021-12-04 22:43:08.014 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : --> POST http://localhost:8080/upload3
2021-12-04 22:43:08.014 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : Content-Type: multipart/form-data; boundary=1f2d4b84-5503-43b1-aa79-2e46bd64074f
2021-12-04 22:43:08.014 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : Content-Length: 343
2021-12-04 22:43:08.015 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient :
2021-12-04 22:43:08.015 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : --1f2d4b84-5503-43b1-aa79-2e46bd64074f
Content-Disposition: form-data; name="json"
Content-Length: 14
{"param": "1"}
--1f2d4b84-5503-43b1-aa79-2e46bd64074f
Content-Disposition: form-data; name="file"; filename="test.csv"
Content-Type: text/csv
Content-Length: 24
no,name
1,taro
2,hanako
--1f2d4b84-5503-43b1-aa79-2e46bd64074f--
2021-12-04 22:43:08.015 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : --> END POST (343-byte body)
実装方法その1
MutliPartBodyオブジェクトを自分で組み立てて@Body
で渡すやり方。
後で出てくる@Multipart
と@Body
は組み合わせられないのですが、最初はそれをやろうとしてハマりました。
interface UploadService {
@POST("upload")
Call<Map<String, String>> upload(@Body MultipartBody multipartBody);
}
次に呼び出し方。
MutliPartBody.Builder.addFormDataPart
を使ってマルチパートのボディ部分を作るのが一番良さそうでした。
それに気づくまではaddPart
を使ったり試行錯誤したのですが、思った通りのリクエスト内容にならなくて結構ハマりました。
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM) //デフォルトはmultipart/mixedなのでmultipart/form-dataに設定し直す
// Content-Typeを指定したくて試行錯誤した結果がこちら
// filenameにnullを渡さないといけないのが気持ち悪いけど他に丁度いいインターフェイスがない
.addFormDataPart("json", null,
RequestBody.create(MediaType.get("application/json"), "{\"param\": \"1\"}"))
.addFormDataPart("file", "test.csv",
RequestBody.create(MediaType.get("text/csv"), new File("/tmp/test.csv")))
.build();
service.upload(multipartBody).execute();
実装方法その2
今度は@Multipart
を使うパターンです。
前述した通り@Body
とは組み合わせられないので@Part
を使います。
interface UploadService2 {
@Multipart
@POST("upload3")
Call<Map<String, String>> upload(@Part MultipartBody.Part json, @Part MultipartBody.Part file);
}
MultipartBody.Partを作ってAPIに渡します。作り方はその1と同じような感じでOKです。
MultipartBody.Part json = MultipartBody.Part.createFormData("json", null,
RequestBody.create(MediaType.get("application/json"), "{\"param\": \"1\"}"));
MultipartBody.Part file = MultipartBody.Part.createFormData("file", "test.csv",
RequestBody.create(MediaType.get("text/csv"), new File("/tmp/test.csv")));
UploadService2 service2 = retrofit.create(UploadService2.class);
service2.upload(json, file).execute();
おわり
以上、やりたいことが出来ました。
結局、かなり試行錯誤しないと実装出来ませんでしたが、やり方が分かってしまえばスッキリ短く書けました!