最近在工作中遇到一个问题,就是日期国际化的问题,因为我们的网站是面向全世界的,当不同国家不同时区的人访问我们的网站时希望看到的是本地的日期,而不是中国的日期。网上大部分教程都是针对中国进行日期转换,而没有考虑过国际化的情况下日期应该怎么显示。
日期国际化传输和存储网上一直没有一个明确的方案,大部分都是国内的日期转换,这样转换会丢失掉一部分日期信息。其实spring boot自带的日期处理已经可以满足国际化的需求。
Spring boot默认使用ISO-8601日期标准格式进行日期的序列化和反序列化。该日期格式为yyyy-MM-dd'T'HH:mm:ss.SSSZ
后端存储的时候,spring boot的默认序列化与反序列化工具jackson 会使用com.fasterxml.jackson.databind.util.StdDateFormat进行日期处理。
当日期从服务端序列化到浏览器的时候会调用formart获取默认utc时间区域
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo,
FieldPosition fieldPosition)
{
TimeZone tz = _timezone;
if (tz == null) {
tz = DEFAULT_TIMEZONE;
}
_format(tz, _locale, date, toAppendTo);
return toAppendTo;
}
然后调用
protected void _format(TimeZone tz, Locale loc, Date date,
StringBuffer buffer)
{
Calendar cal = _getCalendar(tz);
cal.setTime(date);
// [databind#2167]: handle range beyond [1, 9999]
final int year = cal.get(Calendar.YEAR);
// Assuming GregorianCalendar, special handling needed for BCE (aka BC)
if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
_formatBCEYear(buffer, year);
} else {
if (year > 9999) {
// 22-Nov-2018, tatu: Handling beyond 4-digits is not well specified wrt ISO-8601, but
// it seems that plus prefix IS mandated. Padding is an open question, but since agreeement
// for max length would be needed, we ewould need to limit to arbitrary length
// like five digits (erroring out if beyond or padding to that as minimum).
// Instead, let's just print number out as is and let decoder try to make sense of it.
buffer.append('+');
}
pad4(buffer, year);
}
buffer.append('-');
pad2(buffer, cal.get(Calendar.MONTH) + 1);
buffer.append('-');
pad2(buffer, cal.get(Calendar.DAY_OF_MONTH));
buffer.append('T');
pad2(buffer, cal.get(Calendar.HOUR_OF_DAY));
buffer.append(':');
pad2(buffer, cal.get(Calendar.MINUTE));
buffer.append(':');
pad2(buffer, cal.get(Calendar.SECOND));
buffer.append('.');
pad3(buffer, cal.get(Calendar.MILLISECOND));
int offset = tz.getOffset(cal.getTimeInMillis());
if (offset != 0) {
int hours = Math.abs((offset / (60 * 1000)) / 60);
int minutes = Math.abs((offset / (60 * 1000)) % 60);
buffer.append(offset < 0 ? '-' : '+');
pad2(buffer, hours);
if( _tzSerializedWithColon ) {
buffer.append(':');
}
pad2(buffer, minutes);
} else {
// 24-Jun-2017, tatu: While `Z` would be conveniently short, older specs
// mandate use of full `+0000`
// formatted.append('Z');
if( _tzSerializedWithColon ) {
buffer.append("+00:00");
}
else {
buffer.append("+0000");
}
}
}
将带有本地化日期的数据转为零时区的数据,返回给前端。
当后端接收到前端日期相关数据时,会调用
@Override
public Date parse(String dateStr) throws ParseException
{
dateStr = dateStr.trim();
ParsePosition pos = new ParsePosition(0);
Date dt = _parseDate(dateStr, pos);
if (dt != null) {
return dt;
}
StringBuilder sb = new StringBuilder();
for (String f : ALL_FORMATS) {
if (sb.length() > 0) {
sb.append("\", \"");
} else {
sb.append('"');
}
sb.append(f);
}
sb.append('"');
throw new ParseException
(String.format("Cannot parse date \"%s\": not compatible with any of standard forms (%s)",
dateStr, sb.toString()), pos.getErrorIndex());
}
方法进行解析。获得Date对象。然后存储进数据库。
不管前端传输的是什么时区格式的数据,数据库日期格式选的时DateTime还是timestamp。从后端获取时取得的都是零时区的数据
列子:
存储:{"id":4,"name":"eee","createTime":"2019-05-21T22:18:55.000+0800"}
获取:{"id":4,"name":"eee","createTime":"2019-05-21T14:18:55.000+0000"}
所以后端不用进行日期转换,相反前端需要做一定的处理。前端需要引入Moment-Timezone组件,使用moment.guess()获取浏览器所在时区,然后调用
var newYork = moment.tz("2014-06-01 12:00", "America/New_York");
这样的方法将日期转换为当前时区的显示方式。