记一次 Feign 对 LocalDateTime 序列化和反序列化 “缺陷”

记一次 Feign 对 LocalDateTime 序列化和反序列化 “缺陷”

场景描述

接口路径:”putLocalDateTime/{localDateTime}”
参数类型:java.time.LocalDateTime
请求方式:PUT
过程描述:前端请求 -> 微服务 A -> 微服务 B -> B 处理请求返回 -> A 收到返回结果继续返回 -> 前端收到结果
问题发生:在” 微服务 A -> 微服务 B ” 节点报错

请求跟踪

step1: 前端发出请求,A 使用自定义的 WebMvc 数据转换器;接收参数正常,转换结果正常

step2: 微服务之间通过 feign 交互数据;参数正常,A 发起请求

step3: B 收到 A 的请求,B 使用自定义的 WebMvc 数据转换器;参数异常,非期望的字符串

问题分析

所有的数据在传输的过程中是需要被序列化成字符串进而以二进制的形式进行传输的,所以根据以上现象可以分析得出:A 在发出请求的时候对数据序列化并没有按照期望的形式进行序列化,即转换成 “yyyy-MM-dd HH:mm:ss” 的时间格式;所以自定义的数据转换器无法解析导致报错;

源码跟踪

step1: feign 请求入口:feign.SynchronousMethodHandler#invoke

可以看到第一步 create() 方法已经将参数序列化了,这个序列化结果就是上面第三步的参数结果,继续跟踪……

step2: feign 序列化方法调用

看到当前 value 就是上一步的序列化结果,所以方法 expandElements() 就是详细的序列化过程,继续跟踪……

step3: 全局序列化的核心逻辑

上图方法就是全局数据转换的具体实现逻辑,序列化只是转换的一个步骤,反序列化也使用此逻辑;可以看到 feign 对 LocalDateTime 对象序列化时使用的是org.springframework.format.datetime.standard.TemporalAccessorPrinter#print 方法;为什么序列化的结果不是期望的呢,可以观察到这个序列化工具类的 formatter 参数并未被初始化成 “yyyy-MM-dd HH:mm:ss” 的格式,寻找当前工具类在何处被初始化,可以找到调用的方法是 org.springframework.format.datetime.standard.DateTimeFormatterRegistrar#registerFormatters,继续跟踪……

step4: feign 初始化时间类型的 formatter

这里我们可以发现不仅 feign 而是全局对时间类型的序列化设置都是固定的,继续查找当前方法的调用方,发现 feign 有对其进行调用,配置类地址:org.springframework.cloud.openfeign.FeignClientsConfiguration#feignConversionService,终于找到了问题的根源所在,继续跟踪……

step5: feign 的自动化配置

至此找到的问题的所在,feign 的自动化配置调用了一系列的方法导致 feign 针对 LocalDateTime 的序列化只能使用默认的格式(Localized(SHORT,SHORT)) 去序列化,所以只需要重写这些配置并改变其 formatter 即可

问题解决

step1: 重写 feign 的转换服务 bean,指定时间类型转换时的格式(DateTimeFormatter)

@Bean
public FormattingConversionService feignConversionService() {
    DateTimeFormatter df = DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN);
    DateTimeFormatter tf = DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN);
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
    FormattingConversionService conversionService = new DefaultFormattingConversionService();
    conversionService.addFormatterForFieldType(LocalDate.class, new TemporalAccessorPrinter(df), new TemporalAccessorParser(LocalDate.class, df));
    conversionService.addFormatterForFieldType(LocalTime.class, new TemporalAccessorPrinter(tf), new TemporalAccessorParser(LocalTime.class, tf));
    conversionService.addFormatterForFieldType(LocalDateTime.class, new TemporalAccessorPrinter(dtf), new TemporalAccessorParser(LocalDateTime.class, dtf));
    return conversionService;
}

step2: 重新注册至 feign 所使用的合约中以生效

@Bean
public Contract feignContract(ConversionService feignConversionService) {
    return new SpringMvcContract(CollUtil.newArrayList(), feignConversionService);
}

以上 debug 过程中会发现 feign 对参数的序列化是调用了 org.springframework.cloud.openfeign.support.SpringMvcContract.ConvertingExpander#expand 的方法去实现的,所以还需要将上一步骤的转换服务类主动注册至自定义的 SpringMvcContract 中,这样 feign 在初始化过程就会使用自定义的 SpringMvcContract 的合约器作为实例了(默认的 SpringMvcContract 存在 @ConditionalOnMissingBean 是可以覆盖的)
注:如果你的配置不生效,请留意是否是配置的前后顺序导致的,可以在你的配置类上加上 @AutoConfigureBefore(FeignClientsConfiguration.class) 保证你的 bean 优先 feign 的配置生效