前言
最近公司的项目需要从单体架构变为微服务架构,于是乎对项目的代码进行了重构,在处理服务之间的调用问题时,我们使用了openFeign组件,开发的时候都很顺利,但是当传递的参数使用的是Date类型的时候,却出现了意想不到的问题。
问题重现
版本说明:
spring-boot:2.1.10.RELEASE
spring-cloud:Greenwich.SR5
spring-cloud-alibaba:2.1.2.RELEASE
Feign服务端
application.yml
server:
port: 3001
spring:
application:
name: feign-service
# nacos配置
cloud:
nacos:
discovery:
server-addr: localhost:8800
启动类
@SpringBootApplication
@EnableDiscoveryClient // 注册服务到nacos
@Slf4j
public class FeignServiceApplication {
public static void main(String[] args) {
SpringApplication.run(FeignServiceApplication.class, args);
log.info("feign服务已启动...");
}
}
controller
@RestController
@RequestMapping("/api")
@Slf4j
public class FeignServiceController {
@RequestMapping(value = "service/test", method = RequestMethod.GET)
public void feignDateTest(@RequestParam("date") Date date) {
log.info("传递过来的时间参数:{}", date);
}
}
Feign客户端
application.yml
server:
port: 3002
spring:
application:
name: client-service
# nacos配置
cloud:
nacos:
discovery:
server-addr: localhost:8800
启动类
@SpringBootApplication
@EnableFeignClients // 扫描所有使用注解@FeignClient定义的feign客户端
@Slf4j
public class FeignClientApplication {
public static void main(String[] args) {
SpringApplication.run(FeignClientApplication.class, args);
}
}
controller
@RestController
@RequestMapping("/api")
@Slf4j
public class FeignClientController {
@Autowired
private IFeignClient iFeignClient;
@RequestMapping(value = "test", method = RequestMethod.GET)
public void feignDateTest() {
Date date = new Date();
log.info("date参数:{}", date);
// 调用接口
iFeignClient.feignDateTest(date);
}
}
启动两个服务,浏览器进行调用:http://localhost:3002/api/test
c.test.controller.FeignClientController : date参数:Tue Apr 09 11:08:20 CST 2024
c.t.controller.FeignServiceController : 传递过来的时间参数:Wed Apr 10 01:08:20 CST 2024
发现这两个时间居然不一样?openfeign,我的时间呢? 而且我发现这两个时间相差了14个小时
原因分析
原来,在进行网络通信时,openfeign客户端会将Date类型对象转换为String类型,会将Date类型转为String类型,比如上面的date参数,又因为中国的时区是CTS,所以转化为’Tue Apr 09 11:08:20 CST 2024’。
openfeign客户端会将Date类型对象转换为String类型,这是因为在网络传输中,只能传递文本数据而无法直接传递对象。
Date类型是Java中表示日期和时间的对象,它包含了年、月、日、时、分、秒等信息。然而,在进行网络通信时,常用的协议(如HTTP)只支持传输字符串类型的数据。
因此,为了在网络传输中传递日期和时间信息,需要将Date类型对象转换为String类型。这一过程称为日期格式化。通过使用特定的日期格式(例如ISO 8601标准格式),将Date对象转换为字符串,可以确保在不同系统、不同编程语言之间能够正确地传递和解析日期。
当服务端接收的参数有Date类型时,会调用Date的构造函数Date(String s)
,再调用parse(String s)
方法,parse(String s)
方法会根据时区的偏移量进行时间的解析,若不指定时区,默认使用本地时区。
@Deprecated
public Date(String s) {
this(parse(s));
}
@Deprecated
public static long parse(String s) {
}
CTS代表的时区有四个
Central Standard Time (USA) UT-6:00 美国
Central Standard Time (Australia) UT+9:30 澳大利亚
China Standard Time UT+8:00 中国
Cuba Standard Time UT-4:00)古巴
然而,在解析时间时,JVM看到CTS则认为是美国中部的时间(UT-6:00),而系统会检测到系统的时区是中国,就会自动加14个小时(东八区与西六区的时差)。
终于真相大白,原来出问题就在这个parse(String s)
方法上,但是改方法已经有了替代方法DateFormat.parse(String s)
.
解决方案
方案一:
直接使用long型的时间戳来传递时间,或者直接用String传递,到服务端去转换
方案二:
使用@DateTimeFormat
格式化参数
// 客户端
@Component
@FeignClient(name = "feign-service")
public interface IFeignClient {
@RequestMapping(value = "/api/service/test", method = RequestMethod.GET)
void feignDateTest(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @RequestParam("date") Date date);
}
// 服务端
@RestController
@RequestMapping("/api")
@Slf4j
public class FeignServiceController {
@RequestMapping(value = "service/test", method = RequestMethod.GET)
public void feignDateTest(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @RequestParam("date") Date date) {
log.info("传递过来的时间参数:{}", date);
}
}
方案三:
自定义Feign客户端中的日期格式化,规定好将Date参数类型转化为String的格式
客户端:
import org.springframework.cloud.openfeign.FeignFormatterRegistrar;
import org.springframework.format.FormatterRegistry;
import org.springframework.stereotype.Component;
import org.springframework.core.convert.converter.Converter;
import java.util.Date;
import java.text.SimpleDateFormat;
/**
* 自定义的日期格式化注册器,用于将Date类型转换为String类型,并指定特定的日期格式。
*/
@Component
public class DateFormatRegister implements FeignFormatterRegistrar {
public DateFormatRegister() {
}
/**
* 实现FeignFormatterRegistrar接口中的registerFormatters方法,用于注册自定义的日期转换器。
*
* @param registry FormatterRegistry对象,用于注册自定义的转换器。
*/
@Override
public void registerFormatters(FormatterRegistry registry) {
// 将Date类型转换为String类型的转换器注册到FormatterRegistry中
registry.addConverter(Date.class, String.class, new Date2StringConverter());
}
/**
* 内部类,实现Converter接口,用于将Date类型转换为指定格式的String类型。
*/
private class Date2StringConverter implements Converter<Date, String> {
/**
* 实现Converter接口中的convert方法,用于将Date对象转换为指定格式的字符串。
*
* @param source 要转换的Date对象。
* @return 转换后的String对象,表示指定格式的日期字符串。
*/
@Override
public String convert(Date source) {
// 创建SimpleDateFormat对象,指定日期格式为"yyyy-MM-dd HH:mm:ss"
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 使用SimpleDateFormat对象格式化Date对象,得到指定格式的日期字符串
return sdf.format(source);
}
}
}
客户端定义好了,服务端呢?也需要与之对应的解析转化器
服务端:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import javax.annotation.PostConstruct;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 用于配置字符串到日期的转换功能的配置类。
*/
@Configuration
public class String2DateConfig {
// 注入RequestMappingHandlerAdapter,用于获取WebBindingInitializer
@Autowired
private RequestMappingHandlerAdapter handlerAdapter;
/**
* 在初始化阶段增加字符串转换为日期的功能。
*/
@PostConstruct
public void initEditableValidation() {
// 获取WebBindingInitializer
ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) handlerAdapter.getWebBindingInitializer();
// 检查是否设置了ConversionService
if (initializer.getConversionService() != null) {
// 获取ConversionService
GenericConversionService genericConversionService = (GenericConversionService) initializer.getConversionService();
// 添加String到Date的转换器
genericConversionService.addConverter(String.class, Date.class, new String2DateConverter());
}
}
/**
* 内部类,实现Converter接口,用于将String类型转换为Date类型。
*/
private class String2DateConverter implements Converter<String, Date> {
/**
* 将String类型转换为Date类型。
*
* @param source 要转换的字符串。
* @return 转换后的Date对象,表示转换后的日期。
*/
@Override
public Date convert(String source) {
// 创建SimpleDateFormat对象,指定日期格式为"yyyy-MM-dd HH:mm:ss"
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
// 解析字符串为Date对象
return simpleDateFormat.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
}
再次调用,可以看出时间就一样了
至此,时间终于找回来了!
原文链接: openFeign,我的时间呢?