Spring fastjson跨时区问题

相信有的小伙伴有这样的业务场景,服务端时区跟客户端(如浏览器)不一致,接口处理时需要转换掉。
场景一:服务端查询出来的时间是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.getTimeZone("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 = Calendar.getInstance();
        calendar.setTimeInMillis(date.getTime() + timezoneOffsetMillis);
        final SimpleDateFormat dateFormat = new SimpleDateFormat(format);
        dateFormat.setTimeZone(GMT);
        return dateFormat.format(calendar.getTime());
    }

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 = Calendar.getInstance();
        calendar.setTimeInMillis(date.getTime() - timezoneOffsetMillis);
        return calendar.getTime();
    }

    /**
     * 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 = parseJsonFeature(parser.getContext().object, fieldName);
        if (annotation == null) {
            return (T) smartParse(input, DEFAULT_FORMAT, null);
        } else {
            final Class<? extends Supplier<Long>> clz = annotation.timezoneOffsetSup();
            if (supplierCache.get(clz) == null) {
                try {
                    supplierCache.put(clz, clz.newInstance());
                } catch (Exception e) {
                    throw new RuntimeException(clz.getName() + "can not new instance");
                }
            }
            String format = annotation.timeFormat() == null || annotation.timeFormat().length() == 0 ? DEFAULT_FORMAT :
                    annotation.timeFormat();
            return (T) smartParse(input, format, supplierCache.get(clz).get()); // 使用注解提供的偏移量提供函数得到时差偏移量解析
        }
    }

    @Override
    public int getFastMatchToken() {
        return 0;
    }

    @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 = parseJsonFeature(serializer.getContext().object, fieldName);
        if (annotation == null) {
            serializer.write(formatDate((Date) object, DEFAULT_FORMAT, null));
        } else {
            final Class<? extends Supplier<Long>> clz = annotation.timezoneOffsetSup();
            if (supplierCache.get(clz) == null) {
                try {
                    supplierCache.put(clz, clz.newInstance());
                } catch (Exception e) {
                    throw new IOException(clz.getName() + "can not new instance");
                }
            }
            String format = annotation.timeFormat() == null || annotation.timeFormat().length() == 0 ? DEFAULT_FORMAT :
                    annotation.timeFormat();
            serializer.write(formatDate((Date) object, format, supplierCache.get(clz).get())); // 使用注解提供的偏移量提供函数得到时差偏移量格式化
        }
    }

    
    /**
     * parseJsonFeature 解析字段上的JsonFeature注解
     *
     * @param object    目标对象
     * @param fieldName 字段名
     *
     * @return JsonFeature
     */
    private JsonFeature parseJsonFeature(Object object, Object fieldName) {
        try {
            final Field field = object.getClass().getDeclaredField((String) fieldName);
            if (field.isAnnotationPresent(JsonFeature.class)) {
                return field.getAnnotation(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() {
        MapThreadLocalAdaptor.remove(TIMEZONE_OFFSET_MILLIS);
    }

    @Override
    public Long get() {
        final Object value = MapThreadLocalAdaptor.get(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;

}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值