给理想留点时间,熬过低谷,繁华自现。
一、场景:返回数据对象带有时间类型属性
返回数据结构如下:(ps:SpringBoot版本为2.0.3-RELEASE)
@Setter
@Getter
public class DateResponse implements Serializable {
private LocalDate localDate = LocalDate.now();
private LocalDateTime localDateTime = LocalDateTime.now();
private LocalTime localTime = LocalTime.now();
private Date date = new Date();
}
二、出现的问题
请求返回的数据如下:
{
"localDate": [
2020,
6,
20
],
"localDateTime": [
2020,
6,
20,
11,
46,
55,
810000000
],
"localTime": [
11,
46,
55,
810000000
],
"date": 1592624815810
}
Java8的时间类型都变成了数组,Date则变成了时间戳。这和我们期望的不符,我们期望能返回如下的结果:
{
"localDate": "2020-06-21",
"localDateTime": "2020-06-21 10:41:33",
"localTime": "10:41:33",
"date": "2020-06-21 10:41:33"
}
三、分析问题
官方文档里的介绍如下所示
Spring MVC已经为我们提供了一些默认的HttpMessageConverters 来对HTTP请求内容进行转换,但显然这些默认的Converters不是我们需要的。
继续往下看,发现Spring还提供了几种方法来让我们自定义Converter
按照上面说的,可以通过自定义Jackson2ObjectMapperBuilderCustomizer、ObjectMapper、Jackson2ObjectMapperBuilder、MappingJackson2HttpMessageConverter 等Bean来自定义转换格式。但是按照文档里面的解释,例如按照自定义ObjectMapper方式,会禁用ObjectMapper的所有自动配置。综合考虑,还是选择自定义MappingJackson2HttpMessageConverter的方式。
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(objectMapper());
return mappingJackson2HttpMessageConverter;
}
private ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 不序列化null的属性
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(Constants.DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(Constants.DEFAULT_DATE_FORMAT)));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(Constants.DEFAULT_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(Constants.DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(Constants.DEFAULT_DATE_FORMAT)));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(Constants.DEFAULT_TIME_FORMAT)));
objectMapper.registerModule(javaTimeModule).registerModule(new ParameterNamesModule());
objectMapper.setDateFormat(new SimpleDateFormat(Constants.DEFAULT_DATE_TIME_FORMAT));
return objectMapper;
}
static class Constants {
/**
* 默认日期时间格式
*/
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* 默认日期格式
*/
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
/**
* 默认时间格式
*/
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
}
通过如上的配置,我们可以得到我们期望的返回了。
但是,问题又来了。
如果我们在程序里继承了WebMvcConfigurationSupport, 那么我们得到的返回又会是像最开始的那样。这是为什么呢?
官方文档有这样的介绍:
仔细看第二段话,问题好像出现在WebMvcConfigurationSupport这里。我们调试一下程序来看看到底是什么情况。
没有继承WebMvcConfigurationSupport的时候,我们看看WebMvcConfigurationSupport里的Converters有哪些。
在如图所示的两个地方打上断点。
第一个断点我们可以得到如下信息(注意这个@5484):
这个messageConverters是WebMvcConfigurationSupport里的一个属性,Spring通过这个属性里的Converters来对HTTP请求数据进行转换。我们可以看到,下标为7的那个Converter就是我们上面自定义的Converter。所以这个时候,数据是按照我们定的格式返回的。
如果继承了WebMvcConfigurationSupport之后呢?
如上图所示,这些Bean的序号都是连着的,我们自定义的Converter根本没有加入进去。不过文档也说了,可以通过configureMessageConverters这个方法将我们自定义的Converter加入进去。我们来试一下。
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
}
好了,这下messageConverters里面就一个我们自定义的Converter,发起请求,数据按照我们期待的格式返回了。但是会有个疑问,其余的那些Converter都没有了,会不会有什么影响。目前我也不知道会有什么影响>_<,但是看WebMvcConfigurationSupport的源码,发现了一个方法addDefaultHttpMessageConverters,这个方法在什么时候被调用呢?WebMvcConfigurationSupport还有另一个方法getMessageConverters,这个方法源码如下:
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList();
this.configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
this.addDefaultHttpMessageConverters(this.messageConverters);
}
this.extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
这说明了,如果我们通过configureMessageConverters方法加入自定义的Converter,Spring提供的默认的Converters就不会加入到messageConverters里了(其实官方文档有这么一句话就说明了这个问题: However, unlike with normal MVC, you can supply only additional converters that you need (because Spring Boot uses the same mechanism to contribute its defaults).)。再往下看,发现了另一个方法extendMessageConverters,顾名思义,这个方法是扩展messageConverters的。那我们是不是可以实现extendMessageConverters这个方法来只加入我们自定义的Converter而不改变默认的配置呢。动手一试!
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//需要注释掉configureMessageConverters的实现
converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
}
然而,这样配置了,发现返回的数据,时间类型的又变成了数组和时间戳。这是为什么呢。再次打断点调试。
断点打在extendMessageConverters里,得到结果如下
可以看到,我们自定义的Converter尚未加入到converters里,而converters已经有了8个默认的Converter了,继续下一步。
现在messageConverters里一共9个Converter,最后一个(5495)是我们自定义的。看来问题就出现在这里了,我们自定义的Converter被放在了最后面。而messageConverters是个ArrayList类型的,它是顺序访问的。知道问题的所在,那就好办了。把默认的MappingJackson2HttpMessageConverter去掉或者将自定义的MappingJackson2HttpMessageConverter放到第一个就好了。重新实现extendMessageConverters方法如下:
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 去除掉默认的MappingJackson2HttpMessageConverter,加入自定义的MappingJackson2HttpMessageConverter
converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
// 还有种方式:converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper()))
converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
}
按照上述方式实现之后,我们就可以得到期望的返回格式了。
四、总结一下
出现这个问题的时候,在网上索搜了很多文章,但要么是通过注解一个个去格式化,要么就是最开始的那种,自定义Jackson2ObjectMapperBuilderCustomizer、ObjectMapper、Jackson2ObjectMapperBuilder、MappingJackson2HttpMessageConverter 等Bean来解决。正常情况下,这些方式都是能解决问题的。但是不够全面,就比如当我们自己实现了WebMvcConfigurationSupport的时候,第二种方式就不生效了。最终通过打断点调试才发现了问题所在。其实这些问题在官方文档中都有提及到。所以,没事多去读读官方文档,看看源码,还是有好处的。