Jackson反序列化@JsonFormat 不生效的解决方案

作者:无名后生 时间:2023-06-15 20:25:37 

今天在线上发现一个问题,在使用Jackson进行时间的反序列化时,配置的 @JsonFormat 没有生效

查看源码发现,Jackson在反序列化时间时,会判断json字段值类型,如下:

Jackson反序列化@JsonFormat 不生效的解决方案

由于在我们服务里,前端传时间值到后端时采用了时间戳的方式,json值被判断为数字类型,所以Jackson在反序列化时直接简单粗暴的方式处理,将时间戳转换为Date类型:

Jackson反序列化@JsonFormat 不生效的解决方案

为了能够按照正确的格式解析时间,抹去后面的时间点,精确到日,只好自定义一个时间解析器。自定义的时间解析器很好实现,网上已经有很多实例代码,只需要继承 JsonDeserializer<T> 就可以。

问题的关键点在于,如何获取到注解上的时间格式,按照注解上的格式去解析,否则每个解析器的实现只能使用一种固定的格式去解析时间。

1. 所以第一步是获取注解上配置的信息

想要获取字段对应的注解信息,只有找到相应的字段,然后通过字段属性获取注解信息,再通过注解信息获取配置的格式。

但找了很久,也没有在既有的参数里找到获取相关字段的方法,只能去翻看源码,最后在这里发现了获取字段信息的方法以及解析器的生成过程,源代码如下:

Jackson反序列化@JsonFormat 不生效的解决方案

第一个红框表示解析器是在这里生成的,第二个红框就是获取注解信息的地方

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;
/**
* @author 无名小生 Date: 2019-02-19 Time: 19:00
* @version $Id$
*/
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;
/**
* @author 无名小生 Date: 2019-02-21 Time: 11:03
* @version $Id$
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
   ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface DeserializeFormat {
   /**
    * Value that indicates that default {@link java.util.Locale}
    * (from deserialization or serialization context) should be used:
    * annotation does not define value to use.
    */
   public final static String DEFAULT_LOCALE = "##default";
   /**
    * Value that indicates that default {@link java.util.TimeZone}
    * (from deserialization or serialization context) should be used:
    * annotation does not define value to use.
    */
   public final static String DEFAULT_TIMEZONE = "##default";
   /**
    * 按照特定的时间格式解析
    */
   public String pattern() default "";
   /**
    * 目标格式
    * @return
    */
   public String format() default "";
   /**
    * Structure to use for serialization: definition of mapping depends on datatype,
    * but usually has straight-forward counterpart in data format (JSON).
    * Note that commonly only a subset of shapes is available; and if 'invalid' value
    * is chosen, defaults are usually used.
    */
   public DeserializeFormat.Shape shape() default DeserializeFormat.Shape.ANY;
   /**
    * {@link java.util.Locale} to use for serialization (if needed).
    * Special value of {@link #DEFAULT_LOCALE}
    * can be used to mean "just use the default", where default is specified
    * by the serialization context, which in turn defaults to system
    * defaults ({@link java.util.Locale#getDefault()}) unless explicitly
    * set to another locale.
    */
   public String locale() default DEFAULT_LOCALE;
   /**
    * {@link java.util.TimeZone} to use for serialization (if needed).
    * Special value of {@link #DEFAULT_TIMEZONE}
    * can be used to mean "just use the default", where default is specified
    * by the serialization context, which in turn defaults to system
    * defaults ({@link java.util.TimeZone#getDefault()}) unless explicitly
    * set to another locale.
    */
   public String timezone() default DEFAULT_TIMEZONE;
   /*
   /**********************************************************
   /* Value enumeration(s), value class(es)
   /**********************************************************
    */
   /**
    * Value enumeration used for indicating preferred Shape; translates
    * loosely to JSON types, with some extra values to indicate less precise
    * choices (i.e. allowing one of multiple actual shapes)
    */
   public enum Shape
   {
       /**
        * Marker enum value that indicates "default" (or "whatever") choice; needed
        * since Annotations can not have null values for enums.
        */
       ANY,
       /**
        * Value that indicates shape should not be structural (that is, not
        * {@link #ARRAY} or {@link #OBJECT}, but can be any other shape.
        */
       SCALAR,
       /**
        * Value that indicates that (JSON) Array type should be used.
        */
       ARRAY,
       /**
        * Value that indicates that (JSON) Object type should be used.
        */
       OBJECT,
       /**
        * Value that indicates that a numeric (JSON) type should be used
        * (but does not specify whether integer or floating-point representation
        * should be used)
        */
       NUMBER,
       /**
        * Value that indicates that floating-point numeric type should be used
        */
       NUMBER_FLOAT,
       /**
        * Value that indicates that integer number type should be used
        * (and not {@link #NUMBER_FLOAT}).
        */
       NUMBER_INT,
       /**
        * Value that indicates that (JSON) String type should be used.
        */
       STRING,
       /**
        * Value that indicates that (JSON) boolean type
        * (true, false) should be used.
        */
       BOOLEAN
       ;
       public boolean isNumeric() {
           return (this == NUMBER) || (this == NUMBER_INT) || (this == NUMBER_FLOAT);
       }
       public boolean isStructured() {
           return (this == OBJECT) || (this == ARRAY);
       }
   }
   /**
    * Helper class used to contain information from a single {@link DeserializeFormat}
    * annotation.
    */
   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
           );
       }
       /**
        * @since 2.1
        */
       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;
       }
       /**
        * @since 2.4
        */
       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;
       }
       /**
        * @since 2.1
        */
       public DeserializeFormat.Value withPattern(String p,String f) {
           return new DeserializeFormat.Value(p, f, shape, locale, timezoneStr, _timezone);
       }
       /**
        * @since 2.1
        */
       public DeserializeFormat.Value withShape(DeserializeFormat.Shape s) {
           return new DeserializeFormat.Value(pattern, format, s, locale, timezoneStr, _timezone);
       }
       /**
        * @since 2.1
        */
       public DeserializeFormat.Value withLocale(Locale l) {
           return new DeserializeFormat.Value(pattern, format, shape, l, timezoneStr, _timezone);
       }
       /**
        * @since 2.1
        */
       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; }
       /**
        * Alternate access (compared to {@link #getTimeZone()}) which is useful
        * when caller just wants time zone id to convert, but not as JDK
        * provided {@link TimeZone}
        *
        * @since 2.4
        */
       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;
       }
       /**
        * @since 2.4
        */
       public boolean hasShape() { return shape != DeserializeFormat.Shape.ANY; }
       /**
        * @since 2.4
        */
       public boolean hasPattern() {
           return (pattern != null) && (pattern.length() > 0);
       }
       /**
        * @since 2.4
        */
       public boolean hasFormat() {
           return (format != null) && (format.length() > 0);
       }
       /**
        * @since 2.4
        */
       public boolean hasLocale() { return locale != null; }
       /**
        * @since 2.4
        */
       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;
/**
* @author 无名小生 Date: 2019-02-19 Time: 19:00
* @version $Id$
*/
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:时区

来源:https://blog.csdn.net/qq_26881739/article/details/87862506

标签:Jackson,反序列化,@JsonFormat
0
投稿

猜你喜欢

  • Java实现动态获取文件的绝对路径

    2022-12-19 00:34:14
  • ionic监听android返回键实现“再按一次退出”功能

    2021-12-24 10:22:57
  • Spring中的bean概念介绍

    2023-01-18 07:46:49
  • SpringBoot项目的配置文件中设置server.port不生效问题

    2022-11-13 06:01:26
  • C++异常处理 try,catch,throw,finally的用法

    2021-08-21 21:29:55
  • SuperSocket封装成C#类库的步骤

    2023-09-05 02:36:33
  • android Animation监听器AnimationListener的使用方法)

    2022-07-03 00:55:57
  • 利用C#实现可以继承的"枚举"

    2021-08-08 20:55:50
  • 深入java内存查看与分析详解

    2021-06-14 06:48:45
  • java poi导出图片到excel示例代码

    2023-10-30 00:13:17
  • 使用Feign传递请求头信息(Finchley版本)

    2023-06-07 22:38:07
  • Android Studio做超好玩的拼图游戏 附送详细注释源码

    2023-08-05 12:19:16
  • Java建造者设计模式详解

    2022-09-19 13:14:11
  • Android使用PhotoView实现图片双击放大单击退出效果

    2022-10-10 04:52:11
  • Java通过索引值实现约瑟夫环算法

    2021-09-28 22:33:48
  • Unity5.6大规模地形资源创建方法

    2022-07-12 20:59:55
  • Android四大组件之广播BroadcastReceiver详解

    2023-03-29 23:41:14
  • C# Random类随机函数实例详解

    2021-09-12 04:26:42
  • JAVA 字符串加密、密码加密实现方法

    2023-11-28 04:08:09
  • C语言文件操作大全

    2022-10-17 06:25:01
  • asp之家 软件编程 m.aspxhome.com