在JAVA項(xiàng)目中遇到了一個(gè) Bug 因時(shí)區(qū)產(chǎn)生。現(xiàn)記錄解決過程。
1、時(shí)區(qū)
全球分為24個(gè)時(shí)區(qū),相鄰時(shí)區(qū)時(shí)間相差1個(gè)小時(shí)。
2、JAVA 獲取系統(tǒng)當(dāng)前時(shí)間方法
Date date = new Date();
System.currentTimeMillis();
3、結(jié)合時(shí)區(qū)與 Java 獲取系統(tǒng)時(shí)間
比如北京處于東八時(shí)區(qū),東京處于東九時(shí)區(qū),北京時(shí)間比東京時(shí)間晚1個(gè)小時(shí),而英國倫敦時(shí)間比北京晚7個(gè)小時(shí)(英國采用夏令時(shí)時(shí),8月英國處于夏令時(shí))。比如此刻北京時(shí)間是2017年8月24日11:17:10,則東京時(shí)間是2017年8月24日12:17:10,倫敦時(shí)間是2017年8月24日4:17:10。
既然Date里存放的是當(dāng)前時(shí)刻距1970年1月1日0點(diǎn)時(shí)刻的毫秒數(shù),如果此刻在倫敦、北京、東京有三個(gè)程序員同時(shí)執(zhí)行如下語句:
Date date = new Date();
System.currentTimeMillis();
那這三個(gè)date對象里存的毫秒數(shù)是相同的嗎?還是北京的比東京的小3600000(北京時(shí)間比東京時(shí)間晚1小時(shí),1小時(shí)為3600秒即3600000毫秒)?答案是,這3個(gè)Date里的毫秒數(shù)是完全一樣的。確切的說,Date對象里存的是自格林威治時(shí)間( GMT)1970年1月1日0點(diǎn)至Date對象所表示時(shí)刻所經(jīng)過的毫秒數(shù)。所以,如果某一時(shí)刻遍布于世界各地的程序員同時(shí)執(zhí)行new Date語句,這些Date對象所存的毫秒數(shù)是完全一樣的。也就是說,Date里存放的毫秒數(shù)是與時(shí)區(qū)無關(guān)的。
System.out.println(date);
那么北京的程序員將會打印出2017年8月24日11:17:10,而東京的程序員會打印出2017年8月24日12:17:10,倫敦的程序員會打印出2017年8月24日4:17:10。既然Date對象只存了一個(gè)毫秒數(shù),為什么這3個(gè)毫秒數(shù)完全相同的Date對象,可以打印出不同的時(shí)間呢?這是因?yàn)镾ysytem.out.println函數(shù)在打印時(shí)間時(shí),會取操作系統(tǒng)當(dāng)前所設(shè)置的時(shí)區(qū),然后根據(jù)這個(gè)時(shí)區(qū)將同毫秒數(shù)解釋成該時(shí)區(qū)的時(shí)間。當(dāng)然我們也可以手動設(shè)置時(shí)區(qū),以將同一個(gè)Date對象按不同的時(shí)區(qū)輸出。可以做如下實(shí)驗(yàn)驗(yàn)證:
Date date = new Date(1503544630000L);? // 對應(yīng)的北京時(shí)間是2017-08-24 11:17:10
SimpleDateFormat bjSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");? ? // 北京
bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));? // 設(shè)置北京時(shí)區(qū)
SimpleDateFormat tokyoSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");? // 東京
tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));? // 設(shè)置東京時(shí)區(qū)
SimpleDateFormat londonSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 倫敦
londonSdf.setTimeZone(TimeZone.getTimeZone("Europe/London"));? // 設(shè)置倫敦時(shí)區(qū)
System.out.println("毫秒數(shù):" + date.getTime() + ", 北京時(shí)間:" + bjSdf.format(date));
System.out.println("毫秒數(shù):" + date.getTime() + ", 東京時(shí)間:" + tokyoSdf.format(date));
輸出為:
毫秒數(shù):1503544630000, 北京時(shí)間:2017-08-24 11:17:10
毫秒數(shù):1503544630000, 東京時(shí)間:2017-08-24 12:17:10
毫秒數(shù):1503544630000, 倫敦時(shí)間:2017-08-24 04:17:10
可以看出,同一個(gè)Date對象,按不同的時(shí)區(qū)來格式化,將得到不同時(shí)區(qū)的時(shí)間。由此可見,Date對象里保存的毫秒數(shù)和具體輸出的時(shí)間(即年月日時(shí)分秒)是模型和視圖的關(guān)系,而時(shí)區(qū)(即Timezone)則決定了將同一個(gè)模型展示成什么樣的視圖。
4.從字符串中讀取時(shí)間
有時(shí)我們會遇到從一個(gè)字符串中讀取時(shí)間的要求,即從字符串中解析時(shí)間并得到一個(gè)Date對象,比如將"2017-8-24 11:17:10"解析為一個(gè)Date對象?,F(xiàn)在問題來了,這個(gè)時(shí)間到底指的是北京時(shí)間的2017年8月24日11:17:10,還是東京時(shí)間的2017年8月24日11:17:10?如果指的是北京時(shí)間,那么這個(gè)時(shí)間對應(yīng)的東京時(shí)間2017年8月24日12:17:10;如果指的是東京時(shí)間,那么這個(gè)時(shí)間對應(yīng)的北京時(shí)間就是2017年8月24日10:17:10。因此,只說年月日時(shí)分秒而不說是哪個(gè)時(shí)區(qū)的,是有歧義的,沒有歧義的做法是,給出一個(gè)時(shí)間字符串,同時(shí)指明這是哪個(gè)時(shí)區(qū)的時(shí)間。
從字符串中解析時(shí)間的正確作法是:指定時(shí)區(qū)來解析。示例如下:
String timeStr = "2017-8-24 11:17:10"; // 字面時(shí)間
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); // 設(shè)置北京時(shí)區(qū)
Date d = sdf.parse(timeStr);
System.out.println(sdf.format(d) + ", " + d.getTime());
輸出為:
2017-08-24 11:17:10, 1503544630000,
將一個(gè)時(shí)間字符串按不同時(shí)區(qū)來解釋,得到的Date對象的值是不同的。驗(yàn)證如下:
String timeStr = "2017-8-24 11:17:10"; // 字面時(shí)間
SimpleDateFormat bjSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
Date bjDate = bjSdf.parse(timeStr);? // 解析
System.out.println("字面時(shí)間: " + timeStr +",按北京時(shí)間來解釋:" + bjSdf.format(bjDate) + ", " + bjDate.getTime());
SimpleDateFormat tokyoSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");? // 東京
tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));? // 設(shè)置東京時(shí)區(qū)
Date tokyoDate = tokyoSdf.parse(timeStr); // 解析
System.out.println("字面時(shí)間: " + timeStr +",按東京時(shí)間來解釋:"? + tokyoSdf.format(tokyoDate) + ", " + tokyoDate.getTime());
輸出為:
字面時(shí)間: 2017-8-24 11:17:10,按北京時(shí)間來解釋:2017-08-24 11:17:10, 1503544630000
字面時(shí)間: 2017-8-24 11:17:10,按東京時(shí)間來解釋:2017-08-24 11:17:10, 1503541030000
可以看出,對于"2017-8-24 11:17:10"這個(gè)字符串,按北京時(shí)間來解釋得到Date對象的毫秒數(shù)是
1503544630000;而按東京時(shí)間來解釋得到的毫秒數(shù)是1503541030000,前者正好比后者大于3600000毫秒即1個(gè)小時(shí),正好是北京時(shí)間和東京時(shí)間的時(shí)差。這很好理解,北京時(shí)間2017-08-24 11:17:10對應(yīng)的毫秒數(shù)是1503544630000,而東京時(shí)間2017-08-24 11:17:10對應(yīng)的北京時(shí)間其實(shí)是2017-08-24 10:17:10(因?yàn)楸本r(shí)間比東京時(shí)間晚1個(gè)小時(shí)),北京時(shí)間2017-08-24 10:17:10自然比北京時(shí)間2017-08-24 11:17:10少3600000毫秒。
5.將字符串表示的時(shí)間轉(zhuǎn)換成另一個(gè)時(shí)區(qū)的時(shí)間字符串
綜合以上分析,如果給定一個(gè)時(shí)間字符串,并告訴你這是某個(gè)時(shí)區(qū)的時(shí)間,要將它轉(zhuǎn)換為另一個(gè)時(shí)區(qū)的時(shí)間并輸出,正確的做法是:
1.將字符串按原時(shí)區(qū)轉(zhuǎn)換成Date對象;
2.將Date對象格式化成目標(biāo)時(shí)區(qū)的時(shí)間。
比如,將北京時(shí)間"2017-8-24 11:17:10"輸出成東京時(shí)間,代碼為:
String timeStr = "2017-8-24 11:17:10"; // 字面時(shí)間
SimpleDateFormat bjSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
Date date = bjSdf.parse(timeStr);? // 將字符串時(shí)間按北京時(shí)間解析成Date對象
SimpleDateFormat tokyoSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");? // 東京
tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));? // 設(shè)置東京時(shí)區(qū)
System.out.println("北京時(shí)間: " + timeStr +"對應(yīng)的東京時(shí)間為:"? + tokyoSdf.format(date));
輸出為:
北京時(shí)間:2017-8-24 11:17:10對應(yīng)的東京時(shí)間為:2017-08-24 12:17:10
5、系統(tǒng)時(shí)區(qū)的修改
查看系統(tǒng)當(dāng)前時(shí)區(qū)
# date -R
#tzselect
Please identify a location so that time zone rules can be set correctly.
Please select a continent, ocean, "coord", or "TZ".
1) Africa
2) Americas
3) Antarctica
4) Asia
5) Atlantic Ocean
6) Australia
7) Europe
8) Indian Ocean
9) Pacific Ocean
10) coord - I want to use geographical coordinates.
11) TZ - I want to specify the time zone using the Posix TZ format.
# 4
Please select a country whose clocks agree with yours.
1) Afghanistan? ? ? ? ? ? 18) Israel? ? ? ? ? ? ? ? ? ? ? ? 35) Palestine
2) Armenia? ? ? ? ? ? ? ? ? 19) Japan? ? ? ? ? ? ? ? ? ? 36) Philippines
3) Azerbaijan? ? ? ? ? ? ? ? ? ? 20) Jordan? ? ? ? ? ? ? ? ? ? ? ? 37) Qatar
4) Bahrain? ? ? ? ? ? ? ? ? 21) Kazakhstan? ? ? ? ? ? ? ? ? ? 38) Russia
5) Bangladesh? ? ? ? ? ? ? ? ? ? 22) Korea (North)? ? ? ? ? ? ? ? 39) Saudi Arabia
6) Bhutan? ? ? ? ? ? ? ? ? ? 23) Korea (South)? ? ? ? ? ? ? ? 40) Singapore
7) Brunei? ? ? ? ? ? ? ? ? ? 24) Kuwait? ? ? ? ? ? ? ? ? ? ? ? 41) Sri Lanka
8) Cambodia? ? ? ? ? ? ? ? ? ? ? 25) Kyrgyzstan? ? ? ? ? ? ? ? ? ? 42) Syria
9) China? ? ? ? ? ? ? ? ? ? 26) Laos? ? ? ? ? ? ? ? ? ? ? 43) Taiwan
10) Cyprus? ? ? ? ? ? ? ? ? ? 27) Lebanon? ? ? ? ? ? ? ? ? ? ? 44) Tajikistan
11) East Timor? ? ? ? ? ? ? ? ? ? 28) Macau? ? ? ? ? ? ? ? ? ? 45) Thailand
12) Georgia? ? ? ? ? ? ? ? ? 29) Malaysia? ? ? ? ? ? ? ? ? ? ? 46) Turkmenistan
13) Hong Kong? ? ? ? ? ? ? ? ? ? 30) Mongolia? ? ? ? ? ? ? ? ? ? ? 47) United Arab Emirates
14) India? ? ? ? ? ? ? ? ? ? 31) Myanmar (Burma)? ? ? ? ? ? ? ? ? 48) Uzbekistan
15) Indonesia? ? ? ? ? ? ? ? ? ? 32) Nepal? ? ? ? ? ? ? ? ? ? 49) Vietnam
16) Iran? ? ? ? ? ? ? ? ? ? ? 33) Oman? ? ? ? ? ? ? ? ? ? ? 50) Yemen
17) Iraq? ? ? ? ? ? ? ? ? ? ? 34) Pakistan
# 9
Please select one of the following time zone regions.
1) Beijing Time
2) Xinjiang Time
# 1
The following information has been given:? ? China? ? Beijing TimeTherefore TZ='Asia/Shanghai' will be used.
更改系統(tǒng)時(shí)區(qū)
#sudo cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
#reboot
6、JAVA運(yùn)行環(huán)境的時(shí)區(qū)
因?yàn)?JDK再安裝時(shí),記錄了系統(tǒng)的時(shí)區(qū)。
System.getProperty("user.timezone");
System.getProperty("user.country");
所以就算修改了系統(tǒng)的時(shí)區(qū),也無法改變 JDK 的環(huán)境時(shí)區(qū)。解決辦法有兩個(gè)
1、重新安裝? JVM
2、通過命令設(shè)置更新時(shí)區(qū)
#timedatectl set-timezone Asia/Shanghai
通過此命令更新時(shí)區(qū)后。JVM 運(yùn)行環(huán)境時(shí)區(qū)就可以更新為指定時(shí)區(qū)