tokuhirom's blog

@RequestMapping の produces を指定するとどうなるか

@RequestMappingproduces = MediaType.APPLICATION_JSON_UTF8_VALUE を指定したときの効果について。指定すべきかどうなのか

結論

  • つけなくても実害はない
  • XML シリアライザが依存に入っている場合、Accept: application/xml のようにヘッダが入っている場合、XML でレスポンスが返却される。
  • ほとんどの場合は produces 指定するかどうか指定するよりも HttpMessageConverter を明示するようにしたほうが良い。
  • 公開される API の場合には HttpMessageConverter を明示的に JSON 以外出力されないように設定しておくほうが良いかもしれない
    • Accept:application/xml を利用していてそれに依存する利用者がいると困る、という程度だが……

produces を指定した場合としない場合で実際どう挙動が変わるのか

以下のようなコントローラを実装する。


    @ResponseBody
    @GetMapping(value = "/json", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Map<String, String> json() {
        return ImmutableMap.of("a", "b");
    }

    @ResponseBody
    @GetMapping(value = "/musitei")
    public Map<String, String> musitei() {
        return ImmutableMap.of("c", "d");
    }

produces 指定がある場合、許可されていない Acccept ヘッダでリクエストすると 406 Not Acceptable が返却される。

$ curl -v http://localhost:8080/json
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 27 Sep 2016 21:47:24 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
{"a":"b"}

$ curl -v -H 'Accept: application/xml' http://localhost:8080/json
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/xml
> 
< HTTP/1.1 406 Not Acceptable
< Date: Tue, 27 Sep 2016 21:47:02 GMT
< Content-Type: application/xml;charset=UTF-8
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
<Map><timestamp>1475012826311</timestamp><status>406</status><error>Not Acceptable</error><exception>org.springframework.web.HttpMediaTypeNotAcceptableException</exception><message>Not Acceptable</message><path>/json</path></Map>

無指定な場合、Accept: application/xml を指定すると、XML が返却される。

$ curl -v http://localhost:8080/musitei
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /musitei HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 27 Sep 2016 21:48:51 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
{"c":"d"
$ curl -v -H 'Accept: application/xml' http://localhost:8080/musitei
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /musitei HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/xml
> 
< HTTP/1.1 200 OK
< Date: Tue, 27 Sep 2016 21:48:30 GMT
< Content-Type: application/xml;charset=UTF-8
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
<Map><c>d</c></Map>

どのように判別されているのか

以下のあたりで、Accept ヘッダで許容可能と指定されている content-type とサーバーの設定で利用可能な HttpMessageConverter の突き合わせが行われ、利用可能ならシリアライザが使われる。

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse) とかにデバッグポイントしかけて見てみると、そのへんの挙動がわかる。

        HttpServletRequest request = inputMessage.getServletRequest();
        List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
        List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

        if (outputValue != null && producibleMediaTypes.isEmpty()) {
            throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
        }

        Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
        for (MediaType requestedType : requestedMediaTypes) {
            for (MediaType producibleType : producibleMediaTypes) {
                if (requestedType.isCompatibleWith(producibleType)) {
                    compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
                }
            }
        }
        if (compatibleMediaTypes.isEmpty()) {
            if (outputValue != null) {
                throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
            }
            return;
        }

利用可能な HttpMessageConverters はどこで決定されているのか

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters です。

    protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        stringConverter.setWriteAcceptCharset(false);

        messageConverters.add(new ByteArrayHttpMessageConverter());
        messageConverters.add(stringConverter);
        messageConverters.add(new ResourceHttpMessageConverter());
        messageConverters.add(new SourceHttpMessageConverter<Source>());
        messageConverters.add(new AllEncompassingFormHttpMessageConverter());

        if (romePresent) {
            messageConverters.add(new AtomFeedHttpMessageConverter());
            messageConverters.add(new RssChannelHttpMessageConverter());
        }

        if (jackson2XmlPresent) {
            ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build();
            messageConverters.add(new MappingJackson2XmlHttpMessageConverter(objectMapper));
        }
        else if (jaxb2Present) {
            messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        }

        if (jackson2Present) {
            ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build();
            messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper));
        }
        else if (gsonPresent) {
            messageConverters.add(new GsonHttpMessageConverter());
        }
    }

AtomFeedHttpMessageConverter と RssChannelHttpMessageConverter については無視して良い。以下のように、特定のクラスのインスタンスの場合のみ動作するからだ。

public class AtomFeedHttpMessageConverter extends AbstractWireFeedHttpMessageConverter<Feed> {

    public AtomFeedHttpMessageConverter() {
        super(new MediaType("application", "atom+xml"));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return Feed.class.isAssignableFrom(clazz);
    }

}

そういうわけで、問題は XML シリアライザが依存に入ってきたケースに挙動が変わるという点のみが問題となる。

produces を指定すべきか

そもそも produces を指定すべきかという点についてだが、atom, xml, rss 等を明示的に返却したい場合については指定したほうが良いかもしれない(別にしなくてもいい気もする)。 String などの raw type を JSON で返したい場合、StringHttpMessageConverter が優先されてしまうため明示的に指定しないといけない。

しかし大枠でいうと、JSON で返す API 全てに produces 属性を指定するとコードがゴチャゴチャするので HttpMessageConverter から除外する、などの対応を取ったほうがいいと思う。

Created: 2016-09-27 21:37:04 +0000
Updated: 2016-09-27 21:37:04 +0000