日期时间处理和国际化相关

日期/时间的国际化,不仅涉及到地理位置(Locale,比如星期、月份等日历本地化表示),还涉及到时区(TimeZone,针对UTC/GMT的偏移量)。时区不仅是地理位置规定,更是政治规定,比如中国从地理位置上跨5个时区,但只使用一个统一时区(id=Shanghai/Asia)。

用户locale/timezone的获取

  • 猜测:根据IP、HTTP Header(Accept-Language),js脚本等方式猜测,不准确。
  • 客户请求参数。有一个入口让用户选择locale/timezone,同时也可让用户选择date/time format偏好。常见于后台管理系统。

用户locale/timezone的存储

  • cookie
  • 用户profile管理系统

客户端输出

      优先使用用户确认的locale/timezone/format。否则使用应用系统默认,必须显示时区,比如amazon的限时销售显示为PST时区。

客户端输入

  • 相对时间:比如一周内,1天前等。直接转换为应用系统相对时间。
  • 绝对时间:允许客户端输入具体日期/时间的系统,有用户确认timezone的,需转换为应用系统默认timezone再处理。比如:
    DateFormat df = new SimpleDateFormat(pattern, userLocale);
    df.setTimeZone(userTimeZone);
    Date userInputDate = df.parse(inputDate);

服务系统时区

      同一服务系统内所有主机的操作系统、数据库、JVM,原则上应该使用相同时区。

系统交互

      同一服务系统跨时区服务的,日期/时间数据必须带有时区信息。服务系统之间交换日期/时间数据的,必须带有时区信息。

操作系统时区设定

同一服务系统内,数据库服务器按照其服务的地理位置和时区设置,应用服务器参照数据库服务器设置。数据库软件系统和JVM默认时区不做调整,均采用主机操作系统时区。

所有主机开启NTP,应用服务器向数据库服务器请求时间同步。

数据库日期/时间字段类型存储

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' 时间值或持续时间

说明:

  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和数据库字段类型对应关系如下:

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))。

当设置列时(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的差异。比如:


    "createTime" property="createTime" jdbcType="TIMESTAMP" />


小结

  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框架中部分实现。

转载于:https://my.oschina.net/u/593721/blog/132960

你可能感兴趣的:(数据库,测试,json)