Java時間轉換的方法 系統開發過程中常因為時間參數的存儲和呈現方式的問題產生爭議,再加上考慮不同時區的時間在同一系統存儲和展示的情況更為複雜。通常的設計方案是:存儲的時候,為了不讓數據混亂,統一按照UTC+00:00時區的毫秒級長整型數字時間戳來存儲;展示的時候,為了讓用戶方便,按照用戶關註的時區 ...
Java時間轉換的方法
系統開發過程中常因為時間參數的存儲和呈現方式的問題產生爭議,再加上考慮不同時區的時間在同一系統存儲和展示的情況更為複雜。通常的設計方案是:存儲的時候,為了不讓數據混亂,統一按照UTC+00:00時區的毫秒級長整型數字時間戳來存儲;展示的時候,為了讓用戶方便,按照用戶關註的時區來呈現時間,如:“年-月-日 時:分:秒”。
那麼,第一個問題來了。用戶關註的時區究竟如何選取呢?難道就一定是用戶所在地的時區嗎?!
第二個問題,是Java本身的規則一手造成的。比如:1436013000000,這個時間戳是UTC+00:00時區“2015-07-04 12:30:00”對應的毫秒級長整型數字時間戳。在C++當中,無論我們用的電腦配置為何時區,雙向轉換的結果會是一致的。但如果用Java,轉換結果也是關於電腦時區配置的。比如:我們的電腦時區配置為UTC+00:00時區,根據1436013000000這個數字時間戳轉換得到的時間就會是“2015-07-04 12:30:00”;如果我們的電腦時區配置為UTC+08:00時區,根據1436013000000這個數字時間戳轉換得到的時間就會是“2015-07-04 20:30:00”。Java在這個轉換過程中自動為這個參數加上東8區28800000毫秒的時區偏移量。從時間向數字時間戳的轉換過程中,Java又會自動為這個參數減掉28800000毫秒。
錶面上看起來Java的這個機制很人性化。實則不然,如果運行系統的伺服器部署在不同時區的話,麻煩就來了。伺服器部署的時區直接影響了時間參數的轉換結果。
本文,我就對這兩個問題給出一個解決方案。大家可以參考,但是遇到不同情況,建議按照用戶的真實需求,具體問題具體分析。
1 理論時區
理論時區以被15整除的子午線為中心,向東西兩側延伸7.5度,即每15°劃分一個時區,這是理論時區。理論時區的時間採用其中央經線(或標準經線)的地方時。所以每差一個時區,區時相差一個小時,相差多少個時區,就相差多少個小時。東邊的時區時間比西邊的時區時間來得早。為了避免日期的紊亂,提出國際日期變更線的概念。
2 夏令時
夏令時(DaylightSaving Time:DST),又稱“日光節約時制”和“夏令時間”,是一種為節約能源而人為規定地方時間的制度,在這一制度實行期間所採用的統一時間稱為“夏令時間”。一般在天亮早的夏季人為將時間提前一小時,以充分利用光照資源,從而節約照明用電。各個採納夏時制的國傢具體規定不同。目前全世界有近110個國家每年要實行夏令時。
3 Unix Time
Unix時間戳(英文為Unix epoch, Unix time, POSIX time 或 Unix timestamp)是從1970年1月1日(UTC/GMT的午夜00:00:00)開始所經過的毫秒數,不考慮閏秒。也就是說,1970-1-100:00:00的數字時間戳是0,當前時間的數字時間戳是此時此地的時間相距1970-1-100:00:00的毫秒數。相同的計算規則,但在不同的編程語言中的實現仍有差別,以C++和Java為例。C++認為時間是0時區的,而Java計算是帶著系統時區的。
4 時間轉換問題解決方案
解決第一個問題,方案引入了一個“工程時區”的概念,含義是時間展示的時區標準,也就是說用戶查看的時間按照工程時區來展示“年-月-日 時:分:秒”。
解決第二個問題,需要一個轉換方法自動屏蔽運行代碼的電腦所帶時區的影響。無論系統部署在哪裡,系統都要能夠準確呈現關註對象所在地的時間(不以用戶所在地/系統部署地的時區為轉移)。因為關註對象的信息才是用戶真正關註的。
將關註對象所在地的時間“yyyy-MM-dd HH:mm:ss”轉換為UTC+0的毫秒級整型時間戳,關註對象所在地時區最好與工程時區相同,但如果關註對象所在範圍跨時區,關註對象所在地時區與工程時區也可以不同,但這樣呈現的時間數據會與輸入有差別,其含義是與關註對象持續時間同時刻的工程時區的時間。
舉例說明,場景:用戶在雅典(東2區),預定一張從北京(東8區)飛往夏威夷(西10區)的機票。訂票系統部署在紐約(西5區)。系統處理規則:起飛地點是北京,那麼系統呈現的起飛時間,當然應該標註清楚北京(東8區)的時間“yyyy-MM-dd HH:mm:ss”;抵達地點是夏威夷,系統呈現的預計到達時間,當然應該標註清楚夏威夷(西10區)的時間“yyyy-MM-dd HH:mm:ss”。系統呈現的時間與用戶訂票位置——雅典(東2區)的時區、系統部署位置——紐約(西5區)無關。
驗收標準:不論時間數據按照哪個時區輸入,存儲為GMT/UTC+0時區同時刻的毫秒級整型時間戳,用戶能夠感知到的時間都按照工程時區來呈現和輸出。
5 示例
代碼示例只為說明問題,不好勿噴。
1)Java8之前的解決方案
(1)轉換工具類——DateTimeUtil
package dateTime;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import org.apache.commons.lang.StringUtils;
public class DateTimeUtil
{
/**
* datetime format
*/
public static final String YMDHMS = "yyyy-MM-dd HH:mm:ss";
/**
* REGEX_DATETIME
*/
private static final String REGEX_DATETIME = "^((\\d{2}(([02468][048])|([13579][26]))[\\-]?"
+ "((((0?[13578])|(1[02]))[\\-]?((0?[1-9])|([1-2][0-9])|(3[01])))"
+ "|(((0?[469])|(11))[\\-]?((0?[1-9])|([1-2][0-9])|(30)))"
+ "|(0?2[\\-]?((0?[1-9])|([1-2][0-9])))))"
+ "|(\\d{2}(([02468][1235679])|([13579][01345789]))[\\-]?"
+ "((((0?[13578])|(1[02]))[\\-]?((0?[1-9])|([1-2][0-9])|(3[01])))"
+ "|(((0?[469])|(11))[\\-]?((0?[1-9])|([1-2][0-9])|(30)))"
+ "|(0?2[\\-]?((0?[1-9])|(1[0-9])|(2[0-8]))))))" + "\\s"
+ "(((0?[0-9])|(1[0-9])|(2[0-3]))\\:([0-5]?[0-9])((\\s)|(\\:([0-5]?[0-9]))))?$";
/**
* convertToUtcTimeStamp<br>
* 將關註對象所在地的時間"yyyy-MM-dd HH:mm:ss"轉換為UTC+0的毫秒級整型時間戳
* 關註對象所在地時區最好與工程時區相同,
* 但如果關註對象所在範圍跨時區,關註對象所在地時區與工程時區也可以不同,
* 但這樣呈現的時間數據會與輸入有差別,其含義是與關註對象持續時間同時刻的工程時區的時間。
* 時間處理原則:不論時間數據按照哪個時區輸入,
* 存儲為UTC+0時區同時刻的毫秒級整型時間戳,用戶能夠感知到的時間都按照工程時區來呈現。
* @param strDateTime 關註對象所在地的時間"yyyy-MM-dd HH:mm:ss"
* @param iTimeZoneInMilliSec 關註對象所在地時區偏移量(毫秒級整型時間戳)
* @param iDstInMilliSec 關註對象所在地夏令時偏移量(毫秒級整型時間戳)
* @return UTC+0的毫秒級整型時間戳
*/
public static long convertToUtcTimeStamp(String strDateTime,
long iTimeZoneInMilliSec,
long iDstInMilliSec)
{
if (StringUtils.isEmpty(strDateTime) || !strDateTime.matches(REGEX_DATETIME))
{
return 0L;
}
Date oTime = null;
try
{
oTime = parseToDate(strDateTime);
}
catch (Exception e)
{
e.getMessage();
}
long iTimeStamp = 0L;
if (null != oTime)
{
iTimeStamp = oTime.getTime();
}
Calendar oCalendar = Calendar.getInstance(); // 獲取當前日曆對象
long iServerTimeZoneOffset = oCalendar.get(Calendar.ZONE_OFFSET); // 獲取當前時區偏移量
long iServerDSTOffset = oCalendar.getTimeZone().getDSTSavings(); // 獲取當前夏令時偏移量
// 計算UTC+0的毫秒級整型時間戳
long iUtcTimeStamp = iTimeStamp + iServerTimeZoneOffset + iServerDSTOffset
- iTimeZoneInMilliSec - iDstInMilliSec;
return iUtcTimeStamp;
}
/**
* convertToPrjDateTime<br>
* 將UTC+0的毫秒級整型時間戳轉換為工程時區的時間"yyyy-MM-dd HH:mm:ss"<br/>
* @param iUtcTimeStamp UTC+0的毫秒級整型時間戳
* @param iPrjTimeZoneInMilliSec 工程時區偏移量(毫秒級整型時間戳)
* @param iPrjDstInMilliSec 工程夏令時偏移量(毫秒級整型時間戳)
* @return 工程時區的時間"yyyy-MM-dd HH:mm:ss"
*/
public static String convertToPrjDateTime(long iUtcTimeStamp,
long iPrjTimeZoneInMilliSec,
long iPrjDstInMilliSec)
{
Calendar oCalendar = Calendar.getInstance(); // 獲取當前日曆對象
long iServerTimeZoneOffset = oCalendar.get(Calendar.ZONE_OFFSET); // 獲取當前時區偏移量
long iServerDSTOffset = oCalendar.getTimeZone().getDSTSavings(); // 獲取當前夏令時偏移量
// 計算工程時區的時間
String strPrjDateTime = "";
Date oPrjDateTime = new Date(iUtcTimeStamp - iServerTimeZoneOffset - iServerDSTOffset
+ iPrjTimeZoneInMilliSec + iPrjDstInMilliSec);
strPrjDateTime = dateFormat(YMDHMS, oPrjDateTime);
return strPrjDateTime;
}
/**
* parseToDate
* @param value TimeStamp / 年-月-日 時:分:秒
* @return Date
* @throws Exception
*/
private static Date parseToDate(String value) throws Exception
{
Date date = null;
try
{
Long dataValue = Long.valueOf(value);
date = new Date(dataValue);
}
catch (Exception e)
{
date = parseDate(value, YMDHMS);
}
return date;
}
/**
* parseDate
* @param strDate 年-月-日 時:分:秒
* @param strFormat yyyy-MM-dd HH:mm:ss
* @return Date
* @throws Exception
*/
private static Date parseDate(String strDate, String strFormat) throws Exception
{
if (StringUtils.isEmpty(strFormat))
{
strFormat = YMDHMS;
}
DateFormat sdf = new SimpleDateFormat(strFormat);
return sdf.parse(strDate);
}
/**
* dateFormat
* @param strFormat yyyy-MM-dd HH:mm:ss
* @param oDate Date
* @return 年-月-日 時:分:秒
*/
private static String dateFormat(String strFormat, Date oDate)
{
SimpleDateFormat sdf = new SimpleDateFormat(strFormat);
String strDate = sdf.format(oDate);
return strDate;
}
}
(2)轉換Demo——DateTimeTest
package DateTimeTest;
public class DateTimeTest
{
publicstaticvoid main(String[] args)
{
new DateTimeTest();
}
public DateTimeTest()
{
testConvertDateTime();
}
/**
* 測試日期時間轉換不應該受到系統時區的影響。
* 無論系統部署在哪個時區,都應該準確存儲GMT時間戳,呈現工程時區下的年-月-日 時:分:秒
*/
privatevoid testConvertDateTime()
{
String strDateTime01 = "2015-07-04 20:30:00";
longiDateTime01 = DateTimeUtil.convertToUtcTimeStamp(strDateTime01, 28800000l, 0);
System.out.println(iDateTime01);
longiDateTime02 = 1436013000000L;
String strDateTime02 = DateTimeUtil.convertToPrjDateTime(iDateTime02, 28800000l, 0);
System.out.println(strDateTime02);
}
}
2)Java8之後的解決方案
(1)轉換工具類——ZonedDateTimeUtil
package dateTime;
import org.apache.commons.lang.StringUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class ZonedDateTimeUtil {
public static enum EnumDateTimeFormatter {
YMDHMS("yyyy-MM-dd HH:mm:ss", 1),
YMD("yyyy-MM-dd", 2),
HMS("HH:mm:ss", 3);
String name = "";
int index = 0;
private EnumDateTimeFormatter(String name, int index) {
this.name = name;
this.index = index;
}
public String getInfo() {
return this.name;
}
}
public static enum EnumZone {
ZONE_0("+00:00", 0),
ZONE_EAST1("+01:00", 1),
ZONE_EAST2("+02:00", 2),
ZONE_EAST3("+03:00", 3),
ZONE_EAST4("+04:00", 4),
ZONE_EAST5("+05:00", 5),
ZONE_EAST6("+06:00", 6),
ZONE_EAST7("+07:00", 7),
ZONE_EAST8("+08:00", 8),
ZONE_EAST9("+09:00", 9),
ZONE_EAST10("+10:00", 10),
ZONE_EAST11("+11:00", 11),
ZONE_EAST12("+12:00", 12),
ZONE_EAST13("+13:00", 13),
ZONE_WEST1("-01:00", 101),
ZONE_WEST2("-02:00", 102),
ZONE_WEST3("-03:00", 103),
ZONE_WEST4("-04:00", 104),
ZONE_WEST5("-05:00", 105),
ZONE_WEST6("-06:00", 106),
ZONE_WEST7("-07:00", 107),
ZONE_WEST8("-08:00", 108),
ZONE_WEST9("-09:00", 109),
ZONE_WEST10("-10:00", 110),
ZONE_WEST11("-11:00", 111),
ZONE_WEST12("-12:00", 112);
String name = "";
int index = 0;
private EnumZone(String name, int index) {
this.name = name;
this.index = index;
}
public String getInfo() {
return this.name;
}
}
/**
* convertToUtcTimeStamp
* 將關註對象所在地的時間"yyyy-MM-dd HH:mm:ss"轉換為UTC+0的毫秒級整型時間戳
* <p>
* 關註對象所在地時區最好與工程時區相同,
* 但如果關註對象所在範圍跨時區,關註對象所在地時區與工程時區也可以不同,
* 但這樣呈現的時間數據會與輸入有差別,其含義是與關註對象持續時間同時刻的工程時區的時間。
* 時間處理原則:不論時間數據按照哪個時區輸入,
* 存儲為UTC+0時區同時刻的毫秒級整型時間戳,用戶能夠感知到的時間都按照工程時區來呈現和輸出。
*
* @param strDateTime 關註對象所在地的時間"yyyy-MM-dd HH:mm:ss"
* @param enumDateTimeFormatter 時間格式
* @param enumZone 工程時區
* @return UTC+0的毫秒級整型時間戳
*/
public static long convertToUtcTimeStamp(String strDateTime,
EnumDateTimeFormatter enumDateTimeFormatter,
EnumZone enumZone) {
if (StringUtils.isEmpty(strDateTime)
|| null == enumDateTimeFormatter
|| null == enumZone) {
return 0L;
}
long iDateTime = ZonedDateTime.parse(
strDateTime,
DateTimeFormatter.ofPattern(enumDateTimeFormatter.getInfo()).withZone(ZoneId.of(enumZone.getInfo())))
.toInstant()
.toEpochMilli();
return iDateTime;
}
/**
* convertToPrjDateTime
* 將UTC+0的毫秒級整型時間戳轉換為工程時區的時間"yyyy-MM-dd HH:mm:ss"
*
* @param iDateTime UTC+0的毫秒級整型時間戳
* @param enumDateTimeFormatter 時間格式
* @param enumZone 工程時區
* @return 工程時區的時間"yyyy-MM-dd HH:mm:ss"
*/
public static String convertToPrjDateTime(long iDateTime,
EnumDateTimeFormatter enumDateTimeFormatter,
EnumZone enumZone) {
if (null == enumDateTimeFormatter
|| null == enumZone) {
return "";
}
String strDateTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(iDateTime), ZoneId.of(enumZone.getInfo()))
.format(DateTimeFormatter.ofPattern(enumDateTimeFormatter.getInfo()));
return strDateTime;
}
}
(2)轉換Demo——ZonedDateTimeTest
package dateTime;
public class ZonedDateTimeTest {
public static void main(String[] args) {
new ZonedDateTimeTest();
}
public ZonedDateTimeTest() {
test();
}
private void test() {
String strDateTime01 = "2015-07-04 20:30:00";
long iDateTime02 = 1436013000000L;
// java8灝佽
long iDateTime = ZonedDateTimeUtil.convertToUtcTimeStamp(strDateTime01,
ZonedDateTimeUtil.EnumDateTimeFormatter.YMDHMS,
ZonedDateTimeUtil.EnumZone.ZONE_EAST8);
System.out.println(iDateTime);
String strDateTime = ZonedDateTimeUtil.convertToPrjDateTime(iDateTime02,
ZonedDateTimeUtil.EnumDateTimeFormatter.YMDHMS,
ZonedDateTimeUtil.EnumZone.ZONE_EAST8);
System.out.println(strDateTime);
String strDateTime1 = ZonedDateTimeUtil.convertToPrjDateTime(iDateTime02,
ZonedDateTimeUtil.EnumDateTimeFormatter.YMD,
ZonedDateTimeUtil.EnumZone.ZONE_EAST8);
System.out.println(strDateTime1);
String strDateTime2 = ZonedDateTimeUtil.convertToPrjDateTime(iDateTime02,
ZonedDateTimeUtil.EnumDateTimeFormatter.HMS,
ZonedDateTimeUtil.EnumZone.ZONE_EAST8);
System.out.println(strDateTime2);
}
}