Web開發中的時區問題

Web開發中的時區問題

在國際化的業務場景中,時區問題是常見的。本文將就Web開發中的時區問題進行探索。前端

關於時區的概念,想必你們都有些瞭解。咱們的地球被劃分爲24個時區,北京時間爲東八區,而美國的太平洋時間爲西八區,和咱們差了16個小時。java

下面咱們從一個案例提及,咱們的服務器和數據庫部署在北京,而這時美國用戶經過瀏覽器但願能查詢北京時間下的「2020年7月1日8點-2020年7月1日18點」這10個小時的數據。
Web開發中的時區問題
瀏覽器上選擇時間區域查詢數據mysql

爲了模擬瀏覽器在太平洋時間,只需將系統時間設置爲太平洋時間便可。而系統時間的改變會影響到JVM的默認時區,因此爲了讓服務器程序仍處於北京時間,須要經過代碼指定時區,以下:git

TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));

而數據庫MySQL的時區也設置爲北京時間,SQL以下:github

set global time_zone = '+8:00';
set time_zone = '+8:00';
flush privileges;

下面,讓咱們點擊查詢,先看下咱們發送的內容:
Web開發中的時區問題
發送數據的格式sql

能夠看到開始時間和結束時間都比界面上顯示的時間多了8小時。這是由於我使用的ElementUI組件的日期時間選擇器,其默認時區爲0時區,因此會將咱們選擇的時間根據瀏覽器的時區(西八區)轉換成0時區的時間。最後傳輸的內容爲時間+時區的字符串表示。
Web開發中的時區問題
時間-時區的字符串表示數據庫

前端把數據成功發出來了,下面咱們看下後端接收數據的狀況。後端我使用的是SpringBoot,Controller的代碼以下。json

@PostMapping("/time")
public List<Data> test(@RequestBody TimeDto dto) {
    Date startTime = dto.getStartTime();
    Date endTime = dto.getEndTime();
    System.out.println(startTime);
    System.out.println(endTime);

    // 格林時間(0)
    String format = "yyyy-MM-dd HH:mm:ss";
    SimpleDateFormat sdfGreen = new SimpleDateFormat(format);
    sdfGreen.setTimeZone(TimeZone.getTimeZone("GMT+0"));
    System.out.println("格林時間:" + sdfGreen.format(startTime) + "至" + sdfGreen.format(endTime));

    // 北京時間(+8)
    SimpleDateFormat sdfBeijing = new SimpleDateFormat(format);
    sdfBeijing.setTimeZone(TimeZone.getTimeZone("GMT+8"));
    System.out.println("北京時間:" + sdfBeijing.format(startTime) + "至" + sdfBeijing.format(endTime));

    // 太平洋時間(-8)
    SimpleDateFormat sdfPacific = new SimpleDateFormat(format);
    sdfPacific.setTimeZone(TimeZone.getTimeZone("GMT-8"));
    System.out.println("太平洋時間:" + sdfPacific.format(startTime) + "至" + sdfPacific.format(endTime));

    List<Data> dataList = queryDate(dto);

    return dataList;
}
/**
Thu Jul 02 00:00:00 GMT+08:00 2020
Thu Jul 02 10:00:00 GMT+08:00 2020
格林時間:2020-07-01 16:00:00至2020-07-02 02:00:00
北京時間:2020-07-02 00:00:00至2020-07-02 10:00:00
太平洋時間:2020-07-01 08:00:00至2020-07-01 18:00:00
**/

因爲JVM時區爲東八區,因此反序列化時獲得的Date對象也是東八區的時間,即2號0點-2號10點。若是咱們直接用startTime和endTime去查詢,獲得的將是北京時間2號0點到10點的數據,和預想的結果有差別。
Web開發中的時區問題
時區問題致使的查詢時間範圍錯誤後端

那如何才能查詢到北京時間1號8點-1號18點的數據呢。因爲咱們前端傳輸的太平洋時間在後臺接收時發生時區轉換,因此能夠在前端直接傳輸須要查詢的北京時間。也就是1號8點-1號18點。經過設置el-date-picker的value-format屬性,指定選擇的時間格式「yyyy-MM-dd HH:mm:ss」,這樣傳輸的時間字符串將不具備時區屬性。瀏覽器

<el-date-picker
  v-model="dateTimeRange"
  type="datetimerange"
  range-separator="至"
  start-placeholder="開始日期"
  end-placeholder="結束日期"
  value-format="yyyy-MM-dd HH:mm:ss"
  >
</el-date-picker>

Web開發中的時區問題
修正後的發送數據格式

然後端若是不修改,將報出如下錯誤,沒法將該格式的時間轉換成Date對象。

JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2020-07-01 08:00:00": not a valid representation (error: Failed to parse Date value '2020-07-01 08:00:00': Cannot parse date "2020-07-01 08:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2020-07-01 08:00:00": not a valid representation (error: Failed to parse Date value '2020-07-01 08:00:00': Cannot parse date "2020-07-01 08:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))↵ at [Source: (PushbackInputStream); line: 1, column: 14] (through reference chain: com.chaycao.timezone.TimeDto["startTime"])

因此爲能正確反序列化,須要爲jackjson作反序列化提供額外的信息。加上@JsonFormat註解,指定時區和時間格式,便能達到指望的效果,獲得的將是北京時間的1號8點和1號18點。因此,在先後端傳輸發生的時區問題,注意時間數據的序列化和反序列化方式就能解決。

public class TimeDto {
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    Date startTime;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    Date endTime;
    //...
}

下面咱們再看下數據庫中會發生的時區問題。

咱們將MySQL的時區改成太平洋時間。

set global time_zone = '-8:00';
set time_zone = '-8:00';
flush privileges;

看下查詢的結果是否會發生變化,查詢的程序以下:

private List<Data> queryDate(TimeDto dto) {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost/test?useSSL=false&useUnicode=true&characterEncoding=UTF8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai");
    dataSource.setUsername("root");
    dataSource.setPassword("caoniezi");

    Date startTime = dto.getStartTime();
    Date endTime = dto.getEndTime();

    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    String sql = "SELECT * FROM data WHERE create_time >= ? and create_time <= ?";
    List<Map<String, Object>> maps = jdbcTemplate.queryForList(
        sql,
        new Object[]{startTime, endTime});
    List<Data> dataList = new ArrayList<>();
    for (Map<String, Object> map : maps) {
        Data data = new Data();
        data.setId((Integer) map.get("id"));
        data.setContent((String) map.get("content"));
        data.setCreateTime((Date) map.get("create_time"));
        dataList.add(data);
    }
    return dataList;
}

查詢的結果仍然是「D,E,F」,看來數據庫時區的改變對於咱們本次查詢未產生影響。
Web開發中的時區問題
修改MySQL時區後查詢時間範圍正確

這是由於在create_time字段的類型爲datetime,而datetime是沒有時區概念的,存儲的是格式爲YYYYMMDDHHMMSS(年月日時分秒)的整數,不會受到時區的影響。
而若是咱們先將時區改回東八區,將create_time的類型改成timestamp,再把時區改成西八區。查詢的結果是「H,I,J」。

set global time_zone = '+8:00';
set time_zone = '+8:00';
flush privileges; 

ALTER TABLE `data` MODIFY COLUMN `create_time` TIMESTAMP DEFAULT NULL;

set global time_zone = '-8:00';
set time_zone = '-8:00';
flush privileges;

Web開發中的時區問題

修改create_time字段類型爲timestamp

這是由於timestamp是有時區概念,存入的是自時間紀元以來的秒數,在咱們將類型改成timestamp時,create_time的值也會由東八區計算爲0時區的時間秒數存儲。當咱們以西八區查詢時,會減小16小時。
Web開發中的時區問題
修改成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上。
Web開發中的時區問題
serverTimezone爲東八區的查詢狀況

下面咱們把serverTimezone去掉,在未指定serverTimezone的狀況下,驅動會根據MySQL的時區做爲serverTimezone,而後作轉換,這樣獲得的結果就是咱們想要的。
Web開發中的時區問題
serverTimezone不指定的查詢狀況
可是這樣作有一個問題,就是在查詢datetime類型的數據時,也會發生轉換,查詢的結果將是30號16點到1號2點的數據。那麼如何才能保證datetime類型、timestamp類型的數據都正確。首先serverTimezone是須要指定Asia/Shanghai的,否則datetime的數據會發生轉換。而因爲serverTimezone和MySQL時區不一致,查詢的timestampe數據存在時區問題,因此最後的辦法就是修改MySQL時區爲東八區。經過保證MySQL時區、serverTimezone和JVM時區三者一致,來保證時間數據讀寫的正確性。

文中的代碼已上傳至Github,感興趣的同窗能夠本身試下:
https://github.com/chaycao/Learn/tree/master/LearnTimeZone
Web開發中的時區問題

相關文章
相關標籤/搜索