SpringBoot配置内容协商

本文基于 SpringBoot 2.6.3 版本,spring-webmvc 5.3.15版本

SpringMVC内容协商基本使用

官方文档:https://docs.spring.io/spring-boot/docs/2.6.3/reference/htmlsingle/#web.servlet.spring-mvc.content-negotiation

默认情况下,SpringBoot中SpringMVC接口返回的数据是json格式,但有些时候同样的数据我们可能需要根据请求来返回不同的格式。即同一个接口可以返回json格式,又可以返回xml格式。
需要额外添加如下依赖:

<dependency>
    <groupId>com.fasterxml.jackson.dataformatgroupId>
    <artifactId>jackson-dataformat-xmlartifactId>
dependency>

默认情况下SpringMVC根据Accept请求头来确定返回什么格式给客户端。所以在添加了jackson-dataformat-xml直接在浏览器访问接口返回的是xml格式的数据。如果想要返回json格式,可以手动设置Acceptapplication/json
SpringBoot配置内容协商_第1张图片
但如果http客户端无法设置Accept,可以使用请求参数format=json来指定返回的数据格式,优先级高于Accept,但是默认没有开启,需要添加如下参数开启:

spring.mvc.contentnegotiation.favor-parameter=true

SpringBoot配置内容协商_第2张图片
在spring-webmvc 5.2.4之前的版本中支持根据url后缀来返回数据格式,但是该版本开始,已经被标记过时,不推荐该方式

WebMvcConfigurationSupport中会自己推断支持哪些类型,但是对于默认不支持的类型则需要自己通过如下属性设置支持哪些类型. 和配置解析器. 如果key相同则会覆盖推断出来的.

spring.mvc.contentnegotiation.media-types.customType=customType

如下是SpringMVC默认添加的所支持的media-type:
SpringBoot配置内容协商_第3张图片

自定义内容协商

下面我们通过自定义HttpMessageConverter并配置其所支持的MediaType来返回jsonp格式的数据。
首先是创建一个JsonpHttpMessageConverter实现HttpMessageConverter

/**
 * Created by bruce on 2022/2/14 19:13
 */
public class JsonpHttpMessageConverter implements HttpMessageConverter<Object> {

    public static final MediaType JSONP = new MediaType("application", "jsonp");

    private ObjectMapper objectMapper;

    public JsonpHttpMessageConverter() {
        objectMapper = Jackson2ObjectMapperBuilder.json().build();
    }

    @Override
    public boolean canRead(Class clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        return objectMapper.canSerialize(clazz);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return List.of(JSONP);
    }

    @Override
    public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        HttpHeaders headers = outputMessage.getHeaders();
        if (headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
            headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
        }

        String data = "callback(" + objectMapper.writeValueAsString(o) + ")";

        StreamUtils.copy(data, StandardCharsets.UTF_8, outputMessage.getBody());
    }

}

接下来就是通过org.springframework.web.servlet.config.annotation.WebMvcConfigurer来配置JsonpHttpMessageConverter

@Order(1)
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        // configurer.mediaType("json", MediaType.APPLICATION_JSON);
        // configurer.mediaType("xml", MediaType.APPLICATION_XML);
        configurer.mediaType("jsonp", JsonpHttpMessageConverter.JSONP);
    }

    /**
     * @link {https://github.com/spring-projects/spring-boot/issues/21374}
     * @see org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters(java.util.List)
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //移除重复的HttpMessageConverter
        HttpMessageConverter<?> preConverter = null;
        HttpMessageConverter<?> currentConverter = null;
        Iterator<HttpMessageConverter<?>> iterator = converters.iterator();
        while (iterator.hasNext()) {
            if (preConverter == null) {
                preConverter = iterator.next();
                continue;
            }
            currentConverter = iterator.next();

            List<MediaType> preSupports = preConverter.getSupportedMediaTypes();
            List<MediaType> currentSupports = currentConverter.getSupportedMediaTypes();

            if (preConverter.getClass() == currentConverter.getClass()) {
                boolean allIn = currentSupports.stream().allMatch(item -> item.isPresentIn(preSupports));
                if (allIn) {
                    iterator.remove();
                }
            }
            preConverter = currentConverter;
        }

        //添加自己的
        JsonpHttpMessageConverter jsonpHttpMessageConverter = new JsonpHttpMessageConverter();
        converters.add(jsonpHttpMessageConverter);
    }
}

默认情况下SpringBoot中还会配置org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter@Order(1)用于指定多个WebMvcConfigurer之间的排序顺序。

默认情况下,SpringBoot对SpringMVC的自动装配会产生如下几个重复的:

  • org.springframework.http.converter.StringHttpMessageConverter
  • org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
  • org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter

SpringBoot官方解释这是正确的,详情看我提的这个issue:Duplicate httpmessageconverter

但是个人认为这是没有必要的。
因此在WebConfig中有一段是移除重复的HttpMessageConverter的逻辑
后面两行则是添加自己的JsonpHttpMessageConverter

最后在浏览器访问查看效果:
SpringBoot配置内容协商_第4张图片
如何在JsonpHttpMessageConverter中获取客户端的请求参数呢?
方案1:通过 RequestContextHolder.currentRequestAttributes()

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        String jsonpCallback = requestAttributes.getRequest().getParameter("jsonpCallback");
        System.out.println("获取http请求参数jsonpCallback=" + jsonpCallback);

方案2:通过自定义org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
具体不详讲,有需要请点赞后留言。

源码分析

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcContentNegotiationManager
SpringBoot配置内容协商_第5张图片
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#mvcContentNegotiationManager
SpringBoot配置内容协商_第6张图片

  1. 获取默认支持的MediaType,并设置到ContentNegotiationConfigurer
  2. 回调WebMvcConfigurer#configureContentNegotiation(ContentNegotiationConfigurer)接口方法,方便用户自定义ContentNegotiationConfigurer
  3. 多个WebMvcConfigurer之间可以排序。
  4. ContentNegotiationConfigurer中会创建成员变量ContentNegotiationManagerFactoryBean factory
  5. 调用ContentNegotiationConfigurer#buildContentNegotiationManager实际上就是调用ContentNegotiationManagerFactoryBean#build来创建ContentNegotiationManager对象
    SpringBoot配置内容协商_第7张图片
    我们可以通过ContentNegotiationConfigurer设置ContentNegotiationManagerFactoryBean中的一些参数,例如设置自定义支持的MediaType、设置自定义策略(ContentNegotiationStrategy)如何解析请求中MediaType等。SpringMVC中提供的策略有ParameterContentNegotiationStrategyHeaderContentNegotiationStrategy(默认)等。一般这两种策略够用了。

接下来会创建RequestMappingHandlerAdapter
springboot提供了接口org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations,只要将实现类配置成Bean就行,方便用户提供自己的如下实例:
RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver
SpringBoot配置内容协商_第8张图片
SpringBoot配置内容协商_第9张图片
SpringBoot配置内容协商_第10张图片
1.#getMessageConverters方法会回调WebMvcConfigurer#configureMessageConverters(messageConverters)
6. 开始messageConverters集合中为空,调用完WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureMessageConverters之后,会往集合设置默认支持的HttpMessageConverters
7. 我们可以通过配置实现WebMvcConfigurer接口的bean来添加自己的HttpMessageConverter,或者调整messageConverters中的HttpMessageConverter

当初次访问http地址时会执行DispatcherServlet#initHandlerAdapters方法从Context中获取HandlerAdapter
SpringBoot配置内容协商_第11张图片
SpringBoot配置内容协商_第12张图片
SpringBoot配置内容协商_第13张图片
SpringBoot配置内容协商_第14张图片
SpringBoot配置内容协商_第15张图片
SpringBoot配置内容协商_第16张图片
SpringBoot配置内容协商_第17张图片
涉及源码比较多,根据这些慢慢看吧!!!

你可能感兴趣的:(spring,boot,spring)