日期時間處理和國際化相關

日期/時間的國際化,不只涉及到地理位置(Locale,好比星期、月份等日曆本地化表示),還涉及到時區(TimeZone,針對UTC/GMT的偏移量)。時區不只是地理位置規定,更是政治規定,好比中國從地理位置上跨5個時區,但只使用一個統一時區(id=Shanghai/Asia)。 javascript

用戶locale/timezone的獲取

  • 猜想:根據IP、HTTP Header(Accept-Language),js腳本等方式猜想,不許確。
  • 客戶請求參數。有一個入口讓用戶選擇locale/timezone,同時也可以讓用戶選擇date/time format偏好。常見於後臺管理系統。

用戶locale/timezone的存儲

  • cookie
  • 用戶profile管理系統

客戶端輸出

      優先使用用戶確認的locale/timezone/format。不然使用應用系統默認,必須顯示時區,好比amazon的限時銷售顯示爲PST時區。 html

客戶端輸入

  • 相對時間:好比一週內,1天前等。直接轉換爲應用系統相對時間。
  • 絕對時間:容許客戶端輸入具體日期/時間的系統,有用戶確認timezone的,需轉換爲應用系統默認timezone再處理。好比:
    DateFormat df = new SimpleDateFormat(pattern, userLocale);
    df.setTimeZone(userTimeZone);
    Date userInputDate = df.parse(inputDate);

服務系統時區

      同一服務系統內全部主機的操做系統、數據庫、JVM,原則上應該使用相同時區。 java

系統交互

      同一服務系統跨時區服務的,日期/時間數據必須帶有時區信息。服務系統之間交換日期/時間數據的,必須帶有時區信息。 mysql

操做系統時區設定

同一服務系統內,數據庫服務器按照其服務的地理位置和時區設置,應用服務器參照數據庫服務器設置。數據庫軟件系統和JVM默認時區不作調整,均採用主機操做系統時區。 web

全部主機開啓NTP,應用服務器向數據庫服務器請求時間同步。 spring

數據庫日期/時間字段類型存儲

mysql(5.5)

字段類型 
字節 
精度 
範圍 
說明 
date
天 
'1000-01-01' to '9999-12-31' 日期 
datetime
秒 
'1000-01-01 00:00:00' to '9999-12-31 23:59:59' 日期和時間混合 
timestamp
秒 
'1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC. 日期和時間混合,轉換爲UTC時區的時間後存儲;insert/update時,由數據庫自動更新。
year
年 
1901 to 2155, or 0000 年份
time
秒 
'-838:59:59' to '838:59:59' 時間值或持續時間

說明: sql

  1. datetime/timestamp可接受微妙級的時間,可是存儲時只保留到秒級別。須要存儲毫秒級別的,可使用bigint類型字段,在應用程序級別進行long型的epoch毫秒和Date類型轉換。
  2. 只有timestamp類型在存儲時,會將值從數據庫時區轉換成UTC時區存儲;檢索時,從UTC時區轉換爲數據庫時區。中途改變數據庫時區,timestamp類型的值將不會正確轉換。
  3. timestamp使用條件限制:一個表最多隻有一個timestamp類型字段會被正確處理。

oracle(10g)

字段類型 字節 
精度 
範圍 
說明
date 

秒 
-4712-01-01 00:00:00 to 9999-12-31 23:59:59 日期和時間混合
timestamp(n) 7-11 秒~納秒 
秒範圍同上 日期和時間混合,小數秒位數(0-9,至關於秒-納秒)可設置 
timestamp WITH TIME ZONE 9-13 
秒~納秒 秒範圍同上 日期和時間混合,同時存儲時區。若輸入不指定時區,則使用數據庫時區 
timestamp WITH LOCAL TIME ZONE 7-11 秒~納秒 秒範圍同上 轉換爲數據庫時區後存儲,不存儲時區 
interval year to month 5 N/A N/A 存儲年或月指定的時間段
interval day to second 11 
N/A N/A 
存儲天,小時,分鐘,秒指定的時間段

說明: 數據庫

  1. timestamp WITH LOCAL TIME ZONE不存儲時區,所以不能中途改變數據庫時區。

sqlserver(2008)

字段類型 字節 精度 
範圍 
說明
smalldatetime
分 
1900-01-1 00:00:00 to 2079-06-06 23:59:00 日期和時間混合
datetime 8 3.33毫秒 
1753-01-01 00:00:00.000 to 9999-12-31 23:59:59.998 日期和時間混合
datetime2(n) 6-8 100ns 0001-01-01 00:00:00.0000000 to 9999-12-31 23:59:59.9999999 日期和時間混合。n爲小數秒精度,取值範圍爲0-7,表示1秒-100ns  
date 3 天 
001-01-01 to 9999-12-31 日期 
time(n) 3-5 100ns 00:00:00.0000000 to 23:59:59.9999999 時間。n爲小數秒精度,取值範圍爲0-7,表示1秒-100ns
datetimeoffset 8-10 
100ns 0001-01-01 00:00:00.0000000 to 9999-12-31 23:59:59.9999999 日期和時間混合。n爲小數秒精度,取值範圍爲0-7,表示1秒-100ns。 
數據庫時區與UTC的時區偏移量(精確到分鐘)被存儲。

小結

  1. 數據庫系統時區默認取自操做系統的時區設置。所以必須正確設定操做系統的時區。
  2. 字段中存儲時區信息的只有sqlserver2008(datetimeoffset)和oracle(timestamp WITH TIME ZONE),能夠安全的跨時區進行數據庫級別導出導入/複製。固然相應的存儲空間也會加大。
  3. mysql和oracle均有一種字段類型支持存儲前進行時區轉換。數據庫時區一旦設定,均不該該更改。
  4. 大多很多天期/時間類型字段都不進行時區轉換存儲。所以應用程序應該使用與數據庫一致的時區。相對使用數據庫sysdate(),now()等時間函數而言,優先使用應用程序傳入時間。
  5. 對於精確到秒的日期/時間類型字段,不該該做爲樂觀鎖和版本管理用途,而應優先使用int4類型。

JDK6日期時間處理相關類

  • java.util.Date的fastTime域和Calendar的time域,存儲特定的瞬間,精確到毫秒。初始值是系統時間距1970-01-01 00:00:00.000 UTC(epoch time)以來的毫秒數。所以能夠認爲這兩個類自己是帶有時區信息的。
  • Calendar經過改變timezone,將時間在不一樣時區轉換表示。相對java.util.Date,Calendar提供了人可讀的日曆字段:年月日小時星期等,還提供了經過修改這些日曆字段來改變時間值的方法。
  • TimeZone主要提供距UTC時區的偏移毫秒數、夏令時規定。
  • DateFormat及其子類,用來格式化calendar域存儲的時間,或將字符串表示解析成java.util.Date。pattern格式化字串和locale屬性提供更靈活的本地化表示能力。也可經過設置timezone,格式化成不一樣時區的時間表示。
  • 經過-Duser.language、-Duser.country、-Duser.timezone設定與操做系統不一致的地理位置和時區,不推薦。

小結

  1. java.util.Date是目前JDK中存儲時間的標準。Calendar提供了獲取和修改時間值的方便方式。
  2. Locale和TimeZone是日期/時間的國際化處理的核心,分別起着不一樣的做用。
  3. Calendar默認爲寬鬆模式(lenient=true),使用DateFormat解析字串時,可設置爲嚴格模式,避免將"2012-01-32"解析成2012-02-01。
  4. 除Locale是線程安全的不可變類外,其餘都是可變類,非線程安全。

JDBC

      jdbc使用java.util.Date的三個子類(見上圖),負責與數據庫系統交互。java.sql.Date只包含日期,java.sql.Time只包含時間,java.sql.Timestamp包含日期和時間混合,精確到納秒。java type, jdbc sql type和數據庫字段類型對應關係以下: apache

java.sql 
java.sql.Types 
mysql 
sqlserver 
oracle 
Date 
DATE 
date 
date 
date 
Time 
TIME 
time 
time 
date 
Timestamp 
TIMESTAMP 
datetime 
timestamp 
datetime 
datetime2 
datetimeoffset 
date 
timestamp [with ....]
String 
VARCHAR     timestamp WITH TIME ZONE
  • timestamp WITH LOCAL TIME ZONE:

當檢索列時,返回給用戶的值被轉換成 TIME_ZONE 會話參數指定的時區(JVM報告的當前時區:TimeZone.getDefault(),來自-Duser.timezone的設定或主機操做系統時區,或設置TimeZone.setDefault(timezone))。 json

當設置列時(PreparedStatement.setTimestamp):設置的值將被轉換成 TIME_ZONE 會話參數指定的時區。

  • 對於timestamp WITH TIME ZONE來講,默認映射爲JDBC VARCHAR,時區信息將以字符串返回。
  • jdbc規範中的ps.setXXX(index, datetime, calendar)和ps.getXXX(index, calendar)不是全部的jdbc驅動都正確實現。見:http://wenku.baidu.com/view/6a06501ffc4ffe473368ab6b.html

小結

  1. java應用程序應該始終使用java.util.Date做爲領域對象的日期/時間屬性類型。
  2. 大部分的數據庫日期/時間字段不保留時區信息,傳遞給數據庫的值依賴於JVM時區。
  3. 注意jdbc sql type和數據庫字段類型的映射關係。
  4. JVM時區儘可能保持與數據庫系統時區一致。當應用系統JVM與數據庫時區不一樣時,讀寫時須要參照數據庫時區轉換。這個工做應該由框架來完成,對開發透明。

mybatis

mybatis關於日期/時間處理的地方,主要是使用TypeHandler:

register(Date.class, new DateTypeHandler());
    register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
    register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
    register(JdbcType.TIMESTAMP, new DateTypeHandler());
    register(JdbcType.DATE, new DateOnlyTypeHandler());
    register(JdbcType.TIME, new TimeOnlyTypeHandler());

    register(java.sql.Date.class, new SqlDateTypeHandler());
    register(java.sql.Time.class, new SqlTimeTypeHandler());
    register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());

各個TypeHandler主要處理java.util.Date類型和jdbc sql type的映射關係和轉換,屏蔽java.sql.Date/Time/Timestamp的差別。好比:

<resultMap ... >
    <result column="createTime" property="createTime" jdbcType="TIMESTAMP" />
</resultMap>

<select ...>
  	select ...
  	from ...
	where ...
	<![CDATA[
	and  createTime>= #{start,jdbcType=TIMESTAMP} and createTime<#{end,jdbcType=TIMESTAMP}
 	]]>
</select>

小結

  1. 考慮到數據庫日期/時間字段的精度問題,按時間段查詢時,start(含)、end(不含)應該剔除時間信息,只保留到日期。
  2. 能夠編寫具有時區轉換的,或者將Date映射成int8的TypeHandler,覆蓋mybatis的默認處理器。

MVC框架

struts2/spring3 mvc針對日期/時間處理和國際化方面,主要涉及到攔截器設置locale上下文環境、日期/時間字符串輸入解析、驗證、國際化顯示這幾個方面。

設置locale context

  • struts2:I18nInterceptor。支持從請求參數、session中獲取用戶設置的locale。
  • spring3 mvc:LocaleResolver的幾個實現。支持從請求參數、session、http header中獲取。

二者均將獲取到的locale暴露到request scope context中,供解析、國際化使用。二者均缺少TimeZone相關的攔截器。若是單點登陸系統增長了用戶profile管理,則還能夠增長基於profile的攔截器實現。

輸入解析

兩個框架都利用DateFormat的parse方法進行解析,支持locale/timezone轉換。

  • struts2:Converter接口和XWorkBasicConverter,將字符串按照一些上下文locale相關的規範格式、rfc3399等進行fallback解析,使用嚴格模式(lenient=false)。當用戶輸入不符合這些規範格式時,將出現錯誤。
  • spring3 mvc:spring3新的Converter框架衆多轉換器中,惟獨缺乏日期/時間轉換相關實現,而是迫使開發者在DateBinder中註冊自定義的CustomDateEditor(提供特定的DateFormat,設置寬鬆模式或嚴格模式)。

在沒法獲知用戶locale/timezone的狀況下,或者用戶輸入格式隨意,經過猜想來解析用戶的時間含義,是不夠準確的。spring框架把這個決定權交給開發者,很明智。所以,在涉及到用戶輸入時,必須明確規範用戶的輸入格式、協商用戶時區。

驗證

在輸入解析時進行了日期/時間格式合法性驗證後,二者的驗證框架,均有針對日期/時間的能否爲空、範圍驗證。

國際化顯示

都可以在二者支持的模板系統中添加相關的宏或tag。實際格式化由暴露在view context中的DateFormat幫助類,根據request scope context中的locale/timezone或缺省設置來處理。

小結

  1. 兩種框架針對日期/時間的輸入、輸出都有成熟和規範的解決方案,正確使用便可,缺少的功能可擴展。
  2. 必須明確用戶的輸入格式,使用嚴格模式解析。協商用戶時區。

xml/json針對日期/時間的處理

xml/json也被MVC框架用來做爲視圖層。更多的時候,用做兩個系統進行信息交互(好比webservice和rest)的一種中間格式。

XML

JAXB規範中的日期/時間格式,遵循XML中的日期/時間規範(ISO8601),JDK內部實現中格式以下:

  • 日期格式:2012-03-08+08:00
  • 時間格式:22:18:13.453+08:00
  • 日期時間格式:2012-03-08T22:18:13.453+08:00

JSON

JSON規範並無定義如何序列化日期時間。json框架主要處理方式以下:

  • long型:自1970-01-01 00:00:00以來的毫秒數。java和javascript中的Date類型內部都這樣表示時間。jackson json框架考慮到性能,默認以這種方式序列化日期類型。
  • String型:常見的是ISO8601標準的表示,如上。

小結

  1. 遵循標準規範。在系統交互時,必須傳遞時區信息;在沒法肯定用戶時區時,顯示時必須帶有時區信息。

總結

規範

  1. 全部主機開啓NTP。同一服務系統,原則上使用同一時區。數據庫系統、JVM,使用主機默認時區。
  2. 若是應用系統與數據庫系統時區不一致,讀寫時應參照數據庫時區轉換。
  3. 系統間交互,日期/時間信息必須帶有時區信息。
  4. 能肯定用戶locale/timezone的,使用用戶時區進行顯示。不然必須顯示應用系統的時區。
  5. 有用戶輸入的,必須明確規範用戶輸入格式,使用嚴格模式。
  6. 格式化字串以常量定義,避免typo。
  7. 理解並遵循各層規範標準和成熟實現(JDBC/MVC/XML/JSON等)。

公共類庫開發原則

  1. 不重複造輪子,儘可能使用普遍成熟的工具類,好比apache.commons.lang,jodatime等,最多加一個門面。
  2. 儘可能使用mybatis/mvc等框架的標準實現,整理最佳實踐文檔;適當擴展mybatis/mvc框架中部分實現。
相關文章
相關標籤/搜索