这几天的开发过程中,需要修改原有同事写的项目,原项目采用的是Spring Boot写的后端服务,json序列化使用原始Jackson进行,并在配置文件的properties文件中声明了Jackson的一些基本配置
# Json spring.jackson.time-zone=GMT+8 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.property-naming-strategy=SNAKE_CASE spring.jackson.deserialization.READ_UNKNOWN_ENUM_VALUES_AS_NULL=true
起初在没有加入项目的拦截器LoginInterceptor的时候,按照项目配置文件的配置,postman请求时采用属性为下划线的形式并不会报错,而且在序列化时间类型的属性时也不会出现时间格式化出问题的情况。开始对项目进行改造之后增加了登陆验证。并在Spring Boot项目启动的时候增加了Webconfig来向程序注册拦截器
@Configuration public class WebConfig extends WebMvcConfigurationSupport { // 关键,将拦截器作为bean写入配置中 @Autowired private LoginInterceptor loginInterceptor; @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).addPathPatterns("/**"); // 上传图片的路径除外 super.addInterceptors(registry); } }
做完这些之后,准备调试程序时发现,用原有的报文请求程序的接口的时候总是报错。首先是时间类型转换失败,报错信息如下:
Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Can not deserialize value of type java.util.Date from String "2018-06-12 12:00:00": not a valid representation (error: Failed to parse Date value '2018-06-12 12:00:00': Can not parse date "2018-06-12 12:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSS', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not deserialize value of type java.util.Date from String "2018-06-12 12:00:00": not a valid representation (error: Failed to parse Date value '2018-06-12 12:00:00': Can not parse date "2018-06-12 12:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSS', parsing fails (leniency? null)) at [Source: java.io.PushbackInputStream@7688ebdb; line: 1, column: 281] (through reference chain: com.hlt.cloudoak.base.ForeignApplication["order"]->com.hlt.cloudoak.base.ForeignApplication$Order["notification"]->com.hlt.cloudoak.base.ForeignApplication$Order$Notification["date"]->com.hlt.cloudoak.base.ForeignApplication$Order$Notification$HappenDate["notify"])
然后通过一通查资料,最终在Model的属性上增加了@JsonFormat
注解得以解决了这个时间格式化错误的问题。
但是事情并没有完,虽然解决了时间格式化报错的问题。但是使用下划线形式的Json请求接口依旧行不通,转化失败。程序拿不到对应属性的值。于是又是一顿某度和某歌的翻找。
最后在WebConfig类的实现中增加了代码,才使得原有的项目依旧得以采用下划线形式的Json,并且时间格式化时也并不会出错,最终代码如下:
@Configuration public class WebConfig extends WebMvcConfigurationSupport { // 关键,将拦截器作为bean写入配置中 @Autowired private LoginInterceptor loginInterceptor; @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).addPathPatterns("/**"); // 上传图片的路径除外 super.addInterceptors(registry); } @Bean public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); jsonConverter.setObjectMapper(objectMapper); return jsonConverter; } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(customJackson2HttpMessageConverter()); super.addDefaultHttpMessageConverters(converters); } }
资料上讲,自定义WebMvcConfigur之后,原有properties中的jackson配置会失效。所以必须在自定义实现类中再次对jackson的配置进行补充。查询资料的过程中,看到有的文章提到需要将注解@EnableWebMvc
去掉。但是我们的项目中并不显式的包含这个注解,相信可能有部分人跟我一样在看到这个解决方案时并不知道如何对项目进行更改。
摘自:
工作中,自己的解决方案
1、去掉配置文件application.yml
中,关于jackson
的配置(反正配置了也不起作用);
2、单独新建一个@Configuration
类:JacksonConfig.java —— 【后记】最后这种方案也不行,因为只能有1个WebMvcConfigurationSupport
实例,后面把它追加到已有的WebMvcConfig.java
文件中
package love.hibotella.common.config; /** * Jackson 配置 * 【特别注意】只能有1个WebMvcConfigurationSupport实例 * 所有这个新建的“Jackson配置文件”不能有 */ // @Configuration // public class JacksonConfig extends WebMvcConfigurationSupport { public class JacksonConfig { // @Bean // public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() { // MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter(); // jsonConverter.setObjectMapper(getObjectMapper()); // return jsonConverter; // } // // @Override // protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // converters.add(customJackson2HttpMessageConverter()); // super.addDefaultHttpMessageConverters(converters); // } // // public ObjectMapper getObjectMapper() { // return getMapper(); // } // // private ObjectMapper getMapper() { // return new ObjectMapper() // .registerModule(getModule()) // .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); // // .setSerializationInclusion(JsonInclude.Include.NON_NULL); // // .setSerializationInclusion(JsonInclude.Include.NON_EMPTY); // } // // private SimpleModule getModule() { // return new SimpleModule() // .addSerializer(Date.class, new DateSerializer(false, new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN))) // .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN))) // .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))) // .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN))) // ; // // } }
WebMvcConfig.java
package love.hibotella.common.config; import cn.hutool.core.date.DatePattern; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.DateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import love.hibotella.common.interceptor.Interceptor; import love.hibotella.common.interceptor.SecurityInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import javax.annotation.Resource; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 拦截器 配置 */ @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Resource private SecurityInterceptor securityInterceptor; @Resource private Interceptor interceptor; private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 拦截器,默认拦截所有路径,放行特定路径 List<String> excludePathList = new ArrayList<>(); // 放行 Spring Boot 自带的error excludePathList.add("/error"); // 放行 Swagger excludePathList.add("/doc.html"); excludePathList.add("/swagger-resources/**"); excludePathList.add("/webjars/**"); excludePathList.add("/v2/**"); excludePathList.add("/swagger-ui.html"); excludePathList.add("/favicon.ico"); registry.addInterceptor(securityInterceptor).addPathPatterns("/**").excludePathPatterns(excludePathList); registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(excludePathList); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!registry.hasMappingForPattern("/webjars/**")) { registry.addResourceHandler("/webjars/**").addResourceLocations( "classpath:/META-INF/resources/webjars/"); } if (!registry.hasMappingForPattern("/**")) { registry.addResourceHandler("/**").addResourceLocations( CLASSPATH_RESOURCE_LOCATIONS); } } /** * 解决跨域 */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOriginPatterns("*") // 是否允许证书(cookies) .allowCredentials(true) // 设置允许的方法 .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .maxAge(3600 * 24); } //**************************************************************** //【开始】Jackson 配置 //**************************************************************** @Bean public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter(); jsonConverter.setObjectMapper(getObjectMapper()); return jsonConverter; } @Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(customJackson2HttpMessageConverter()); super.addDefaultHttpMessageConverters(converters); } public ObjectMapper getObjectMapper() { return getMapper(); } private ObjectMapper getMapper() { return new ObjectMapper() .registerModule(getModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); // .setSerializationInclusion(JsonInclude.Include.NON_NULL); // .setSerializationInclusion(JsonInclude.Include.NON_EMPTY); } private SimpleModule getModule() { return new SimpleModule() .addSerializer(Date.class, new DateSerializer(false, new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN))) .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN))) .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))) .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN))) ; } //**************************************************************** //【结束】Jackson 配置 //**************************************************************** }
参考2:
package com.wanma.framework_web.config; import cn.hutool.core.date.DatePattern; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.DateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import com.wanma.framework_web.interceptor.SecurityInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.*; import javax.annotation.Resource; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 拦截器 配置 */ @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Resource private SecurityInterceptor securityInterceptor; private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // configurer.favorParameter(true) // .parameterName("mediaType") // .defaultContentType(MediaType.APPLICATION_JSON) // .mediaType("xml", MediaType.APPLICATION_XML) // .mediaType("html", MediaType.TEXT_HTML) // .mediaType("json", MediaType.APPLICATION_JSON); } /** * 注册自定义拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 拦截器,默认拦截所有路径,放行特定路径 List<String> excludePathList = new ArrayList<>(); // 放行 Spring Boot 自带的error excludePathList.add("/error"); // 放行 Swagger excludePathList.add("/doc.html"); excludePathList.add("/swagger-resources/**"); excludePathList.add("/webjars/**"); excludePathList.add("/v2/**"); excludePathList.add("/swagger-ui.html"); excludePathList.add("/favicon.ico"); registry.addInterceptor(this.securityInterceptor).addPathPatterns("/**").excludePathPatterns(excludePathList); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!registry.hasMappingForPattern("/webjars/**")) { registry.addResourceHandler("/webjars/**").addResourceLocations( "classpath:/META-INF/resources/webjars/"); } if (!registry.hasMappingForPattern("/**")) { registry.addResourceHandler("/**").addResourceLocations( CLASSPATH_RESOURCE_LOCATIONS); } } /** * 解决跨域 */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOriginPatterns("*") // 是否允许证书(cookies) .allowCredentials(true) // 设置允许的方法 .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .maxAge(3600 * 24); } //**************************************************************** //【开始】Jackson 配置 //**************************************************************** @Bean public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter(); jsonConverter.setObjectMapper(this.getObjectMapper()); return jsonConverter; } @Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(this.customJackson2HttpMessageConverter()); super.addDefaultHttpMessageConverters(converters); } public ObjectMapper getObjectMapper() { return this.getMapper(); } private ObjectMapper getMapper() { return new ObjectMapper() .registerModule(this.getModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); // .setSerializationInclusion(JsonInclude.Include.NON_NULL); // .setSerializationInclusion(JsonInclude.Include.NON_EMPTY); } private SimpleModule getModule() { return new SimpleModule() .addSerializer(Date.class, new DateSerializer(false, new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN))) .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN))) .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))) .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN))) ; } //**************************************************************** //【结束】Jackson 配置 //**************************************************************** }