本篇博客讲解的内容是基于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)。