前言

 本篇文章对国际化场景中,接口请求接收和返回体中包含的时间字符串值如何根据时区自动转换进行了总结,至于系统如何设置和获取Locale和TimeZone对象,计划另起一篇文章介绍.文中有所欠缺和错误的地方,欢迎各位留言补充指正.

背景

 当用户所在时区和服务器所在时区不一致时,会产生时区相关问题,如时间显示错误、程序取得的时间和数据库存储的时间不一致、定时任务的触发没有跟随用户当前的时区等等问题.

目标

  1. 允许跨时区用户访问系统时,根据用户所在时区对时间进行转换,用户根据时间进行筛选等操作也适配用户对应的时区

  2. 统一前后端交互时间格式(yyyy-MM-dd HH:mm:ss)

  3. 摆脱操作系统时区对应用产生的影响,以不变的时区接收可变的时区,统一处理成应用服务指定的时区时间

支持场景

  1. GET请求及POST表单请求(RequestParam和PathVariable参数)中日期字符串在转为Date、LocalDateTime类型时可以自动转为应用服务当前时区的时间

  2. POST-application/json请求(RequestBody参数)在使用javabean作为入参时,javabean对象中的Date、LocalDateTime类型可以根据请求头中的时区字段自动转为应用服务当前时区的时间

  3. 接口返回对象时,对象中的Date、LocalDateTime类型的日期值,可以根据请求头中的时区字段,自动转为该时区的时间

  4. 对于特殊日期格式,可通过注解方式在javabean中单独配置

解决方案

日期时区传递模型图

 以跨境下单场景为例,不同时区下,客户端、服务端、数据库时间都是不同的:

日期转换时序图

 以用户在东10区,服务器在东8区为例,服务器比用户所在时区慢2个小时
在这里插入图片描述

数据库、应用层统一时区

  1. 数据库、应用层统一时区为东8区(GMT+8),可以避免数据库、应用层面之间的再做一次日期转换处理

  2. 将日期转换在代码层面解决,避免对服务器操作系统本身的时区、环境配置产生依赖,以免增加系统国际化部署时的难度

数据库设置时区
  1. 方式一: 修改配置文件永久生效

 windows系统中配置文件为my.ini。linux系统中配置文件为/etc/my.cnf

 在[mysqld]的下面添加或者修改如下内容:

default-time_zone = '+8:00'

 修改完配置文件后需要重启mysql服务器,

 linux系统中服务器重启命令如下:

systemctl restart mysqld.service
  1. 方式二: 通过mysql命令行模式下动态修改,重启失效
    这种修改只在当前的mysql启动状态生效,如果mysql重启,则恢复到my.ini的设置状态
set global time_zone = '+8:00';
FLUSH PRIVILEGES;
应用层设置时区
  1. 在启动类中设置默认时区
@SpringBootApplication
public class TestApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
    
    @PostConstruct
    void started() {
        //时区设置:默认中国上海
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
    }
}
  1. 日志输出指定时区

 以log4j为例,修改Log4j的配置文件:

<Configuration status="info" monitorInterval="10">
   <Properties>
       <Property name="TIME_ZONE">GMT+8</Property>
       <Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss}{${TIME_ZONE}}</Property>

统一日期格式传递规则

  1. 请求头/url携带timeZone字段标识用户所在时区

  2. 时间使用字符串传递,格式统一为yyyy-MM-dd HH:mm:ss,示例:

2022-08-08 18:00:00

后台日期格式处理

 实现效果: 实现请求入参和返回体中的日期字符串和javabean中的日期类型,可以根据时区自动互相转换
 以下的转换器入参没有HttpServletRequest对象,无法从入参直接获取到请求头,但是可以通过RequestContextHolder对象拿到本次请求的Request对象获取请求头:

ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
servletRequestAttributes.getRequest();
全局日期格式处理
  1. GET请求及POST表单请求(RequestParam和PathVariable参数): 自定义spring mvc的参数解析器,配置Converter<String, T>转换器实现参数转换
/**
 * <p>
 *  支持场景:
 *      GET请求及POST表单请求(RequestParam和PathVariable参数)中日期字符串在转为Date、LocalDateTime类型时可以自动转为应用服务当前时区的时间
 * </p>
 */
@Configuration
public class DateConverterConfig {
		@Bean
		public Converter<String, LocalDateTime> localDateTimeConverter() {
		    return new Converter<String, LocalDateTime>() {
		        @Override
		        public LocalDateTime convert(String source) {
		            try {
		                // TODO String类型的日期字符串转为LocalDateTime类型,并加上时区处理
		                return parse(source);
		            } catch (ParseException e) {
		                log.error(e.getMessage(),e);
		            }
		        }
		    };
		}
}
  1. POST-application/json请求(RequestBody参数) 接管Jackson的JSON的序列化和反序列化方式实现日期在转化过程中加入时区的处理,以LocalDateTime为例:
/**
 * <p>
 *  支持场景:
 *   <br/> 1. POST-application/json请求(RequestBody参数)在使用javabean作为入参时,javabean对象中的Date、LocalDateTime类型可以根据请求头中的时区字段自动转为应用服务当前时区的时间
 *   <br/> 2. 接口返回对象时,对象中的Date、LocalDateTime类型的日期值,可以根据请求头中的时区字段,自动转为该时区的时间 * </p>
 * </p>
 */
@JsonComponent
public class DateJsonSerializerConfig {

		/**
		 * 反序列化,其它时区的时间转为本地时间
		 */
		public static class LocalDateTimeJsonDeserializer extends JsonDeserializer<LocalDateTime> {
		    @Override
		    public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
		        // TODO 根据时区字段将日期转为本地时区时间
		        1. String类型日期转为LocalDateTime类型
		        if(请求头中含有timeZone‘时区’字段){
		            2. LocalDateTime日期时间转为应用服务时区时间
		        }
		        return localDateTime;
		    }
		}
		
		/**
		 * 序列化,本地时间转为其它时区的时间
		 */
		public static class LocalDateTimeJsonSerializer extends JsonSerializer<LocalDateTime> {
		    @Override
		    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
		        // TODO 本地时间转对应时区的时间
		        if(请求头中含有timeZone‘时区’字段){
		            1. LocalDateTime日期时间转为timeZone时区时间
		            2. localDateTime类型转为String类型
		            return;
		        }
		        1. localDateTime类型直接转为String类型
		    }
		}
}
局部日期格式处理

 如果全局日期的格式不满足实际需求,可采用在javabean实体类上加注解的方式修改,单独标注该实体类属性即可

// JsonFormat注解:将date转json
@JsonFormat(pattern = "yyyy-MM-dd", timezone="GMT+8")
private Date createTime;
特殊日期格式处理

 对于以非日期类型接受或返回时间的方式,此时全局配置的转换器无法根据类型自动转换日期,都需要自行处理.

 以String接受请求body数据为例,需要将String转为jsonObject对象后,使用正则匹配每一个字段值,当字段是日期,且格式为yyyy-MM-dd HH:mm:ss 时,进行时区转换处理并替换原有的日期值,返回数据的处理同理.

其它时区设置

  1. 数据库日期存储推荐使用DATETIME,而不是TIMESTAMP字段类型

    1. 性能不如 DATETIMEDATETIME 不存在时区转化问题。
    2. 性能抖动:海量并发时,存在性能抖动问题。
  2. 为了优化TIMESTAMP的使用,强烈建议使用显式的时区,而不是默认的操作系统时区。比如在配置文件中显示地设置时区,而不要使用系统时区:

set global time_zone='+8:00';

参考文档

时区问题总结(后续补充)

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐