Web 时区在国际化的业务场景中,时区问题是常见的。关于时区的概念,地球被划分为24个时区,北京时间为东八区,而美国的太平洋时间为西八区,和我们差了16个小时。下面从一个案例说起,服务器和数据库部署在北京,而这时美国用户通过浏览器希望能查询北京时间下的“2020年7月1日8点-2020年7月1日18点”这10个小时的数据。
浏览器上选择时间区域查询数据为了模拟浏览器在太平洋时间,只需将系统时间设置为太平洋时间即可。而系统时间的改变会影响到JVM的默认时区,所以为了让服务器程序仍处于北京时间,需要通过代码指定时区,如下:
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
而数据库MySQL的时区也设置为北京时间,SQL如下:
set global time_zone = '+8:00';set time_zone = '+8:00';flush privileges;
下面,让点击查询,先看下发送的内容:
发送数据的格式可以看到开始时间和结束时间都比界面上显示的时间多了8小时。这是因为使用的ElementUI组件的日期时间选择器,其默认时区为0时区,所以会将选择的时间根据浏览器的时区(西八区)转换成0时区的时间。最后传输的内容为时间+时区的字符串表示。
时间-时区的字符串表示前端把数据成功发出来了,下面看下后端接收数据的情况。后端使用的是SpringBoot,Controller的代码如下。
@PostMapping("/time")public%20List<Data>%20test(@RequestBody%20TimeDto%20dto)%20{%20%20%20%20Date%20startTime%20=%20dto.getStartTime();%20%20%20%20Date%20endTime%20=%20dto.getEndTime();%20%20%20%20System.out.println(startTime);%20%20%20%20System.out.println(endTime);%20%20%20%20//%20格林时间(0)%20%20%20%20String%20format%20=%20"yyyy-MM-dd%20HH:mm:ss";%20%20%20%20SimpleDateFormat%20sdfGreen%20=%20new%20SimpleDateFormat(format);%20%20%20%20sdfGreen.setTimeZone(TimeZone.getTimeZone("GMT+0"));%20%20%20%20System.out.println("格林时间:"%20+%20sdfGreen.format(startTime)%20+%20"至"%20+%20sdfGreen.format(endTime));%20%20%20%20//%20北京时间(+8)%20%20%20%20SimpleDateFormat%20sdfBeijing%20=%20new%20SimpleDateFormat(format);%20%20%20%20sdfBeijing.setTimeZone(TimeZone.getTimeZone("GMT+8"));%20%20%20%20System.out.println("北京时间:"%20+%20sdfBeijing.format(startTime)%20+%20"至"%20+%20sdfBeijing.format(endTime));%20%20%20%20//%20太平洋时间(-8)%20%20%20%20SimpleDateFormat%20sdfPacific%20=%20new%20SimpleDateFormat(format);%20%20%20%20sdfPacific.setTimeZone(TimeZone.getTimeZone("GMT-8"));%20%20%20%20System.out.println("太平洋时间:"%20+%20sdfPacific.format(startTime)%20+%20"至"%20+%20sdfPacific.format(endTime));%20%20%20%20List<Data>%20dataList%20=%20queryDate(dto);%20%20%20%20return%20dataList;}/**Thu%20Jul%2002%2000:00:00%20GMT+08:00%202020Thu%20Jul%2002%2010:00:00%20GMT+08:00%202020格林时间:2020-07-01%2016:00:00至2020-07-02%2002:00:00北京时间:2020-07-02%2000:00:00至2020-07-02%2010:00:00太平洋时间:2020-07-01%2008:00:00至2020-07-01%2018:00:00**/
由于JVM时区为东八区,所以反序列化时得到的Date对象也是东八区的时间,即2号0点-2号10点。如果直接用startTime和endTime去查询,得到的将是北京时间2号0点到10点的数据,和预想的结果有差异。时区问题导致的查询时间范围错误那如何才能查询到北京时间1号8点-1号18点的数据呢。由于前端传输的太平洋时间在后台接收时发生时区转换,所以可以在前端直接传输需要查询的北京时间。也就是1号8点-1号18点。通过设置
el-date-picker的value-format属性,指定选择的时间格式“yyyy-MM-dd%20HH:mm:ss”,这样传输的时间字符串将不具有时区属性。
<el-date-picker%20%20v-model="dateTimeRange"%20%20type="datetimerange"%20%20range-separator="至"%20%20start-placeholder="开始日期"%20%20end-placeholder="结束日期"%20%20value-format="yyyy-MM-dd%20HH:mm:ss"%20%20></el-date-picker>
修正后的发送数据格式而后端如果不修改,将报出以下错误,无法将该格式的时间转换成Date对象。
JSON%20parse%20error:%20Cannot%20deserialize%20value%20of%20type%20`java.util.Date`%20from%20String%20"2020-07-01%2008:00:00":%20not%20a%20valid%20representation%20(error:%20Failed%20to%20parse%20Date%20value%20'2020-07-01%2008:00:00':%20Cannot%20parse%20date%20"2020-07-01%2008:00:00":%20while%20it%20seems%20to%20fit%20format%20'yyyy-MM-dd'T'HH:mm:ss.SSSZ',%20parsing%20fails%20(leniency?%20null));%20nested%20exception%20is%20com.fasterxml.jackson.databind.exc.InvalidFormatException:%20Cannot%20deserialize%20value%20of%20type%20`java.util.Date`%20from%20String%20"2020-07-01%2008:00:00":%20not%20a%20valid%20representation%20(error:%20Failed%20to%20parse%20Date%20value%20'2020-07-01%2008:00:00':%20Cannot%20parse%20date%20"2020-07-01%2008:00:00":%20while%20it%20seems%20to%20fit%20format%20'yyyy-MM-dd'T'HH:mm:ss.SSSZ',%20parsing%20fails%20(leniency?%20null))↵%20at%20[Source:%20(PushbackInputStream);%20line:%201,%20column:%2014]%20(through%20reference%20chain:%20com.chaycao.timezone.TimeDto["startTime"])
所以为能正确反序列化,需要为jackjson做反序列化提供额外的信息。加上@JsonFormat注解,指定时区和时间格式,便能达到期望的效果,得到的将是北京时间的1号8点和1号18点。所以,在前后端传输发生的时区问题,注意时间数据的序列化和反序列化方式就能解决。
public%20class%20TimeDto%20{%20%20%20%20@JsonFormat(timezone%20=%20"GMT+8",%20pattern%20=%20"yyyy-MM-dd%20HH:mm:ss")%20%20%20%20Date%20startTime;%20%20%20%20@JsonFormat(timezone%20=%20"GMT+8",%20pattern%20=%20"yyyy-MM-dd%20HH:mm:ss")%20%20%20%20Date%20endTime;%20%20%20%20//...}
下面再看下数据库中会发生的时区问题。将MySQL的时区改为太平洋时间。
set%20global%20time_zone%20=%20'-8:00';set%20time_zone%20=%20'-8:00';flush%20privileges;
看下查询的结果是否会发生变化,查询的程序如下:
private%20List<Data>%20queryDate(TimeDto%20dto)%20{%20%20%20%20DriverManagerDataSource%20dataSource%20=%20new%20DriverManagerDataSource();%20%20%20%20dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");%20%20%20%20dataSource.setUrl("jdbc:mysql://localhost/test?useSSL=false&useUnicode=true&characterEncoding=UTF8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai");%20%20%20%20dataSource.setUsername("root");%20%20%20%20dataSource.setPassword("caoniezi");%20%20%20%20Date%20startTime%20=%20dto.getStartTime();%20%20%20%20Date%20endTime%20=%20dto.getEndTime();%20%20%20%20JdbcTemplate%20jdbcTemplate%20=%20new%20JdbcTemplate(dataSource);%20%20%20%20String%20sql%20=%20"SELECT%20*%20FROM%20data%20WHERE%20create_time%20>=%20?%20and%20create_time%20<=%20?";%20%20%20%20List<Map<String,%20Object>>%20maps%20=%20jdbcTemplate.queryForList(%20%20%20%20%20%20%20%20sql,%20%20%20%20%20%20%20%20new%20Object[]{startTime,%20endTime});%20%20%20%20List<Data>%20dataList%20=%20new%20ArrayList<>();%20%20%20%20for%20(Map<String,%20Object>%20map%20:%20maps)%20{%20%20%20%20%20%20%20%20Data%20data%20=%20new%20Data();%20%20%20%20%20%20%20%20data.setId((Integer)%20map.get("id"));%20%20%20%20%20%20%20%20data.setContent((String)%20map.get("content"));%20%20%20%20%20%20%20%20data.setCreateTime((Date)%20map.get("create_time"));%20%20%20%20%20%20%20%20dataList.add(data);%20%20%20%20}%20%20%20%20return%20dataList;}
查询的结果仍然是“D,E,F”,看来数据库时区的改变对于本次查询未产生影响。修改MySQL时区后查询时间范围正确这是因为在
create_time字段的类型为datetime,而datetime是没有时区概念的,存储的是格式为YYYYMMDDHHMMSS(年月日时分秒)的整数,不会受到时区的影响。而如果先将时区改回东八区,将create_time的类型改为timestamp,再把时区改为西八区。查询的结果是“H,I,J”。
set%20global%20time_zone%20=%20'+8:00';set%20time_zone%20=%20'+8:00';flush%20privileges;%20ALTER%20TABLE%20`data`%20MODIFY%20COLUMN%20`create_time`%20TIMESTAMP%20DEFAULT%20NULL;set%20global%20time_zone%20=%20'-8:00';set%20time_zone%20=%20'-8:00';flush%20privileges;
修改create_time字段类型为timestamp这是因为timestamp是有时区概念,存入的是自时间纪元以来的秒数,将类型改为timestamp时,
create_time的值也会由东八区计算为0时区的时间秒数存储。当以西八区查询时,会减少16小时。
修改为timestamp后查询那如何才能在西八区的数据库中查出想要的数据。jdbc连接url中的serverTimezone参数,其作用是为驱动指定MySQL的时区,在之前的操作中,修改了MySQL的时区,而serverTimezone未修改,仍然是东八区。
jdbc:mysql://localhost/test?useSSL=false&useUnicode=true&characterEncoding=UTF8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
查询情况如下,MySQL驱动会根据指定的serverTimezone和JVM时区做转换,由于两者都是东八区,所以startTime和endTime的时间字符串不变,但是由于MySQL时区已变为西八区,查询结果就落到了H、I、J上。
serverTimezone为东八区的查询情况下面把serverTimezone去掉,在未指定serverTimezone的情况下,驱动会根据MySQL的时区作为serverTimezone,然后做转换,这样得到的结果就是想要的。
serverTimezone不指定的查询情况但是这样做有一个问题,就是在查询datetime类型的数据时,也会发生转换,查询的结果将是30号16点到1号2点的数据。那么如何才能保证datetime类型、timestamp类型的数据都正确。首先serverTimezone是需要指定Asia/Shanghai的,不然datetime的数据会发生转换。而由于serverTimezone和MySQL时区不一致,查询的timestampe数据存在时区问题,所以最后的办法就是修改MySQL时区为东八区。通过保证MySQL时区、serverTimezone和JVM时区三者一致,来保证时间数据读写的正确性。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论