今天在线上发现一个问题,在使用Jackson进行时间的反序列化时,配置的 @JsonFormat 没有生效
查看源码发现,Jackson在反序列化时间时,会判断json字段值类型,如下:
由于在我们服务里,前端传时间值到后端时采用了时间戳的方式,json值被判断为数字类型,所以Jackson在反序列化时直接简单粗暴的方式处理,将时间戳转换为Date类型:
为了能够按照正确的格式解析时间,抹去后面的时间点,精确到日,只好自定义一个时间解析器。自定义的时间解析器很好实现,网上已经有很多实例代码,只需要继承 JsonDeserializer<T> 就可以。
问题的关键点在于,如何获取到注解上的时间格式,按照注解上的格式去解析,否则每个解析器的实现只能使用一种固定的格式去解析时间。
1. 所以第一步是获取注解上配置的信息
想要获取字段对应的注解信息,只有找到相应的字段,然后通过字段属性获取注解信息,再通过注解信息获取配置的格式。
但找了很久,也没有在既有的参数里找到获取相关字段的方法,只能去翻看源码,最后在这里发现了获取字段信息的方法以及解析器的生成过程,源代码如下:
第一个红框表示解析器是在这里生成的,第二个红框就是获取注解信息的地方
2. 注解获取以后便创建自定义的时间解析器
猜想,我们可不可以也实现这个类,重写生成解析器的方法?那就试试呗~ 我们在自定义的时间解析器上同样实现这个类,重写了生成时间解析器的方法,并初始化一些自定义的信息供解析时间使用(当然猜想是正确的,因为官方就是这么搞的,只是官方的是一个内部类实现的),具体代码如下:
时间解析器代码:
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.google.common.collect.Lists;
import com.tujia.rba.framework.core.remote.api.BizErrorCode;
import com.tujia.rba.framework.core.remote.api.BizException;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DateJsonDeserializer extends JsonDeserializer<Date> implements ContextualDeserializer {
private final static Logger logger = LoggerFactory.getLogger(DateJsonDeserializer.class);
private final static List<String> FORMATS = Lists.newArrayList(
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyyMMdd-HHmmss", "yyyy-MM-dd", "MM-dd", "HH:mm:ss", "yyyy-MM"
);
public final DateFormat df;
public final String formatString;
public DateJsonDeserializer() {
this.df = null;
this.formatString = null;
}
public DateJsonDeserializer(DateFormat df) {
this.df = df;
this.formatString = "";
}
public DateJsonDeserializer(DateFormat df, String formatString) {
this.df = df;
this.formatString = formatString;
}
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
try {
String dateValue = p.getText();
if (df == null || StringUtils.isEmpty(dateValue)) {
return null;
}
logger.info("使用自定义解析器解析字段:{}:时间:{}",p.getCurrentName(),p.getText());
Date date;
if (StringUtils.isNumeric(dateValue)){
date = new Date(Long.valueOf(dateValue));
}else {
String[] patterns = FORMATS.toArray(new String[0]);
date = DateUtils.parseDate(p.getText(),patterns);
}
return df.parse(df.format(date));
} catch (ParseException | SecurityException e) {
logger.error("JSON反序列化,时间解析失败", e);
throw new BizException(BizErrorCode.UNEXPECTED_ERROR);
}
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
if (property != null) {
JsonFormat.Value format = ctxt.getAnnotationIntrospector().findFormat(property.getMember());
if (format != null) {
TimeZone tz = format.getTimeZone();
// First: fully custom pattern?
if (format.hasPattern()) {
final String pattern = format.getPattern();
if (!FORMATS.contains(pattern)){
FORMATS.add(pattern);
}
final Locale loc = format.hasLocale() ? format.getLocale() : ctxt.getLocale();
SimpleDateFormat df = new SimpleDateFormat(pattern, loc);
if (tz == null) {
tz = ctxt.getTimeZone();
}
df.setTimeZone(tz);
return new DateJsonDeserializer(df, pattern);
}
// But if not, can still override timezone
if (tz != null) {
DateFormat df = ctxt.getConfig().getDateFormat();
// one shortcut: with our custom format, can simplify handling a bit
if (df.getClass() == StdDateFormat.class) {
final Locale loc = format.hasLocale() ? format.getLocale() : ctxt.getLocale();
StdDateFormat std = (StdDateFormat) df;
std = std.withTimeZone(tz);
std = std.withLocale(loc);
df = std;
} else {
// otherwise need to clone, re-set timezone:
df = (DateFormat) df.clone();
df.setTimeZone(tz);
}
return new DateJsonDeserializer(df);
}
}
}
return this;
}
}
至此,自定义时间解析器就完成了
但是,为了能够更灵活的控制时间的解析(例如:输入的时间格式和目标时间格式不同),我又重新自定义了一个时间解析的注解,基本仿照官方的 @Format 注解,具体代码如下
自定义时间解析注解:
import com.fasterxml.jackson.annotation.JacksonAnnotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Locale;
import java.util.TimeZone;
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface DeserializeFormat {
public final static String DEFAULT_LOCALE = "##default";
public final static String DEFAULT_TIMEZONE = "##default";
public String pattern() default "";
public String format() default "";
public DeserializeFormat.Shape shape() default DeserializeFormat.Shape.ANY;
public String locale() default DEFAULT_LOCALE;
public String timezone() default DEFAULT_TIMEZONE;
public enum Shape
{
ANY,
SCALAR,
ARRAY,
OBJECT,
NUMBER,
NUMBER_FLOAT,
NUMBER_INT,
STRING,
BOOLEAN
;
public boolean isNumeric() {
return (this == NUMBER) || (this == NUMBER_INT) || (this == NUMBER_FLOAT);
}
public boolean isStructured() {
return (this == OBJECT) || (this == ARRAY);
}
}
public static class Value
{
private final String pattern;
private final String format;
private final DeserializeFormat.Shape shape;
private final Locale locale;
private final String timezoneStr;
// lazily constructed when created from annotations
private TimeZone _timezone;
public Value() {
this("", "", DeserializeFormat.Shape.ANY, "", "");
}
public Value(DeserializeFormat ann) {
this(ann.pattern(),ann.format(), ann.shape(), ann.locale(), ann.timezone());
}
public Value(String p, String f, DeserializeFormat.Shape sh, String localeStr, String tzStr)
{
this(p,f, sh,
(localeStr == null || localeStr.length() == 0 || DEFAULT_LOCALE.equals(localeStr)) ?
null : new Locale(localeStr),
(tzStr == null || tzStr.length() == 0 || DEFAULT_TIMEZONE.equals(tzStr)) ?
null : tzStr,
null
);
}
public Value(String p, String f, DeserializeFormat.Shape sh, Locale l, TimeZone tz)
{
pattern = p;
format = f;
shape = (sh == null) ? DeserializeFormat.Shape.ANY : sh;
locale = l;
_timezone = tz;
timezoneStr = null;
}
public Value(String p, String f, DeserializeFormat.Shape sh, Locale l, String tzStr, TimeZone tz)
{
pattern = p;
format = f;
shape = (sh == null) ? DeserializeFormat.Shape.ANY : sh;
locale = l;
_timezone = tz;
timezoneStr = tzStr;
}
public DeserializeFormat.Value withPattern(String p,String f) {
return new DeserializeFormat.Value(p, f, shape, locale, timezoneStr, _timezone);
}
public DeserializeFormat.Value withShape(DeserializeFormat.Shape s) {
return new DeserializeFormat.Value(pattern, format, s, locale, timezoneStr, _timezone);
}
public DeserializeFormat.Value withLocale(Locale l) {
return new DeserializeFormat.Value(pattern, format, shape, l, timezoneStr, _timezone);
}
public DeserializeFormat.Value withTimeZone(TimeZone tz) {
return new DeserializeFormat.Value(pattern, format, shape, locale, null, tz);
}
public String getPattern() { return pattern; }
public String getFormat() { return format; }
public DeserializeFormat.Shape getShape() { return shape; }
public Locale getLocale() { return locale; }
public String timeZoneAsString() {
if (_timezone != null) {
return _timezone.getID();
}
return timezoneStr;
}
public TimeZone getTimeZone() {
TimeZone tz = _timezone;
if (tz == null) {
if (timezoneStr == null) {
return null;
}
tz = TimeZone.getTimeZone(timezoneStr);
_timezone = tz;
}
return tz;
}
public boolean hasShape() { return shape != DeserializeFormat.Shape.ANY; }
public boolean hasPattern() {
return (pattern != null) && (pattern.length() > 0);
}
public boolean hasFormat() {
return (format != null) && (format.length() > 0);
}
public boolean hasLocale() { return locale != null; }
public boolean hasTimeZone() {
return (_timezone != null) || (timezoneStr != null && !timezoneStr.isEmpty());
}
}
}
使用自定义解析注解的时间解析器
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.google.common.collect.Lists;
import com.tujia.rba.framework.core.remote.api.BizErrorCode;
import com.tujia.rba.framework.core.remote.api.BizException;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DateJsonDeserializer extends JsonDeserializer<Date> implements ContextualDeserializer {
private final static Logger logger = LoggerFactory.getLogger(DateJsonDeserializer.class);
private final static List<String> FORMATS = Lists
.newArrayList("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyyMMdd-HHmmss", "yyyy-MM-dd", "MM-dd", "HH:mm:ss",
"yyyy-MM");
public final DateFormat df;
public final String formatString;
public DateJsonDeserializer() {
this.df = null;
this.formatString = null;
}
public DateJsonDeserializer(DateFormat df) {
this.df = df;
this.formatString = "";
}
public DateJsonDeserializer(DateFormat df, String formatString) {
this.df = df;
this.formatString = formatString;
}
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
try {
String dateValue = p.getText();
if (df == null || StringUtils.isEmpty(dateValue)) {
return null;
}
Date date;
if (StringUtils.isNumeric(dateValue)){
date = new Date(Long.valueOf(dateValue));
}else {
String[] formatArray = FORMATS.toArray(new String[0]);
date = DateUtils.parseDate(p.getText(),formatArray);
}
return df.parse(df.format(date));
} catch (ParseException | SecurityException e) {
logger.error("JSON反序列化,时间解析失败", e);
throw new BizException(BizErrorCode.UNEXPECTED_ERROR);
}
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
if (property != null) {
// JsonFormat.Value format = ctxt.getAnnotationIntrospector().findFormat(property.getMember());
DeserializeFormat deFormat = property.getAnnotation(DeserializeFormat.class);
DeserializeFormat.Value format = (deFormat == null) ? null : new DeserializeFormat.Value(deFormat);
if (format != null) {
TimeZone tz = format.getTimeZone();
// First: fully custom pattern?
if (format.hasPattern() && !FORMATS.contains(format.getPattern())){
FORMATS.add(format.getPattern());
}
if (format.hasFormat()) {
final String dateFormat = format.getFormat();
final Locale loc = format.hasLocale() ? format.getLocale() : ctxt.getLocale();
SimpleDateFormat df = new SimpleDateFormat(dateFormat, loc);
if (tz == null) {
tz = ctxt.getTimeZone();
}
df.setTimeZone(tz);
return new DateJsonDeserializer(df, dateFormat);
}
// But if not, can still override timezone
if (tz != null) {
DateFormat df = ctxt.getConfig().getDateFormat();
// one shortcut: with our custom format, can simplify handling a bit
if (df.getClass() == StdDateFormat.class) {
final Locale loc = format.hasLocale() ? format.getLocale() : ctxt.getLocale();
StdDateFormat std = (StdDateFormat) df;
std = std.withTimeZone(tz);
std = std.withLocale(loc);
df = std;
} else {
// otherwise need to clone, re-set timezone:
df = (DateFormat) df.clone();
df.setTimeZone(tz);
}
return new DateJsonDeserializer(df);
}
}
}
return this;
}
}
@JsonFormat的使用
实体类字段中添加@JsonFormat注解(),返回 yyyy-MM-dd HH:mm:ss 时间格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date joinedDate;
pattern
:日期格式
timezone
:时区
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。