♨️

Spring BootのMediaTypeと対応クラスの決まり方

2022/07/21に公開

概要

Kotlinに不慣れだったため、Spring BootのRouterFunctionでjpegイメージをByteの配列で返す際、KotlinのArray<Byte>ByteArray
の違いで上手くいかなかった。
それを調べている際にSpring BootのMediaTypeと対応クラスの決まり方について少し調べたため、備忘録として記事に残しておく。

冒頭の課題の結論としては画像を返したければByteArrayを使えば良く、Array<Byte>ではダメな理由は対応するEncoderが無いためだと思われる (ここでタイトルが関わってくる)。

fooService.getFoo()Array<Byte>もしくはByteArrayで画像データを返すメソッドとする。

失敗

@Component
class FooHandler(private val fooService: FooService) {

    fun getFoo(req: ServerRequest): Mono<ServerResponse> {
        return ok()
            .contentType(MediaType.IMAGE_JPEG)
            .body(
                fooService.getFoo(),
                Array<Byte>::class.java
            )
    }
}

org.springframework.web.reactive.function.UnsupportedMediaTypeExceptionがthrowされて失敗する。

レスポンスとしては500エラーで

Content type 'image/jpeg' not supported for bodyType=java.lang.Byte[]

と言われる。

成功

@Component
class FooHandler(private val fooService: FooService) {

    fun getFoo(req: ServerRequest): Mono<ServerResponse> {
        return ok()
            .contentType(MediaType.IMAGE_JPEG)
            .body(
                fooService.getFoo(),
                ByteArray::class.java // ←ここ
            )
    }
}

理由

KotlinのArrayとJavaの配列

https://stackoverflow.com/questions/9457942/difference-between-bytearray-and-arraybyte-in-kotlin

https://kotlinlang.org/docs/java-interop.html#java-arrays

上記リンクによれば、以下のような対応になっているとのこと。

Kotlin Java
Array<Byte> Byte[]
ByteArray byte[]

パフォーマンスを目的にJavaのプリミティブ型にはそれぞれ特化したクラスが用意されているらしい。

Spring BootのMediaTypeと対応クラス

そもそもByte[]で何故使えないのかが気になった。

https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/function/UnsupportedMediaTypeException.html#UnsupportedMediaTypeException-org.springframework.http.MediaType-java.util.List-org.springframework.core.ResolvableType-

これが使われているコードを見てみると、

https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/http/codec/HttpMessageWriter.html#getWritableMediaTypes--

このメソッドによって対応するMediaTypeが決まっていた。
Byte[]が対応するMediaTypeをIntelliJのdebuggerで調べてみると以下の四つだった。

  • application/json
  • application/*+json
  • application/x-ndjson
  • text/event-stream

これは

https://github.com/spring-projects/spring-framework/blob/8ccf05adeefa3fd2a377eba067a156ea163dd616/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java#L388

このあたりで、用意されているEncoderクラスを回してencodeできるMediaTypeのみを抜き出すことで決まっている。

例えばEncoderの一つであるJackson2JsonEncoderを見ると、親クラスであるJackson2CodecSupportで以下のようにサポートしているMediaType
が定義されており、

https://github.com/spring-projects/spring-framework/blob/8ccf05adeefa3fd2a377eba067a156ea163dd616/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java#L78-L81

これとcanEncode()の実装で対応MediaTypeか否かが決まる。

https://github.com/spring-projects/spring-framework/blob/8ccf05adeefa3fd2a377eba067a156ea163dd616/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java#L108-L141

具体的には、

  • 指定MediaTypeがサポートに入っていて、
  • 指定MediaTypeと指定classに対応するObjectMapperが登録されていて、
  • そのObjectMapperでserializeできる

場合にMediaTypeが対応しているかが決まるようである。

まとめ

画像を返したいならArray<Byte>ではなくByteArray を使えば良い。

理由はKotlinのArray<Byte>がJavaのByte[]に対応しており、Spring Bootで定義されるEncoderがByte[]に対応していないから。

その他

https://github.com/spring-projects/spring-framework/blob/8ccf05adeefa3fd2a377eba067a156ea163dd616/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java#L388

ここFooWriterクラスを回しているのにreaderという変数名になってることに気づいた。

Discussion