Spring MVC ContentNegotiation内容协商机制(一个请求路径返回多种数据格式)源码解析

本篇博客讲解的内容是基于Spring4和Servlet3.0的环境,无配置文件形式的Spring框架。使用java配置和使用xml文件配置实质是一样的,原理都不变。先看ContentNegotiation配置:

@Configuration
@EnableWebMvc
public class MvcContextConfig extends WebMvcConfigurerAdapter {

    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.useJaf(false).favorPathExtension(false).favorParameter(true).parameterName("mediaType")
            .ignoreAcceptHeader(true).defaultContentType(MediaType.APPLICATION_JSON);
    }
}

通过@EnableWebMvc注解引入了DelegatingWebMvcConfiguration类,其父类是WebMvcConfigurationSupport,其内部定义了一个Bean:

    @Bean
    public ContentNegotiationManager mvcContentNegotiationManager() {
        if (this.contentNegotiationManager == null) {
            ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
            configurer.mediaTypes(getDefaultMediaTypes());
            configureContentNegotiation(configurer);
            try {
                this.contentNegotiationManager = configurer.getContentNegotiationManager(); //生成内容协商管理器
            }
            catch (Exception ex) {
                throw new BeanInitializationException("Could not create ContentNegotiationManager", ex);
            }
        }
        return this.contentNegotiationManager;
    }

因此Spring会在容器中生成一个beanName是mvcContentNegotiationManager的Bean,类型是ContentNegotiationManager ,在实例化这个Bean的过程中,会调用我们在MvcContextConfig 对其的配置方法。

接下来看我们在配置ContentNegotiationManager时是如何发生作用的!

public class ContentNegotiationConfigurer {

    private final ContentNegotiationManagerFactoryBean factory =
            new ContentNegotiationManagerFactoryBean();

    private final Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();

    public ContentNegotiationConfigurer useJaf(boolean useJaf) {
        this.factory.setUseJaf(useJaf);
        return this;
    }

    public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) {
        this.factory.setFavorPathExtension(favorPathExtension); //是否通过路径指定返回数据类型
        return this;
    }

    public ContentNegotiationConfigurer favorParameter(boolean favorParameter) {
        this.factory.setFavorParameter(favorParameter); //是否使用url上的参数来指定数据返回类型
        return this;
    }

    public ContentNegotiationConfigurer parameterName(String parameterName) {
        this.factory.setParameterName(parameterName); //设置url上的参数名称
        return this;
    }

    public ContentNegotiationConfigurer ignoreAcceptHeader(boolean ignoreAcceptHeader) {
        this.factory.setIgnoreAcceptHeader(ignoreAcceptHeader); //是否忽略HttpHeader上的Accept指定
        return this;
    }

    public ContentNegotiationConfigurer defaultContentType(MediaType defaultContentType) {
        //设置一个默认的返回内容形式,当未明确指定返回内容形式时,使用此设置
        this.factory.setDefaultContentType(defaultContentType);
        return this;
    }

}

我们通过configurer.useJaf(false).favorPathExtension(false).favorParameter(true).parameterName(“mediaType”).ignoreAcceptHeader(true).defaultContentType(MediaType.APPLICATION_JSON);这段代码,对ContentNegotiation做了一些配置,不使用JAF (Java Activation Framework),不使用路径上的信息来指定,使用url上的参数来指定返回的内容形式,参数的名称是mediaType,忽略HttpHeader上的Accept参数,设置默认的数据返回类型是JSON。

下面看上述的配置时如何生成内容协商管理器的,即ContentNegotiationConfigurer.getContentNegotiationManager()

public class ContentNegotiationConfigurer {

    protected ContentNegotiationManager getContentNegotiationManager() throws Exception {
        this.factory.addMediaTypes(this.mediaTypes);
        this.factory.afterPropertiesSet();  //先执行afterPropertiesSet方法
        return this.factory.getObject();
    }

}

public class ContentNegotiationManagerFactoryBean implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {

    public void afterPropertiesSet() {
        List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();

        if (this.favorPathExtension) {  //这步我们设置了false
            PathExtensionContentNegotiationStrategy strategy;
            if (this.servletContext != null && !isUseJafTurnedOff()) {
                strategy = new ServletPathExtensionContentNegotiationStrategy(
                        this.servletContext, this.mediaTypes);
            }
            else {
                strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
            }
            strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
            if (this.useJaf != null) {
                strategy.setUseJaf(this.useJaf);
            }
            strategies.add(strategy);
        }

        if (this.favorParameter) {  //设置true,因此生成了一个内容协商策略实现,同时参数名称设置了mediaType,原来是format
            ParameterContentNegotiationStrategy strategy =
                    new ParameterContentNegotiationStrategy(this.mediaTypes);
            strategy.setParameterName(this.parameterName);
            strategies.add(strategy);
        }

        if (!this.ignoreAcceptHeader) {  //这步设置了true,因此会被路过否则时获取HttpHeader上的Accept来指定返回数据格式
            strategies.add(new HeaderContentNegotiationStrategy());
        }

        if (this.defaultNegotiationStrategy != null) {  //添加了一个默认的内容协商策略实现,注意默认的策略实现位于List集合的最后一位
            strategies.add(this.defaultNegotiationStrategy);
        }
        //生成内容协商机制管理器
        this.contentNegotiationManager = new ContentNegotiationManager(strategies); 
    }


}

此时,ContentNegotiationManager这个Bean已经生成了,注册到了MVC容器中。

public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {

    private final List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();

    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
        //按strategies的顺序,使用每个策略来尝试解析,如果有一个成功了则直接返回,策略集合的最后一位是defaultContentType,
        //也就是说如果请求中没有显式的指定返回格式,则会使用默认的格式,而不会出现未指定就没格式的情况
        for (ContentNegotiationStrategy strategy : this.strategies) {
            List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
            if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
                continue;
            }
            return mediaTypes;
        }
        return Collections.emptyList();
    }

}

ContentNegotiationManager继承自ContentNegotiationStrategy,同时其内部又含有多个ContentNegotiationStrategy实例,使用了策略模式和组合模式的思想。在从一个Request请求中解析返回数据格式时,自身没有解析逻辑,而是调用其内部持有的ContentNegotiationStrategy集合来循环解析,而这个集合就是我们刚开始配置时生成的策略。

在RequestMappingHandlerAdapter执行完Controller的方法时,返回结果。如果Controller类或者相应的方法上含有@ResponseBody(@RestController)注解时,会使用RequestResponseBodyMethodProcessor来对返回的结果解析成相应的数据格式。代码看起父类AbstractMessageConverterMethodProcessor:

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {

    protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
            ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

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

    }
    //contentNegotiationManager从HttpServletRequest 中解析返回的数据格式
    private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
        List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
        return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
    }
}

当然我们默认的数据返回格式时json,那么久需要一个HttpMessageConverter来将Java Object转换成json,那么就需要MappingJackson2HttpMessageConverter这个转换器类,而这个转换器需要json依赖或者gson依赖。所以有时候会报返回json数据时出现问题,就是因为没有加上json或者gson的依赖包。一般依赖包加上了,Spring会自动检测是否存在此依赖,存在此依赖就会将MappingJackson2HttpMessageConverter注册到容器中。可以查看WebMvcConfigurationSupport的getDefaultMediaTypes()方法。

在上述配置下,如果要指定返回其他的数据格式,比如xml,则在请求url上加参数 ?mediaType=xml,我们指定的参数配置生成了一个ParameterContentNegotiationStrategy实例,ContentNegotiationManager使用此实例去解析HttpSrevletRequest请求(其父类AbstractMappingContentNegotiationStrategy从参数中解析mediaType的值,得到xml,然后找到xml对应的MediType)。

    原文作者:Spring MVC
    原文地址: https://blog.csdn.net/qq_27529917/article/details/78447806
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞