问题
从mysql数据库查询出来的时间数据,返回给前端后,
如果采用yyyy-MM-dd HH:mm:ss的格式进行时间格式化,会相差8小时。
而如果采用yyyy-MM-dd的格式,会相差一天。
实体中的created_at字段:
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date createdAt;
mysql数据库时间:
返回前端时间:
问题:
希望的结构是后端返回给前端的创建时间应该和数据库里面数据保持一致,
但是实际上返回给前端创建时间相差8小时。
一、原因分析
所有返回的创建时间都相差8小时,很明显和createdAt字段的时区有关。
因为默认的时区UTC和北京时间(GMT+8)正好相差8小时。
但是这里我们在createdAt字段序列化的时候,已经通过@JsonFormat注解指定了序列化的格式为yyyy-MM-dd HH:mm:ss,时区为北京时间GMT+8。
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
继续通过debug断点分析,查看从数据库查询出的结果是什么?
发现从数据库中查询出来的结果,已经相差了8小时,排除JSON序列化的问题。
其中CST表示使用的是北京时区。
而存储在mysql数据库里面的时间字段,是没有时区概念的,时间字段的时区是由建立mysql的连接决定的。
spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC&zeroDateTimeBehavior=convertToNull
其中serverTimezone=UTC指定时区。
问题产生原因:
由于在建立mysql连接时,通过serverTimezone=UTC指定了时区为UTC,所以认为mysql数据库里面的时间字段的对应时区都是UTC,而通过mybatis获取时间字段数据和实体绑定时,由于时区会被转为北京时区CST,所以时间数值相差8小时。
二、问题解决
为了保证获取的时间字段的数据值保持一致,一定要保证mysql数据库连接中指定的时区serverTimezone和时间字段JSON格式化时指定的时区保持一致。
所以,有2种修复方案:
方案一:由于mysql连接中使用的时区为UTC,在JSON格式化时,也采用UTC时区
(采用这种方案的原因主要是由于当前项目中不方便修改mysql连接配置信息,所以只能通过JSON格式化指定时区对时间数据的时差进行修正。)
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "UTC")
private Date createdAt;
结果:
方案二:修改mysql连接中serverTimezone=CST(北京时间),JSON格式化中也使用CST时区
这也是更推荐采用的方案。能保证程序中获取的时间都是正确的北京时间,避免出现时差问题。
关于北京时区的表示方式,CST,GMT+8,Asia/Shanghai都表示北京时区。
mysql连接:
spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=CST&zeroDateTimeBehavior=convertToNull
java实体:
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "CST")
private Date createdAt;
三、时区说明
常见的时区有UTC、GMT、CST、GMT+8、Asia/Shanghai
具体说明:
UTC,世界标准时间 (UTC, Coordinated Universal Time)
是由国际无线电咨询委员会规定和推荐,并由国际时间局(BIH)负责保持的以秒为基础的时间标度,当今民用时间的基础。它使用一天 24 小时时间制,并结合了地球的自转时间与原子钟的高精度度量。UTC相当于本初子午线(即经度0度)上的平均太阳时,过去曾用格林威治平均时(GMT)来表示,北京时间比UTC时间早8小时。
UTC是一个标准,而不是一个时区。UTC 是一个全球通用的时间标准。全球各地都同意将各自的时间进行同步协调 (coordinated),这也是 UTC 名字的来源:Universal Coordinated Time。
GMT(Greenwich Mean Time) 格林尼治平时
由于地球轨道并非圆形,其运行速度又随着地球与太阳的距离改变而出现变化,因此视太阳时欠缺均匀性。视太阳日的长度同时亦受到地球自转轴相对轨道面的倾斜度所影响。为着要纠正上述的不均匀性,天文学家计算地球非圆形轨迹与极轴倾斜对视太阳时的效应。平太阳时就是指经修订后的视太阳时。在格林尼治子午线上的平太阳时称为世界时(UT0),又叫格林尼治平时(GMT)。 为了确保协调世界时与世界时(UT1)相差不会超过0.9秒,有需要时便会在协调世界时内加上正或负闰秒。因此协调世界时与国际原子时(TAI)之间会出现若干整数秒的差别。位于巴黎的国际地球自转事务中央局(IERS)负责决定何时加入闰秒。
我们可以认为格林威治时间就是世界协调时间(GMT=UTC),格林威治时间和UTC时间均用秒数来计算的。
CST:北京时间(中国标准时间),北京处于东八区,所以也可以表示成GMT+8。
Asia/Shanghai:上海时区
原因是1949年以前,中国一共分了5个时区,以哈尔滨 ( Asia/Harbin)、上海(Asia/Shanghai)、重庆(Asia/Chongqing)、乌鲁木齐(Asia/Urumqi)、喀什(Asia/Kashgar)为代表——分别是:长白时区GMT+8:30、中原标准时区 GMT+8、陇蜀时区GMT+7、新藏时区GMT+6和昆仑时区GMT+5:30。它是1912年北京观象台制订,后由内政部批准过。而且从国际标准本身的角度来看,北京和上海处于同一时区,只能保留一个,而作为时区代表上海已经足够具有代表性。所以目前还没有Asia/beijing。
CST、GMT+8、Asia/Shanghai三个时区等同,都表示东八区北京时间。
三、为什么@JsonFormat指定了时区还是相差8小时
由于mysql连接属性中通过serverTimezone=UTC指定了服务器的时区是UTC,所以认为数据库存储的时间字段的值对应的时区都是UTC时区。
而java程序中的Date字段默认对应的是CST北京时区。
这样就导致了java程序中获取的日期字段的值与数据库中的日期数据相差8小时。
而我们通常的json格式化都会将时区指定为东八区北京时间,即
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "CST")
或者
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
所以最终导致JSON格式化后返回给前端的时间相差了8小时。
四、为什么采用@JSONField进行时间格式化没有起作用
@JSONField(format = "yyyy-MM-dd")
private Date createdAt;
原因:
@JSONField注解是fastjson框架下的注解
@JsonFormat是jackson框架下的注解
而spring boot默认使用的是jackson序列化框架。
五、使用@JSONField注解如何指定时区
@JsonFormat注解可以通过timezone属性指定时区,但是fastjson中的时间格式注解@JSONField并没有发现对应的时区属性字段,那么使用@JSONField注解时,需要怎么指定时区呢?
解决:
通过JSON.defaultTimeZone指定JSON对应的默认时区。
JSON.defaultTimeZone=TimeZone.getTimeZone("UTC");
log.info(JSON.toJSONString(followInfoDto));
实体字段上添加@JSONField注解指定时间格式
/**
* 创建时间
*/
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createdAt;
输出结果:
六、@JsonFormat、@JSONField、@DateTimeFormat注解的区别
@DatetimeFormat是Spring框架下的注解,将String转换成Date,主要用于前台给后台传参时用Date类型参数绑定使用。
@JsonFormat是jackson框架下的注解,jackson框架是Spring Boot项目下默认的json序列化框架。主要用于指定json序列化格式, 一般用于后台返回数据给前台时,指定实体的序列化格式。
@JSONField是fastjson框架下的注解,主要用于指定实体序列化后面的字段名称和序列化格式。
查看源码发现,@JsonFormat和@JSONField默认时区都是是DEFAULT_TIMEZONE = “##default”,对应的默认时区是UTC,而不是系统默认时区。
JsonFormat.DEFAULT_TIMEZONE is NOT the system default, as the documentation and SO answer suggest, but actually defaults to UTC.
七、spring boot替换序列化框架
采用fastjson替换spring boot默认的json序列化框架jackson。
一般情况下,不推荐替换成fastjson框架。这是由于fastjson虽然在序列化速度上有一定的优势,但是一直以来都频繁爆出安全漏洞,相比之下默认的序列化框架jackson在速度和安全性方面都更有保障。
1、引入fastjson依赖库:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
2、配置fastjson序列化:
spring boot支持两种配置方法:
第一种方法:
(1)启动类继承extends WebMvcConfigurerAdapter
(2)覆盖方法configureMessageConverters或extendMessageConverters
具体代码如下:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 网上这种单独清除默认的Jackson2转换器的方法,还是会出问题
//converters.removeIf(httpMessageConverter -> httpMessageConverter instanceof MappingJackson2HttpMessageConverter);
//清空所有默认的转换器,不然容易受StringHttpMessageConverter的影响
converters.clear();
//指定时区
JSON.defaultTimeZone= TimeZone.getTimeZone("GMT+08");
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON);
fastMediaTypes.add(MediaType.TEXT_PLAIN);
fastMediaTypes.add(MediaType.ALL);
fastConverter.setSupportedMediaTypes(fastMediaTypes);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(fastConverter);
}
}
说明:
消息转换器converters的列表执行有先后次序,先匹配先处理,后面的转化器就不会起作用了。
如果不清空默认的转化器,直接添加的fastConverter会一直不起作用。
这里重写configureMessageConverters方法和extendMessageConverters方法效果是一样的。
常见异常:
Response body Unrecognized response type; displaying content as text.
由于没有清空默认的转换器,走了其他的转换器,导致的。
注意⚠️:
网上教程大多都是通过WebMvcConfigurerAdapter对象来配置,但是WebMvcConfigurerAdapter已经过期,尽量不要使用。
第二种方法:
在配置类中通过@Bean注解声明FastJsonHttpMessageConverter。
优点配置简单,不会出现中文乱码问题。
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
//指定时区
JSON.defaultTimeZone= TimeZone.getTimeZone("GMT+08");
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return new HttpMessageConverters(converter);
}
源码分析:
消息转换自动化配置类 HttpMessageConvertersAutoConfiguration
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({HttpMessageConverter.class})
@Conditional({HttpMessageConvertersAutoConfiguration.NotReactiveWebApplicationCondition.class})
@AutoConfigureAfter({GsonAutoConfiguration.class, JacksonAutoConfiguration.class, JsonbAutoConfiguration.class})
@Import({JacksonHttpMessageConvertersConfiguration.class, GsonHttpMessageConvertersConfiguration.class, JsonbHttpMessageConvertersConfiguration.class})
public class HttpMessageConvertersAutoConfiguration {
static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper";
public HttpMessageConvertersAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters((Collection)converters.orderedStream().collect(Collectors.toList()));
}
在声明HttpMessageConverters的方法上发现@ConditionalOnMissingBean注解,也就是当程序中没有声明HttpMessageConverters的bean时,才会加载默认的这些converters。
如果程序中声明了自己的HttpMessageConverters,那么就不会加载默认的这些converters。
总结
本文主要通过json格式化出现时差问题,扩展介绍了json时间格式化的相关注意事项。
1、mysql中日期字段数据的时区由连接属性中的serverTimezone属性决定。
2、介绍了常见的时区UTC、GMT、CST、GMT+8、Asia/Shanghai
3、@JsonFormat、@JSONField、@DateTimeFormat注解的区别
4、@JSONField可以通过JSON.defaultTimeZone指定时区
5、介绍如何使用FastJson框架替换spring boot默认的JSON序列号框架jackson。