WebMvcAutoConfiguration
对该自动配置类一句话解释:通过EnableAutoConfiguration
方式代替@EnableWebMvc
。
说明:在
Spring Boot
环境下,强烈不建议你启用@EnableWebMvc
注解
@Configuration @ConditionalOnWebApplication ... // 若你开启了`@EnableWebMvc`,该自动配置类就不生效啦 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) public class WebMvcAutoConfiguration { // 对WebMvcConfigurerAdapter你肯定不陌生:它是你定制MVC的钩子(WebMvcConfigurer接口) // EnableWebMvcConfiguration继承自DelegatingWebMvcConfiguration,效果同@EnableWebMvc呀 @Configuration @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { ... // 通过构造器注入(请注意使用了@Lazy,这是很重要的一个技巧) private final HttpMessageConverters messageConverters; ... // 把容器内所有的消息转换器,全部添加进去,让它生效 @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.addAll(this.messageConverters.getConverters()); } ... } }
容器内的所有的消息转换器,最终通过WebMvcConfigurer#configureMessageConverters()这个API被放进去,为了更方便同学们理解,再回顾下这段代码:
WebMvcConfigurationSupport: protected final List<HttpMessageConverter<?>> getMessageConverters() { if (this.messageConverters == null) { this.messageConverters = new ArrayList<HttpMessageConverter<?>>(); configureMessageConverters(this.messageConverters); if (this.messageConverters.isEmpty()) { addDefaultHttpMessageConverters(this.messageConverters); } extendMessageConverters(this.messageConverters); } return this.messageConverters; }
so,如果你自己定义了消息转换器,那么messageConverters将不再是empty,所以默认的那些转换器们(包括默认会装配的MappingJackson2HttpMessageConverter)也就不会再执行了。
但是,你可千万不要轻易得出结论:Spring Boot下默认只有两个消息转换器。请看如下代码:
HttpMessageConverters构造器: // 这里的addDefaultConverters()就是把默认注册的那些注册好 // 并且最终还有个非常有意思的Combined动作 public HttpMessageConverters(boolean addDefaultConverters, Collection<HttpMessageConverter<?>> converters) { List<HttpMessageConverter<?>> combined = getCombinedConverters(converters, addDefaultConverters ? getDefaultConverters() : Collections.<HttpMessageConverter<?>>emptyList()); combined = postProcessConverters(combined); this.converters = Collections.unmodifiableList(combined); }
Spring Boot
它不仅保留了默认的消息转换器们,保持最大的向下兼容能力,同时还让你定义的Bean也能加入进来。最终拥有的消息转换器我截图如下:
可以看见:MappingJackson2HttpMessageConverter和StringHttpMessageConverter均出现了两次(HttpMessageConverters内部有个顺序的协商,有兴趣的可自行了解),但是@Bean方式进来的均在前面,所以会覆盖默认行为。
出现差异的根本原因
最后的最后,终于轮到解答如标题"险些暴雷"疑问的根本原因了。解答这个原因本身其实非常简单,展示不同版本JacksonAutoConfiguration的源码对比一看便知:
1.5.22.RELEASE版本:
@Configuration @ConditionalOnClass(ObjectMapper.class) public class JacksonAutoConfiguration { ... // 无static静态代码块 }
2.0.0.RELEASE
版本:
@Configuration @ConditionalOnClass(ObjectMapper.class) public class JacksonAutoConfiguration { private static final Map<?, Boolean> FEATURE_DEFAULTS; static { Map<Object, Boolean> featureDefaults = new HashMap<>(); featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults); } ... }
Jackson默认是开启SerializationFeature.WRITE_DATES_AS_TIMESTAMPS这个特征值的,所以它对时间类型的序列化方式是用时间戳方式。
1.x并没有对Jackson默认行为做更改,而自2.0.0.RELEASE版本起,Spring Boot默认把此特征值给置为fasle了。小小改动,巨大能量,险些让我项目暴雷。
说明:因我写的脚手架多个团队使用,因此向下兼容能力及其重要
解决方案
虽然说这对Spring Boot本身不是问题,但是如果你想要向下兼容这便成了问题。
定位到了问题所在,从来不缺解决方案。若你仍旧像保持之前的序列化数据格式,你可以这么做(提供两种方案以供参考):
- 增加属性spring.jackson.serialization.write-dates-as-timestamps=true
- [享学Jackson] 专栏里有讲述,此属性值的优先级高于静态代码块,所以这么做是有效的
- 自定义一个Jackson2ObjectMapperBuilderCustomizer(保证在默认的定制器之后执行即可)
小知识点:1.x和2.x动态代理方式的差异
1.x默认使用的JDK动态代码方式
2.x默认使用的CGLIB的动态代理方式
需要注意的是这和Spring无差别,Spring一直默认的就是JDK代理方式,而是Spring在其基础上做了定制,详情请参见AopAutoConfiguration自动配置类,它在1.x和2.x的表现是不一样的。
总结
本篇文章作为采坑指导系列具有很强的现实意义,如果你现正处在1.x升到2.x的状态,那么本文应该能对你有些帮助。这次遇到的问题,作为程序员我们应该能得出如下总结:
- 一定要有版本意识,一定要有版本意识,一定要有版本意识
- 序列化/反序列化是特别敏感的一个知识点,平时很少人关注所以容易导致出了问题就摸瞎,建议团队内有专人研究
- 小小改动,往往具有大大能量。对未知应具有敬畏之心,小心为之。