时间格式解析出错问题解决
背景介绍
因为项目中需要对接第三方平台,则需要调用三方的接口,这样就需要对三方返回的数据进行解析处理,
尤其是时间类型的处理最为需要注意,因为你在不知道底层原理的情况下,程序常常动不动就报错给你看,
更可怕的是,三方的时间格式还有多种。
原理说明
在SpringBoot中,我们在处理三方返回的数据类型为json数据类型,需要进行反序列化的操作,
将json数据类型转化为对应的java类型,反之为序列化操作,这样就会需要有工具类来处理Json
数据,我们熟知的解析Json数据的工具有FastJson,Jackson,Gson这几种,但在SpringBoot中,默认
使用的是Jackson进行对数据处理,这样我们就需要知道Jackson中是如何处理时间格式的。
Jackson中源码跟踪处理(请你耐心跟我看一下)
根据入参与出参可知,找到了关键代码
这里开始判断解析格式处理了
了解到Jackson解析的时间格式必须为yyyy-MM-dd'T'HH:mm:ss.sssZ,最重要的是中间的'T'字符。
另外一种情况就是时间格式末尾不是Z字符而是带时区的时间
格式类似:2018-09-13T05:34:31.999+0800 或 2018-09-13T05:34:31.999+08:00
这些格式在原生的Jackson中是都不支持的,所以要利用其它工具进行解析比如,Hutool中的
Date date = DateUtil.parseUTC(String)来解析时间,里面封闭了多种的时间格式,这样
我们可以直接使用就不重复造轮子了。
回到主题,重写Jackson,以应对多种时间格式
先说全局处理的方式(因为最有效和方便,统一时间格式)
第一步,首先定义进行时间转换的方法
//这里有个坑,就是要实现import org.springframework.core.convert.converter.Converter;包下的接口
public class DateConverter implements Converter<String, Date> {
//因为三方的时间格式边Hutool中也没有,补充的
public final static String UTC_WITH_ZONE_MS_OFFSET_PATTERN ="yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSZ";
//三方时间格式有时区,所以使用Hutool中对应的解析方式
public final static FastDateFormat FAST_DATE_FORMAT_WOQU = FastDateFormat.getInstance(UTC_WITH_ZONE_MS_OFFSET_PATTERN, TimeZone.getTimeZone("UTC"));
@Override
public Date convert(String s) {
Date date;
//首先判断是否是三方的特别时间格式,条件为长度判断
if (s.length() == UTC_WITH_ZONE_MS_OFFSET_PATTERN.length() + 3 ) {
date = new DateTime(s, FAST_DATE_FORMAT_WOQU);
// 格式类似:2018-09-13T05:34:31+0800 或 2018-09-13T05:34:31+08:00
log.info("带毫秒时间格式处理:"+s);
return date;
}
//如果不是特殊格式,使用Hutool的方法进行解析
return DateUtil.parseUTC(s);
}
}
第二步,重写Jackson并配置上处理时间格式的转换器
/**
* @version 1.0
* * 日期转换配置
* * 解决@RequestAttribute、@RequestParam和@RequestBody三种类型的时间类型参数接收与转换问题
* @date 2021/3/22 17:02
*/
@Configuration
public class DateConfig {
/**
* 默认日期时间格式
*/
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* Date转换器,用于转换RequestParam和PathVariable参数
*/
@Bean
public Converter<String, Date> dateConverter() {
return new DateConverter();
}
/**
* Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
* 使用@RequestBody注解的对象中的Date类型将从这里被转换
*/
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
//反序列化的时候如果多了其他属性,不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//如果是空对象的时候,不抛出异常
// objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
//属性为null的转换
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
JavaTimeModule javaTimeModule = new JavaTimeModule();
//Date序列化和反序列化
javaTimeModule.addSerializer(Date.class, new JsonSerializer<Date>() {
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
String formattedDate = formatter.format(date);
jsonGenerator.writeString(formattedDate);
}
});
javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<Date>() {
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
//这里添加上自定义的时间转换器
return new DateConverter().convert(jsonParser.getText());
}
});
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
}
完成以上配置,你就可以完美的处理String转Date时间格式啦!!!
再说一说,局部的处理方式
@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd'T'HH:mm:ss",timezone = "GMT+8")
使用@JsonFormat指定时间格式可以局部的处理Json中的String转Date的格式,虽然网上有很多的说法是此注解
是用来处理后端传给前端,也就是序列化过程的格式,可是我测试发现@JsonFormat同样处理了反序列化的操作,以下是源码的简单查看。
使用局部处理会存在的问题就是,一样返回的时间格式变了,比如有时间因为三方出问题了
最后发现最有效的方法是,直接替换Feign中Jackson的处理方式
@Slf4j
public class WoquFeignJacksonCovert {
/**
* 默认日期时间格式
*/
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Bean
public Decoder woquFeignDecoder() {
HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(customObjectMapper());
ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter);
return new ResponseEntityDecoder(new SpringDecoder(objectFactory));
}
public ObjectMapper customObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
//反序列化的时候如果多了其他属性,不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//如果是空对象的时候,不抛出异常
// objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
//属性为null的转换
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
JavaTimeModule javaTimeModule = new JavaTimeModule();
//Date序列化和反序列化
javaTimeModule.addSerializer(Date.class, new JsonSerializer<Date>() {
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
String formattedDate = formatter.format(date);
jsonGenerator.writeString(formattedDate);
}
});
javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<Date>() {
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return new DateConverter().convert(jsonParser.getText());
}
});
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
}
@Slf4j
public class DateConverter implements Converter<String, Date> {
public final static String UTC_WITH_ZONE_MS_OFFSET_PATTERN_9 ="yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSZ";
public final static FastDateFormat FAST_DATE_FORMAT_WOQU_9 = FastDateFormat.getInstance(UTC_WITH_ZONE_MS_OFFSET_PATTERN_9, TimeZone.getTimeZone("UTC"));
public final static String UTC_WITH_ZONE_MS_OFFSET_PATTERN_8 ="yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSZ";
public final static FastDateFormat FAST_DATE_FORMAT_WOQU_8 = FastDateFormat.getInstance(UTC_WITH_ZONE_MS_OFFSET_PATTERN_8, TimeZone.getTimeZone("UTC"));
public final static String UTC_WITH_ZONE_MS_OFFSET_PATTERN_7 ="yyyy-MM-dd'T'HH:mm:ss.SSSSSSSZ";
public final static FastDateFormat FAST_DATE_FORMAT_WOQU_7 = FastDateFormat.getInstance(UTC_WITH_ZONE_MS_OFFSET_PATTERN_7, TimeZone.getTimeZone("UTC"));
@Override
public Date convert(String s) {
Date date;
if (s.length() == UTC_WITH_ZONE_MS_OFFSET_PATTERN_9.length() + 3 ) {
date = new DateTime(s, FAST_DATE_FORMAT_WOQU_9);
return date;
}
if (s.length() == UTC_WITH_ZONE_MS_OFFSET_PATTERN_8.length() + 3 ) {
date = new DateTime(s, FAST_DATE_FORMAT_WOQU_8);
return date;
}
if (s.length() == UTC_WITH_ZONE_MS_OFFSET_PATTERN_7.length() + 3 ) {
date = new DateTime(s, FAST_DATE_FORMAT_WOQU_7);
return date;
}
return DateUtil.parseUTC(s);
}
}
把自定义的处理配置加入到Feign配置中去即可
@FeignClient(name = "woqu-alert", url = "${woqu.url}", configuration = {WoquFeignConfiguration.class, WoquErrorDecoder.class, WoquFeignJacksonCovert.class}, primary = false)