时间格式解析出错问题解决

633 阅读5分钟

时间格式解析出错问题解决

背景介绍

    因为项目中需要对接第三方平台,则需要调用三方的接口,这样就需要对三方返回的数据进行解析处理,
尤其是时间类型的处理最为需要注意,因为你在不知道底层原理的情况下,程序常常动不动就报错给你看,
更可怕的是,三方的时间格式还有多种。

原理说明

    在SpringBoot中,我们在处理三方返回的数据类型为json数据类型,需要进行反序列化的操作,
 将json数据类型转化为对应的java类型,反之为序列化操作,这样就会需要有工具类来处理Json
 数据,我们熟知的解析Json数据的工具有FastJson,Jackson,Gson这几种,但在SpringBoot中,默认
 使用的是Jackson进行对数据处理,这样我们就需要知道Jackson中是如何处理时间格式的。

Jackson中源码跟踪处理(请你耐心跟我看一下)

根据入参与出参可知,找到了关键代码

image.png

image.png

image.png

这里开始判断解析格式处理了

image.png

image.png

了解到Jackson解析的时间格式必须为yyyy-MM-dd'T'HH:mm:ss.sssZ,最重要的是中间的'T'字符。

另外一种情况就是时间格式末尾不是Z字符而是带时区的时间

格式类似:2018-09-13T05:34:31.999+08002018-09-13T05:34:31.999+08:00
这些格式在原生的Jackson中是都不支持的,所以要利用其它工具进行解析比如,Hutool中的
Date date = DateUtil.parseUTC(String)来解析时间,里面封闭了多种的时间格式,这样
我们可以直接使用就不重复造轮子了。

image.png

image.png

回到主题,重写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;
    }
}
    完成以上配置,你就可以完美的处理StringDate时间格式啦!!!

再说一说,局部的处理方式

@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd'T'HH:mm:ss",timezone = "GMT+8")
使用@JsonFormat指定时间格式可以局部的处理Json中的String转Date的格式,虽然网上有很多的说法是此注解
是用来处理后端传给前端,也就是序列化过程的格式,可是我测试发现@JsonFormat同样处理了反序列化的操作,以下是源码的简单查看。
使用局部处理会存在的问题就是,一样返回的时间格式变了,比如有时间因为三方出问题了

image.png

image.png

image.png

最后发现最有效的方法是,直接替换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)