Springfastjson跨时区问题
相信有的⼩伙伴有这样的业务场景,服务端时区跟客户端(如浏览器)不⼀致,接⼝处理时需要转换掉。
场景⼀:服务端查询出来的时间是UTC时间,客户端在中国,需要展⽰为东⼋区时间;
场景⼆:客户端在中国,时间是东⼋区时间,服务端使⽤的UTC时间,存⼊时需要转换客户端时间为UTC时间;
场景三:客户端⽤户在UTC时区、东⼋区都有分布。
我们知道SpringMVC下,经常会配置HttpMessageConverter进⾏消息转换,如常⽤的ByteArrayHttpMessageConverter、StringHttpMessageConverter、MappingJackson2HttpMessageConverter、FastJsonHttpMessageConverter等。
以FastJsonHttpMessageConverter为例,发现fastjson序列化、反序列化时⽀持指定⾃定义序列化、反序列化类型,使⽤
@JSONField(serializeUsing = xxx.class)指定,那么我们尝试⾃定义序列化、反序列化类来解决时差问题。
⾸先,假设如果知道客户端跟服务端的时差,那么就可以解决java.util.Date格式化成String或将String解析成java.util.Date的问题1 定义格式化函数
private static final String DEFAULT_FORMAT ="yyyy-MM-dd HH:mm:ss";
private static final TimeZone GMT = TimeZone("GMT");
/**
* formatDate 格式化时间
*
* @param date                时间
* @param targetFormat        ⽬标格式
* @param timezoneOffsetMillis ⽬标时区与标准时区的时间差(毫秒)
*
* @return java.lang.String
*/
private String formatDate(Date date, String targetFormat, Long timezoneOffsetMillis){
String format = targetFormat == null ? DEFAULT_FORMAT : targetFormat;
if(timezoneOffsetMillis == null){
return new SimpleDateFormat(format).format(date);
}
Calendar calendar = Instance();
calendar.Time()+ timezoneOffsetMillis);
final SimpleDateFormat dateFormat =new SimpleDateFormat(format);
dateFormat.setTimeZone(GMT);
return dateFormat.Time());
}
2 定义解析函数
private static final List<String> NUMERIC_AUTO_FORMATS = Arrays.asList("yyyyMMddHHmmss",
"yyyyMMddHH","yyyyMMdd");
private static final List<String> STRING_AUTO_FORMATS = Arrays.asList("yyyy-MM-dd HH:mm:ss.SSS",
"yyyy-MM-dd HH:mm:ss","yyyy-MM-dd HH:mm","yyyy-MM-dd HH","yyyy-MM-dd");
/**
* smartParse 智能解析(尝试按⽬标格式以及预定义格式解析)
*
* @param dateStr              时间字符串
* @param targetFormat        ⽬标格式
* @param timezoneOffsetMillis ⽬标时区与标准时区的时间差(毫秒)
*
* @return java.util.Date
*/
private Date smartParse(String dateStr, String targetFormat, Long timezoneOffsetMillis){
if(dateStr == null || dateStr.length()==0){
return null;
}
TimeZone timeZone = timezoneOffsetMillis == null ? null : GMT;
Date date =multiParse(dateStr, Arrays.asList(targetFormat), timeZone);
if(date == null){
date =multiParse(dateStr,isNumeric(dateStr)? NUMERIC_AUTO_FORMATS : STRING_AUTO_FORMATS, timeZone);
}
if(date == null){
return null;
}
if(timezoneOffsetMillis == null){
return date;
}
Calendar calendar = Instance();
calendar.Time()- timezoneOffsetMillis);
Time();
}
/**
* multiParse 多格式解析(尝试⽤多种格式解析字符串为时间)
*
* @param dateStr  时间字符串
* @param formats  格式
* @param timeZone 时区
*
* @return java.util.Date
*/
private Date multiParse(String dateStr,@NotNull Collection<String> formats, TimeZone timeZone){
for(String format : formats){
try{
final SimpleDateFormat dateFormat =new SimpleDateFormat(format);
if(timeZone != null){
dateFormat.setTimeZone(timeZone);
}
Date date = dateFormat.parse(dateStr);
if(date != null){
return date;
}
}catch(Exception e){
}
}
return null;
}
/**
* isNumeric 判读⼀个字符串是否是数字
*
* @param str 输⼊字符串
*
* @return boolean
*/
private static boolean isNumeric(String str){
if(str == null){
return false;
}
int sz = str.length();
for(int i =0; i < sz; i++){
if(Character.isDigit(str.charAt(i))==false){
return false;
}
}
return true;
}
3 定义时差偏移获取描述
那么剩下的问题就是客户端与服务端交互时获取这个时差了。客户端与服务端通过接⼝交互时,可将时差放置在上下⽂中,如HTTP请求,可放在header中或其它传参位置。如前端可以通过js代码new Date().getTimezoneOffset()获取标准时区与当前时区相差的分钟数,稍加转化下即可得到与标准时区时差毫秒数,传递给服务端。
服务端收到时差后可以放在ThreadLocal中,这样执⾏json序列化与反序列化可以从ThreadLocal中取时差。为更具通⽤性,我们先定义⼀个注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface JsonFeature {
/**
* 时间格式
*/
String timeFormat()default"yyyy-MM-dd HH:mm:ss";
/**
* 时间偏移函数
*/
Class<?extends Supplier<Long>>timezoneOffsetSup()default NoneSupplier.class;
/**
* ⽆偏移函数
*/
final class NoneSupplier implements Supplier<Long>{
@Override
public Long get(){
return null;
}
}
}
4 实现序列化与反序列化
然后实现⾃定义序列化与反序列化的⼯具类
@Slf4j
public class OffsetTimeJsonCodec implements ObjectSerializer, ObjectDeserializer {
private static ConcurrentMap<Class<?extends Supplier<Long>>, Supplier<Long>> supplierCache =
new ConcurrentHashMap<>();
@Override
public<T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName){
final Object fieldValue = parser.parse(fieldName);
if(fieldValue == null){
return null;
}
final String input = String.valueOf(fieldValue);
JsonFeature annotation =Context().object, fieldName);
if(annotation == null){
return(T)smartParse(input, DEFAULT_FORMAT, null);
}else{
final Class<?extends Supplier<Long>> clz = annotation.timezoneOffsetSup();
(clz)== null){
try{
supplierCache.put(clz, wInstance());
}catch(Exception e){
throw new Name()+"can not new instance");
}
}
String format = annotation.timeFormat()== null || annotation.timeFormat().length()==0? DEFAULT_FORMAT :
annotation.timeFormat();
return(T)smartParse(input, format, (clz).get());// 使⽤注解提供的偏移量提供函数得到时差偏移量解析}
}
@Override
public int getFastMatchToken(){
return0;
}
}
@Override
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType,int features)
throws IOException {
if(object == null){
serializer.write(null);
return;
}
JsonFeature annotation =Context().object, fieldName);
if(annotation == null){
serializer.write(formatDate((Date) object, DEFAULT_FORMAT, null));
}else{
final Class<?extends Supplier<Long>> clz = annotation.timezoneOffsetSup();
(clz)== null){
try{
浏览器json格式化
supplierCache.put(clz, wInstance());
}catch(Exception e){
throw new Name()+"can not new instance");
}
}
String format = annotation.timeFormat()== null || annotation.timeFormat().length()==0? DEFAULT_FORMAT :
annotation.timeFormat();
serializer.write(formatDate((Date) object, format, (clz).get()));// 使⽤注解提供的偏移量提供函数得到时差偏移量格式化}
}
/**
* parseJsonFeature 解析字段上的JsonFeature注解
*
* @param object    ⽬标对象
* @param fieldName 字段名
*
* @return JsonFeature
*/
private JsonFeature parseJsonFeature(Object object, Object fieldName){
try{
final Field field = Class().getDeclaredField((String) fieldName);
if(field.isAnnotationPresent(JsonFeature.class)){
Annotation(JsonFeature.class);
}
}catch(Exception e){
log.warn("parse JsonFeature fail, fieldName={}, msg={}", fieldName, e.getMessage());
}
return null;
}
}
5 举例:ThreadLocal偏移量
5.1 定义偏移量获取⽅式
定义获取时区偏移量的⼀种⽅式
public class ThreadLocalTimeOffsetSupplier implements Supplier<Long>{
private static final String TIMEZONE_OFFSET_MILLIS ="_OFFSET_MILLIS_";
public static void put(Long timezoneOffsetMillis){
MapThreadLocalAdaptor.put(TIMEZONE_OFFSET_MILLIS, timezoneOffsetMillis);
}
public static void clear(){
}
@Override
public Long get(){
final Object value = (TIMEZONE_OFFSET_MILLIS);
return value == null ? null : Long.valueOf(String.valueOf(value));
}
}
MapThreadLocalAdaptor是ThreadLocal的⼀个⼯具
5.2 接⼝拦截,设置时区偏移
如HTTP请求,⾃定义filter,获取传输时间偏移参数,写⼊到ThreadLocal中
6 VO字段上指定序列化类
服务端时间序列化成字符串到客户端展⽰
@Data
@Accessors(chain =true)
public class SysUserVo implements Serializable {
private String account;
private String email;
@JsonFeature(timezoneOffsetSup = ThreadLocalTimeOffsetSupplier.class)
@JSONField(serializeUsing = OffsetTimeJsonCodec.class)// 使⽤serializeUsing属性private Date createTime;
@JsonFeature(timezoneOffsetSup = ThreadLocalTimeOffsetSupplier.class)
@JSONField(serializeUsing = OffsetTimeJsonCodec.class)
private Date updateTime;
}
接收客户端时间字符串转成服务端时间
@Data
@Accessors(chain =true)
public class UpdateUserReq implements Serializable {
private String account;
private String email;
@JsonFeature(timezoneOffsetSup = ThreadLocalTimeOffsetSupplier.class)
@JSONField(deserializeUsing = OffsetTimeJsonCodec.class)// 使⽤deserializeUsing属性private Date updateTime;
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。