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

今天在线上发现一个问题,在使用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;
/**
 * @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:时区

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 解决SpringMVC 返回Java8 时间JSON数据的格式化问题处理

    有时在Spring MVC中返回JSON格式的response的时候会使用@ResponseBody注解,不过在处理java8中时间的时候会很麻烦,一般我们使用的HTTPMessageConverter是MappingJackson2HttpMessageConverter,它默认返回的时间格式是这种: "startDate" : { "year" : 2010, "month" : "JANUARY", "dayO

  • 关于fastjson的@JSONField注解的一些问题(详解)

    @JSONField 看源码它可以作用于字段和方法上. 引用网上说的, 一.作用Field @JSONField作用在Field时,其name不仅定义了输入key的名称,同时也定义了输出的名称. 但是我在使用中,发现并不如上所说. 例如 @JSONField(name="project_id") private Long ProjectID 发现bean 转json的时候并是"project_id":xxx的形式,json转bean的时候也不会把"proj

  • springboot json时间格式化处理的方法

    application.properties中加入如下代码 springboot 默认使用 jackson 解析 json spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 如果个别实体需要使用其他格式的 pattern,在实体上加入注解即可 import org.springframework.format.annotation.DateTimeFormat; import com.fas

  • Shell脚本自动更新hosts实现免翻墙访问google

    上次给大家发了一个python更新google hosts的脚本,今天看到有人发出了一句用shell来获取google hosts的脚本,我就拿来稍微简单加工了下,下面给大家shell版的更新google hosts的脚本. 脚本内容: 复制代码 代码如下: cat google_update.sh #!/bin/bash data=`date +%y%m%d%H%M` curl http://www.360kb.com/kb/2_122.html 2>/dev/null | sed -n '/

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

    今天在线上发现一个问题,在使用Jackson进行时间的反序列化时,配置的 @JsonFormat 没有生效 查看源码发现,Jackson在反序列化时间时,会判断json字段值类型,如下: 由于在我们服务里,前端传时间值到后端时采用了时间戳的方式,json值被判断为数字类型,所以Jackson在反序列化时直接简单粗暴的方式处理,将时间戳转换为Date类型: 为了能够按照正确的格式解析时间,抹去后面的时间点,精确到日,只好自定义一个时间解析器.自定义的时间解析器很好实现,网上已经有很多实例代码,只需

  • 使用Jackson反序列化遇到的问题及解决

    Jackson反序列化遇到的问题 最近在项目中需要使用Jackson把前台转来的字符转为对象,转换过程中发生了错误,报错如下 ​com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not construct instance of java.util.Date from String value '2018-09-14 15:12:08': not a valid representation (error: Failed

  • 解决Jackson反序列化map,set等复杂类型问题

    工作中遇到了这个问题,我简单的用代码复现一下,就是一个map,value又为一个set,导致反序列化报错 @Test public void test10() throws Exception { //准备一下数据 Map<String, Set<String>> map = new HashMap<>(); map.put("aaa",new HashSet<String>(){{add("111");add(&qu

  • maven依赖版本没有按照最短路径原则生效的解决方案

    女朋友他们项目用了 spring-boot,以 spring-boot-parent 作为 parent: <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.9</version> </parent> 女朋友最近想用 elast

  • 关于keep-alive路由多级嵌套不生效的解决方案

    目录 问题 定位问题 解决方案 额外的问题及题解 问题 发现了一个问题,多级嵌套的路由缓存页面没有生效.网上其实有一些是把路由拍平,然后再去处理菜单,但是我觉得还不如从根源上去解决这个问题.顺带了解一下keep-alive的工作原理. 实际就是根据组件的名称,如果即将渲染的组件名称命中存在缓存数组,那么直接取缓存里的组件进行渲染.同时,这个命中缓存是带有父子组件关联关系的 以下会根据本图做讲解 定位问题 keep-alive实际就是根据组件的名称,如果即将渲染的组件名称命中存在缓存数组,那么直接

  • jackson反序列化时如何忽略不需要的字段

    目录 jackson反序列化忽略字段 解决办法很简单 jackson忽略指定属性 例子 jackson反序列化忽略字段 JSON字符串中含有我们并不需要的字段,那么当对应的实体类中不含有该字段时,会抛出一个异常,告诉你有些字段没有在实体类中找到. 解决办法很简单 在声明ObjectMapper之后,加上上述代码: objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); jackson忽略指

  • MyBatis中PageHelper不生效的解决方案

    MyBatis中PageHelper不生效 今天使用pageHelper,发现设置了PageHelper.startPage(page, pageSize);pageSize设置为10,但是结果并没有分页,查处了全部的数据: 问题解决: 原因是mybatis的依赖版本问题,之前配置的是1.0.0版本,这个版本不支持分页拦截 <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>

  • docker swarm外部验证负载均衡时不生效的解决方案

    问题描述 我在本地创建了3个装了centos7的虚拟机, 并初始化了swarm集群, 即1个manager节点, 2个worker节点; 三台机子的ip分别是 192.168.124.8 - (manager节点), 192.168.124.9 - (worker节点), 192.168.124.10 - (worker节点) [root@localhost ~]# docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENG

  • Jackson 反序列化时实现大小写不敏感设置

    常用配置 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(Feature.IGNORE_UNKNOWN,true); objectMapper.configure(Feature.WRITE_BIGDECIMAL_AS_PLAIN,true); objectMapper.configure(JsonParser.Feature.ALLOW_MISSING_VALUES,true); objectMapp

  • SpringMVC @NotNull校验不生效的解决方案

    目录 SpringMVC @NotNull校验不生效 加了两个依赖问题解决 @NotNull注解失效原因之一 Lombok判断注解怎么使用,总结下来为 SpringMVC @NotNull校验不生效 是不是少包了. @NotEmpty也找不到. 加了两个依赖问题解决 <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> &l

随机推荐