Jackson反序列化@JsonFormat 不生效的解決方案
今天在線上發(fā)現(xiàn)一個(gè)問(wèn)題,在使用Jackson進(jìn)行時(shí)間的反序列化時(shí),配置的 @JsonFormat 沒(méi)有生效
查看源碼發(fā)現(xiàn),Jackson在反序列化時(shí)間時(shí),會(huì)判斷json字段值類型,如下:

由于在我們服務(wù)里,前端傳時(shí)間值到后端時(shí)采用了時(shí)間戳的方式,json值被判斷為數(shù)字類型,所以Jackson在反序列化時(shí)直接簡(jiǎn)單粗暴的方式處理,將時(shí)間戳轉(zhuǎn)換為Date類型:

為了能夠按照正確的格式解析時(shí)間,抹去后面的時(shí)間點(diǎn),精確到日,只好自定義一個(gè)時(shí)間解析器。自定義的時(shí)間解析器很好實(shí)現(xiàn),網(wǎng)上已經(jīng)有很多實(shí)例代碼,只需要繼承 JsonDeserializer<T> 就可以。
問(wèn)題的關(guān)鍵點(diǎn)在于,如何獲取到注解上的時(shí)間格式,按照注解上的格式去解析,否則每個(gè)解析器的實(shí)現(xiàn)只能使用一種固定的格式去解析時(shí)間。
1. 所以第一步是獲取注解上配置的信息
想要獲取字段對(duì)應(yīng)的注解信息,只有找到相應(yīng)的字段,然后通過(guò)字段屬性獲取注解信息,再通過(guò)注解信息獲取配置的格式。
但找了很久,也沒(méi)有在既有的參數(shù)里找到獲取相關(guān)字段的方法,只能去翻看源碼,最后在這里發(fā)現(xiàn)了獲取字段信息的方法以及解析器的生成過(guò)程,源代碼如下:

第一個(gè)紅框表示解析器是在這里生成的,第二個(gè)紅框就是獲取注解信息的地方
2. 注解獲取以后便創(chuàng)建自定義的時(shí)間解析器
猜想,我們可不可以也實(shí)現(xiàn)這個(gè)類,重寫生成解析器的方法?那就試試唄~ 我們?cè)谧远x的時(shí)間解析器上同樣實(shí)現(xiàn)這個(gè)類,重寫了生成時(shí)間解析器的方法,并初始化一些自定義的信息供解析時(shí)間使用(當(dāng)然猜想是正確的,因?yàn)楣俜骄褪沁@么搞的,只是官方的是一個(gè)內(nèi)部類實(shí)現(xiàn)的),具體代碼如下:
時(shí)間解析器代碼:
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 無(wú)名小生 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("使用自定義解析器解析字段:{}:時(shí)間:{}",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反序列化,時(shí)間解析失敗", 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;
}
}
至此,自定義時(shí)間解析器就完成了
但是,為了能夠更靈活的控制時(shí)間的解析(例如:輸入的時(shí)間格式和目標(biāo)時(shí)間格式不同),我又重新自定義了一個(gè)時(shí)間解析的注解,基本仿照官方的 @Format 注解,具體代碼如下
自定義時(shí)間解析注解:
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 無(wú)名小生 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";
/**
* 按照特定的時(shí)間格式解析
*/
public String pattern() default "";
/**
* 目標(biāo)格式
* @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());
}
}
}
使用自定義解析注解的時(shí)間解析器
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 無(wú)名小生 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反序列化,時(shí)間解析失敗", 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的使用
實(shí)體類字段中添加@JsonFormat注解(),返回 yyyy-MM-dd HH:mm:ss 時(shí)間格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date joinedDate;
pattern:日期格式
timezone:時(shí)區(qū)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Cloud Gateway 使用JWT工具類做用戶登錄校驗(yàn)功能
這篇文章主要介紹了Spring Cloud Gateway 使用JWT工具類做用戶登錄校驗(yàn)的示例代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01
Mybatis不啟動(dòng)項(xiàng)目直接測(cè)試Mapper的實(shí)現(xiàn)方法
在項(xiàng)目開(kāi)發(fā)中,測(cè)試單個(gè)Mybatis Mapper方法通常需要啟動(dòng)整個(gè)SpringBoot項(xiàng)目,消耗大量時(shí)間,本文介紹通過(guò)Main方法和Mybatis配置類,快速測(cè)試Mapper功能,無(wú)需啟動(dòng)整個(gè)項(xiàng)目,這方法使用AnnotationConfigApplicationContext容器2024-09-09
Spring MVC+FastJson+hibernate-validator整合的完整實(shí)例教程
這篇文章主要給大家介紹了關(guān)于Spring MVC+FastJson+hibernate-validator整合的完整實(shí)例教程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-04-04
xxl-job如何濫用netty導(dǎo)致的問(wèn)題及解決方案
本篇文章講解xxl-job作為一款分布式任務(wù)調(diào)度系統(tǒng)是如何濫用netty的,導(dǎo)致了怎樣的后果以及如何修改源碼解決這些問(wèn)題,netty作為一種高性能的網(wǎng)絡(luò)編程框架,十分受大家喜愛(ài),今天就xxl-job濫用netty這一問(wèn)題給大家詳細(xì)下,感興趣的朋友一起看看吧2021-05-05
Java數(shù)據(jù)結(jié)構(gòu)之線段樹(shù)的原理與實(shí)現(xiàn)
線段樹(shù)是一種二叉搜索樹(shù),是用來(lái)維護(hù)區(qū)間信息的數(shù)據(jù)結(jié)構(gòu)。本文將利用示例詳細(xì)講講Java數(shù)據(jù)結(jié)構(gòu)中線段樹(shù)的原理與實(shí)現(xiàn),需要的可以參考一下2022-06-06
java數(shù)組復(fù)制的四種方法效率對(duì)比
這篇文章主要介紹了java數(shù)組復(fù)制的四種方法效率對(duì)比,文中有簡(jiǎn)單的代碼示例,以及效率的比較結(jié)果,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11

