这些Jackson用法你都会吗(上篇)

    JacksonJava语言中的一个JSON类库。性能良好,同时拥有高度可扩展的API等都是Jackson的亮点。参考下图(时间参考本文发布时间),可以看到在Jackson在主流JSON类库中的使用相当广泛(其中也有部分得益于SpringMVC将其作为默认的Json消息转换器)。接下来赶紧进入主题吧,看看本文主要介绍的Jackson用法你都会吗?

这些Jackson用法你都会吗(上篇)
不同JSON类库使用情况

时间相关配置

    Jackson的核心类是com.fasterxml.jackson.databind.ObjectMapper,基本所有的序列化(对象转JSON)/反序列化(JSON转对象)都是通过该类实现的。全文基于jackson 2.11.2版本进行介绍。

    关于时间这一块,通常我们需要将时间格式化(未特殊声明,全文用格式化指代序列化和反序列化)成符合我们要求的格式。Jackson默认会将java.util.Date格式化成13位毫秒级时间戳,并且对JSR310的格式化也是当作普通类处理(遍历getXxx方法,去掉get前缀,Xxx首字母小写作为属性名进行格式化)。

public class JacksonTest {

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Accessors(chain = true)
    static class DateVo implements Serializable {
        private static final long serialVersionUID = 1L;
        private Date date;
        private LocalDateTime localDateTime;
        private LocalDate localDate;
        private LocalTime localTime;
    }

    @Test
    @DisplayName("默认的Jackson时间序列化")
    void defaultTime() throws JsonProcessingException {
        String json = prettyOm.writeValueAsString(dateVo);
        System.out.println(json);
    }

}

    上述代码的运行结果如下,格式化结果非常不符合我们的一般要求:

{
  "date" : 1599910607807,
  "localDateTime" : {
    "month" : "SEPTEMBER",
    "year" : 2020,
    "dayOfMonth" : 12,
    "hour" : 19,
    "minute" : 36,
    "monthValue" : 9,
    "nano" : 813000000,
    "second" : 47,
    "dayOfWeek" : "SATURDAY",
    "dayOfYear" : 256,
    "chronology" : {
      "id" : "ISO",
      "calendarType" : "iso8601"
    }
  },
  "localDate" : {
    "year" : 2020,
    "month" : "SEPTEMBER",
    "dayOfMonth" : 12,
    "monthValue" : 9,
    "era" : "CE",
    "dayOfWeek" : "SATURDAY",
    "dayOfYear" : 256,
    "leapYear" : true,
    "chronology" : {
      "id" : "ISO",
      "calendarType" : "iso8601"
    }
  },
  "localTime" : {
    "hour" : 19,
    "minute" : 36,
    "second" : 47,
    "nano" : 814000000
  }
}

    针对java.util.Date类,可以通过@com.fasterxml.jackson.annotation.JsonFormat注解实现格式化,但是针对JSR310提供的时间类暂时没有注解可以让我们直接进行格式化。这时候需要我们注册相应的格式化模块。如下所示:

public class JacksonTest {
    private final SimpleModule jsr310Module;

    {
        // 自定义 JSR310 格式化模块,设置常用的 JSR310 类及其处理器
        DateTimeFormatter ldtFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        DateTimeFormatter ldFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        DateTimeFormatter ltFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        jsr310Module = new SimpleModule()
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(ldtFormatter))
                .addSerializer(LocalDate.class, new LocalDateSerializer(ldFormatter))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(ltFormatter))
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(ldtFormatter))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(ldFormatter))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(ltFormatter));
    }

    @Test
    @DisplayName("自定义时间格式化")
    void customDateFormat() throws Exception {
        // 直接使用 jackson-datatype-jsr310 提供的 JSR310 格式化实现,虽然非常全面,但是格式还是需要进一步调整。
        // JavaTimeModule javaTimeModule = new JavaTimeModule();
        ObjectMapper om = new ObjectMapper()
                // 设置地区为中国
                .setLocale(Locale.CHINA)
                // 设置时区为东8区
                .setTimeZone(TimeZone.getTimeZone(ZoneOffset.ofHours(8)))
                // 设置 Date 时间格式化格式为 yyyy-MM-dd HH:mm:ss
                .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA))
                // 关闭时间转时间戳
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                // 激活缩进换行配置(优雅输出)
                .enable(SerializationFeature.INDENT_OUTPUT)
                // 注册 JSR310 时间API的相关格式转换模块
                .registerModule(jsr310Module);

        System.out.println(om.writeValueAsString(dateVo));
    }
}

    注册JSR310的时间格式化模块一般有两种选择,一种是直接使用jackson-datatype-jsr310依赖提供的com.fasterxml.jackson.datatype.jsr310.JavaTimeModule模块器,该模块器覆盖了JSR310的大部分时间类,但是格式一般还需要我们进一步调整,相当繁琐;所以一般我们都是自己注册所需要的时间模块,如上代码,我们针对LocalDateTimeLocalDateLocalTime三个最常用的时间类进行格式化处理,最终注册到ObjectMapper中。

    同时,对于Date数据类型,我们一般也都是通过配置定义格式化,而不是通过注解方式定义格式化,因为使用注解需要标注在所有使用Date的类上,容易遗漏并且过于繁琐。配置时主要设置地区、时区、格式化定义并关闭默认的时间戳格式化。

优雅输出

    默认Jackson序列化结果是单行的,如果我们需要输出包含缩进换行的优雅JSON字符串,有以下两种方法可以实现我们的要求。

临时性

    临时性的输出优雅JSON字符串,可以直接通过com.fasterxml.jackson.databind.ObjectMapper.writerWithDefaultPrettyPrinter方法获取对应的ObjectWriter,然后通过ObjectWriter序列化,该方法只针对本次输出。(当然我们将ObjectWriter长期持有,该类的实例是线程安全的。后续通过ObjectWriter进行序列化也相当于永久性。)

永久性

    另一种是直接激活ObjectMapper的优雅输出配置,通过ObjectMapper.enable(SerializationFeature.INDENT_OUTPUT)或者ObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true)永久性激活优雅输出配置。

其他常见配置

    除了上述说的时间配置和优雅输出配置,还有以下几个配置我们通常会用到:

FAIL_ON_EMPTY_BEANS

    如果你的类中所有字段定义都没有提供getter方法,如下代码所示:

public class JacksonTest {
    static class EmptyBean implements Serializable {
        private static final long serialVersionUID = 1L;

        private int minutes;

        public int minutes() {
            return minutes;
        }

        public EmptyBean minutes(int minutes) {
            this.minutes = minutes;
            return this;
        }
    }

    @Test
    @DisplayName("允许空Bean")
    void failOnEmptyBeans() throws Exception {
        // Jackson 序列化规则默认从 getXxx 中删除 get 前缀,然后将首字母小写
        EmptyBean eb = new EmptyBean().minutes(1);
        System.out.println(prettyOm.writeValueAsString(eb));
        // System.out.println(prettyOm.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS).writeValueAsString(eb));
    }
}

    那么这时候在序列化时将会抛出com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.xxx.Cls and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)。异常信息里也提醒了我们可以如何处理该问题。

    这时候通过ObjectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)或者ObjectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)可以关闭该功能。这样就能正常序列化成{}(一个空JSON对象)。

JsonInclude.Include.NON_NULL

    如果某个对象字段值为null,默认Jackson也会将null字段也序列化进JSON中,而我们通常为了节约带宽考虑,都是需要不序列化null字段的,此时可以通过ObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)关闭null值的序列化。

FAIL_ON_NULL_FOR_PRIMITIVES

    如果JSON的某个字段值为null,并且该字段将映射到实体的基本数据类型字段,那么如果开启了DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES(默认是关闭状态),将会抛出com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot map null into type int (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)异常。

FAIL_ON_UNKNOWN_PROPERTIES

    JSON的字段比类的字段多时,Jackson在反序列化时将会直接抛出无法识别的属性异常com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "nickname" (class com.swj.mj.jackson.JacksonTest$UnknownPropertiesBean), not marked as ignorable (one known property: "name"])

public class JacksonTest {
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Accessors(chain = true)
    static class UnknownPropertiesBean implements Serializable {

        private static final long serialVersionUID = 1L;
        private String name;
    }

    @Test
    void failOnUnknownProperties() throws Exception {
        String json = "{"name": "Reka","nickname": null}";
        // om.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        UnknownPropertiesBean bean = om.readValue(json, UnknownPropertiesBean.class);
        assert "Reka".equals(bean.getName());
    }
}

    这时候需要通过ObjectMapper.disable(SerializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)或者ObjectMapper.configure(SerializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)关闭该功能。该功能一般需要关闭,因为对接外部接口时,我们只需要用到其中一部分字段或者对方接口字段增加,如果此时没关闭该功能将会导致功能出异常。

ALLOW_SINGLE_QUOTES

    有时候会遇到一些非标准JSON字符串,比如字符串使用了单引号而不是双引号,这时候默认Jackson会抛出异常com.fasterxml.jackson.core.JsonParseException: Unexpected character (''' (code 39)): was expecting double-quote to start field name at [Source: (String)"{'name': 'Reka','nickname': null}"; line: 1, column: 3]。这时候可以通过Object.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES)或者ObjectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)允许单引号。

一些常见的注解

    除了@JsonFormat注解外,Jackson还提供了以下注解供我们使用。

这些Jackson用法你都会吗(上篇)
Jackson提供的注解

    但我们一般比较常用的注解有:@JsonProperty@JsonAlias@JsonFormat@JsonIgnore@JsonPropertyOrder。下面分别简单介绍一个各个注解的功能:

@JsonProperty

    该注解主要是重新定义字段在格式化时的名称,通常标注在字段或者getter方法上。基本用法如下:

public class JsonPropertyBean implements Serializable {
    private static final long serialVersionUID = 1L;

    @JsonProperty("nickName")
    private String nickname;
    private String name;

    @JsonProperty("username")
    public String getName() {
        return this.name;
    }

    public String getNickname() {
        return nickname;
    }
}

@JsonAlias

    该注解用于定义字段的反序列化别名,即将一个或者多个不同的别名映射到字段上,从Jackson 2.9开始提供。基本用法如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class JsonAliasBean implements Serializable {
   private static final long serialVersionUID = 1L;

   @JsonAlias("nickName")
   private String nickname;
   @JsonAlias({"username""Name"})
   private String name;
}

    这样在反序列化的时候就可以将nickName或者nickname映射到nickname字段上,将usernameName或者name映射到name字段上。

@JsonFormat

    该注解常用于定义java.util.Date的格式化,主要通过locale定义地区,timezone定义时区,pattern定义格式。基本用法如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class JsonFormatBean implements Serializable {

    private static final long serialVersionUID = 1L;

    @JsonFormat(locale = "zh_CN", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date date;
}

@JsonIgnore

    该注解标注的字段将不参与格式化(序列化和反序列化),与transient起相同效果。

@JsonPropertyOrder

    该注解常标注在类上,用于控制序列化时的字段顺序;有两种排序定义:一种是直接指定字段名的顺序(通过value字符串数组定义顺序),另一种是根据字段的字母序排序(通过alphabetic = true激活字母序)。基本用法如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@JsonPropertyOrder(alphabetic = true)
public class JsonPropertyOrderBean implements Serializable {

    private static final long serialVersionUID = 1L;

    private String username;
    private String nickname;
    private Date createTime;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@JsonPropertyOrder({"createTime""nickname""username"})
public class JsonPropertyOrderBean implements Serializable {

    private static final long serialVersionUID = 1L;

    private String username;
    private String nickname;
    private Date createTime;
}

自定义序列化器和反序列化器

    我们先前介绍了关于JSR310时间类的格式化需要我们通过注册模块的方式进行配置,这其中就涉及到了自定义序列化器和反序列化器的功能。自定义可以让我们高度自由地处理序列化和反序列化规则,一般自定义序列化器通过继承com.fasterxml.jackson.databind.ser.std.StdSerializer实现,自定义反序列化器通过继承com.fasterxml.jackson.databind.deser.std.StdDeserializer实现:

public class ZonedDateTimeSerializer extends StdSerializer<ZonedDateTime{

    private static final long serialVersionUID = 1L;
    private final DateTimeFormatter formatter;

    public ZonedDateTimeSerializer(DateTimeFormatter formatter) {
        super(ZonedDateTime.class);
        this.formatter = formatter;
    }

    @Override
    public void serialize(ZonedDateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        if (Objects.isNull(value)) {
            gen.writeNull();
        } else {
            gen.writeString(formatter.format(value));
        }
    }

}

public class ZonedDateTimeDeserializer extends StdDeserializer<ZonedDateTime{

    private static final long serialVersionUID = 1L;
    private final DateTimeFormatter formatter;

    public ZonedDateTimeDeserializer(DateTimeFormatter formatter) {
        super(ZonedDateTime.class);
        this.formatter = formatter;
    }

    @Override
    public ZonedDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        String value = p.getValueAsString();
        if (Objects.isNull(value) || value.isEmpty()) {
            return null;
        }
        return this.formatter.parse(p.getValueAsString(), ZonedDateTime::from);
    }
}

public class JacksonTest {
    @Test
    void customModule() throws Exception {
        ObjectMapper om = new ObjectMapper();
        DateTimeFormatter zdtFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss ZZZ");
        om.enable(SerializationFeature.INDENT_OUTPUT)
                .registerModule(
                        new SimpleModule()
                                .addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer(zdtFormatter))
                                .addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer(zdtFormatter))
                );
        ZonedDateTimeBean bean = new ZonedDateTimeBean();
        System.out.println(om.writeValueAsString(bean));
        String json = "{}";
        assert Objects.isNull(om.readValue(json, ZonedDateTimeBean.class).getDateTime());

        bean.setDateTime(ZonedDateTime.now());
        System.out.println(om.writeValueAsString(bean));
        json = "{"dateTime" : "2020-09-12 21:25:01 +0800"}";
        assert om.readValue(json, ZonedDateTimeBean.class).getDateTime().equals(ZonedDateTime.of(
                LocalDateTime.of(202091221251), ZoneOffset.ofHours(8)));
    }
}

    如上,我们定义了ZonedDateTime的格式化器,然后添加到SimpleModule模块中,并注册到ObjectMaper中即可使用。

结束语

    本篇介绍到的Jackson用法不知你是否都熟练掌握了呢?限于篇幅原因,这里我们介绍的都是比较基础的配置与用法,下篇我们将会介绍Jackson的泛型反序列化、自定义Jackson格式化注解、与Spring集成、与redis集成等的用法及其注意点。敬请期待!


原文始发于微信公众号(三维家技术实践):这些Jackson用法你都会吗(上篇)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30568.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!