1.前言

通常我们在使用Spring Boot框架时,如果没有特别指定接口的序列化类型,则会使用Spring Boot框架默认集成的Jackson框架进行处理,通过Jackson框架将服务端响应的数据序列化成JSON格式的数据。

本文主要针对在Spring Boot框架中使用Jackson进行处理的经验进行总结,同时也结合在实际开发场景中碰到的问题以及解决方案进行陈述。

本文涉及到的源码地址:https://gitee.com/dt_research_institute/code-in-action

PS:目前市面上针对JSON序列化的框架很多,比较出名的就是JacksonGsonFastJson。如果开发者对序列化框架没有特别的要求的情况下,个人建议是直接使用Spring Boot框架默认集成的Jackson,没有必要进行更换。

2.统一序列化时间格式

在我们的接口中,针对时间类型的字段序列化是最常见的需求之一,一般前后端开发人员会针对时间字段统一进行约束,这样有助于在编码开发时,统一编码规范。

在Spring Boot框架中,如果使用Jackson处理框架,并且没有任何配置的情况下,Jackson针对不同时间类型字段,序列化的格式也会不尽相同。

先来看一个简单示例,User.java实体类编码如下:

public class User {

    private String name;

    private Integer age;

    private LocalDateTime birthday;

    private Date studyDate;

    private LocalDate workDate;

    private Calendar firstWorkDate;

    public static User buildOne(){
User user=new User();
LocalDateTime now=LocalDateTime.now();
user.setWorkDate(now.plusYears(25).toLocalDate());
user.setStudyDate(Date.from(now.plusYears(5).atZone(ZoneId.systemDefault()).toInstant()));
user.setName("姓名-"+RandomUtil.randomString(5));
user.setAge(RandomUtil.randomInt(0,100));
user.setBirthday(now);
user.setFirstWorkDate(Calendar.getInstance());
return user;
} //getter and setter...
}

接口代码层也很简单,返回一个User的实体对象即可,代码如下:

@RestController
public class UserApplication { @GetMapping("/queryOne")
public ResponseEntity<User> queryOne(){
return ResponseEntity.ok(User.buildOne());
}
}

如果我们对框架代码没有任何的配置,此时我们通过调用接口/queryOne,拿到的返回结果数据如下图:

Jackson序列化框架针对四个不同的时间类型字段,序列化处理的操作是不同的,如果我们对时间字段有格式化的要求时,我们应该如何处理呢?

2.1 通过@JsonFormat注解

最直接也是最简单的一种方式,是我们通过使用Jackson提供的@JsonFormat注解,对需要格式化处理的时间字段进行标注,在@JsonFormat注解中写上我们的时间格式化字符,User.java代码如下:

public class User {

    private String name;

    private Integer age;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime birthday; private Date studyDate; private LocalDate workDate; private Calendar firstWorkDate;
//getter and setter...
}

此时,我们再通过调用接口,拿到的返回结果如下图:

通过对birthday字段标注@JsonFormat注解,最终Jackson框架会将该字段序列化为我们标注的格式类型。

2.2 配置全局application.yml

通过@JsonFormat注解的方式虽然能解决问题,但是我们在实际的开发当中,涉及到的时间字段会非常多,如果全部都用注解的方式对项目中的时间字段进行标注,那开发的工作量也会很大,并且多团队一起协同编码时,难免会存在遗漏的情况,因此,@JsonFormat注解只适用于针对特定的接口,特定的场景下,对序列化响应的时间字段进行约束,而在全局的角度来看,开发者应该考虑通过在application.yml配置文件中进行全局配置

针对Spring Boot框架中Jackson的全局配置,我们在application.yml进行配置时,IDEA等编辑器会给出相应的提示,包含的属性如下图:

开发者可以通过org.springframework.boot.autoconfigure.jackson.JacksonProperties.java查看所有配置的源码信息

配置属性 说明
date-format 日期字段格式化,例如:yyyy-MM-dd HH:mm:ss

针对日期字段的格式化处理,我们只需要使用date-format属性进行配置即可,application.yml配置如下:

spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss

当然,如果有必要的话,还需要配置time-zone时区属性,不过该属性不配置的情况下,Jackson会使用系统默认时区。

我们从Spring Boot的源码中可以看到对Jackson的时间处理逻辑,JacksonAutoConfiguration.java中部分代码如下:

private void configureDateFormat(Jackson2ObjectMapperBuilder builder) {
// We support a fully qualified class name extending DateFormat or a date
// pattern string value
String dateFormat = this.jacksonProperties.getDateFormat();
if (dateFormat != null) {
try {
Class<?> dateFormatClass = ClassUtils.forName(dateFormat, null);
builder.dateFormat((DateFormat) BeanUtils.instantiateClass(dateFormatClass));
}
catch (ClassNotFoundException ex) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
// Since Jackson 2.6.3 we always need to set a TimeZone (see
// gh-4170). If none in our properties fallback to the Jackson's
// default
TimeZone timeZone = this.jacksonProperties.getTimeZone();
if (timeZone == null) {
timeZone = new ObjectMapper().getSerializationConfig().getTimeZone();
}
simpleDateFormat.setTimeZone(timeZone);
builder.dateFormat(simpleDateFormat);
}
}
}

从上面的代码中,我们可以看到的处理逻辑:

  • 从yml配置文件中拿到dateFormat属性字段
  • 首先通过ClassUtils.forName方法来判断开发者配置的是否是格式化类,如果配置的是格式化类,则直接配置dateFormat属性
  • 类找不到的情况下,捕获ClassNotFoundException异常,默认使用JDK自带的SimpleDateFormat类进行初始化

最终,我们在application.yml配置文件中配置了全局的Jackson针对日期处理的格式化信息,此时我们再看/queryOne接口响应的内容是什么情况呢?如下图:

从图中我们可以发现,除了LocalDate类型的字段,包含时分秒类型的日期类型:LocalDateTimeDateCalendar全部按照我们的要求将日期序列化成了yyyy-MM-dd HH:mm:ss格式,达到了我们的要求。

3.Jackson在Spring Boot框架中的配置选项

在上面的时间字段序列化处理,我们已经知道了如何配置,那么在Spring Boot的框架中,针对Jackson的各个配置项主要包含哪些呢?我们通过IDEA的提示可以看到,配置如下图:

在上面的12个属性中,每个属性的配置都会对Jackson产生不同的效果,接下来,我们逐一详解每个属性配置的作用

3.1 date-format日期格式化

date-format在前面我们已经知道了该属性的作用,主要是针对日期字段的格式化

3.2 time-zone时区

time-zone字段也是和日期字段类型,使用不同的时区,最终日期类型字段响应的结果会不一样

时区的表示方法有两种:

  • 指定时区的名称,例如:Asia/Shanghai,America/Los_Angeles
  • 通过格林威治平时GMT针对时分秒做+或者-自定义操作

通过指定时区的名称,假设我们指定当前的项目是America/Los_Angeles,那么接口响应的数据是什么效果呢?

PS:时区名称如果不是很清楚的话,一般在Linux服务器的/usr/share/zoneinfo目录可以进行查看,如下图:

application.yml:

spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: America/Los_Angeles

效果图如下:

我们在结合代码来分析:

//User.java
public static User buildOne(){
User user=new User();
LocalDateTime now=LocalDateTime.now();
user.setWorkDate(now.plusYears(25).toLocalDate());
user.setStudyDate(Date.from(now.plusYears(5).atZone(ZoneId.systemDefault()).toInstant()));
user.setName("姓名-"+RandomUtil.randomString(5));
user.setAge(RandomUtil.randomInt(0,100));
user.setBirthday(now);
user.setFirstWorkDate(Calendar.getInstance());
return user;
}

由于洛杉矶时区与上海时区相差16个小时,因此,Jackson框架针对日期的序列化时,分别做了不同类型的处理,但我们也能看出差别

  • LocalDateTimeLocalDate类型的字段,Jackson的时区设置不会对该字段产生影响(因为这两个日期类型自带时区属性)
  • DateCalendar类型的字段受Jackson序列化框架的时区设置影响

另外一种方式是通过格林威治平时(GMT)做加减法,主要有两种格式支持:

  • GMT+HHMM或者GMT-HHMM或者GMT+H:其中HH代表的是小时数,MM代表的是分钟数,取值范围是0-9,例如我们常见的GMT+8代表东八区,也就是北京时间
  • GMT+HH:MM或者GMT-HH:MM:其中HH代表的是小时数,MM代表的是分钟数,取值范围是0-9,和上面意思差不多

可以自己写测试代码进行测试,示例如下:

public class TimeTest {
public static void main(String[] args) {
LocalDateTime localDateTime=LocalDateTime.now();
DateTimeFormatter dateTimeFormatter=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(localDateTime.format(dateTimeFormatter));
System.out.println(LocalDateTime.now(ZoneId.of("GMT+0901")).format(dateTimeFormatter));
System.out.println(LocalDateTime.now(ZoneId.of("GMT+09:01")).format(dateTimeFormatter));
}
}

3.3 locale本地化

JSON序列化时Locale的变量设置

3.4 visibility访问级别

Jackson支持从私有字段中读取值,但是默认情况下不这样做,如果我们的项目中存在不同的序列化反序列化需求,那么我们可以在配置文件中对visibility进行配置

我们将上面User.java代码中的name属性的get方法修饰符从public变更为private,其他字段保持不变

代码如下:

public class User {

    private String name;

    private Integer age;
private Date nowDate; private LocalDateTime birthday; private Date studyDate; private LocalDate workDate; private Calendar firstWorkDate; //getter方法修饰符从public修改为private
private String getName() {
return name;
}
//other setter and getter
}

此时,我们通过调用/queryOne接口响应结果如下:

从结果中我们可以看到,由于我们将name属性的getter方法设置为了private,因此jackson在序列化时,没有拿到该字段

此时,我们再修改application.yml的配置,如下:

spring:
jackson:
visibility:
getter: any

我们通过将getter设置为any级别的类型,再调用/queryOne接口,响应结果如下:

从图中可以看出,jackson序列化结果中又出现了name属性,这代表即使name字段的属性和getter方法都是private,但是jackson还是获取到了该成员变量的值,并且进行了序列化处理。

通过设置visibility属性即可达到上面的效果。开发者根据自己的需要自行进行选择。

3.5 property-naming-strategy属性命名策略

通常比较常见的我们针对java代码中的实体类属性一般都是驼峰命名法(Camel-Case),但是Jackson序列化框架也提供了更多的序列化策略,而property-naming-strategy就是配置该属性的。

先来看Spring Boot框架如何配置jackson的命名策略

JacksonAutoConfiguration.java

private void configurePropertyNamingStrategyField(Jackson2ObjectMapperBuilder builder, String fieldName) {
// Find the field (this way we automatically support new constants
// that may be added by Jackson in the future)
Field field = ReflectionUtils.findField(PropertyNamingStrategy.class, fieldName,
PropertyNamingStrategy.class);
Assert.notNull(field, () -> "Constant named '" + fieldName + "' not found on "
+ PropertyNamingStrategy.class.getName());
try {
builder.propertyNamingStrategy((PropertyNamingStrategy) field.get(null));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}

通过反射,直接获取PropertyNamingStrategy类中的成员变量的值

PropertyNamingStrategy定义了Jackson(2.11.4)框架中的命名策略常量成员变量

package com.fasterxml.jackson.databind;

//other import

public class PropertyNamingStrategy // NOTE: was abstract until 2.7
implements java.io.Serializable
{
/**
* Naming convention used in languages like C, where words are in lower-case
* letters, separated by underscores.
* See {@link SnakeCaseStrategy} for details.
*
* @since 2.7 (was formerly called {@link #CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES})
*/
public static final PropertyNamingStrategy SNAKE_CASE = new SnakeCaseStrategy(); /**
* Naming convention used in languages like Pascal, where words are capitalized
* and no separator is used between words.
* See {@link PascalCaseStrategy} for details.
*
* @since 2.7 (was formerly called {@link #PASCAL_CASE_TO_CAMEL_CASE})
*/
public static final PropertyNamingStrategy UPPER_CAMEL_CASE = new UpperCamelCaseStrategy(); /**
* Naming convention used in Java, where words other than first are capitalized
* and no separator is used between words. Since this is the native Java naming convention,
* naming strategy will not do any transformation between names in data (JSON) and
* POJOS.
*
* @since 2.7 (was formerly called {@link #PASCAL_CASE_TO_CAMEL_CASE})
*/
public static final PropertyNamingStrategy LOWER_CAMEL_CASE = new PropertyNamingStrategy(); /**
* Naming convention in which all words of the logical name are in lower case, and
* no separator is used between words.
* See {@link LowerCaseStrategy} for details.
*
* @since 2.4
*/
public static final PropertyNamingStrategy LOWER_CASE = new LowerCaseStrategy(); /**
* Naming convention used in languages like Lisp, where words are in lower-case
* letters, separated by hyphens.
* See {@link KebabCaseStrategy} for details.
*
* @since 2.7
*/
public static final PropertyNamingStrategy KEBAB_CASE = new KebabCaseStrategy(); /**
* Naming convention widely used as configuration properties name, where words are in
* lower-case letters, separated by dots.
* See {@link LowerDotCaseStrategy} for details.
*
* @since 2.10
*/
public static final PropertyNamingStrategy LOWER_DOT_CASE = new LowerDotCaseStrategy(); //others...
}

从源码中我们可以看到,有六种策略供我们进行配置,配置示例如下:

spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
locale: zh_CN
time-zone: GMT+8
visibility:
getter: any
property-naming-strategy: LOWER_CAMEL_CASE

SNAKE_CASE

SNAKE_CASE主要包含的规则,详见SnakeCaseStrategy

  • java属性名称中所有大写的字符都会转换为两个字符,下划线和该字符的小写形式,例如userName会转换为user_name,对于连续性的大写字符,近第一个进行下划线转换,后面的大小字符则是小写,例如theWWW会转换为the_www
  • 对于首字母大写的情况,近转成小写,例如:Results会转换为results,并不会转换为_results
  • 针对属性中已经包含下划线的情况,仅做小写转换处理
  • 下划线出现在首位的情况下,会被去除处理,例如属性名:_user会被转换为user

真实效果如下图:

UPPER_CAMEL_CASE

UPPER_CAMEL_CASE顾名思义,驼峰命名法的规则,只是首字母会转换为大写,详见UpperCamelCaseStrategy

真实效果图如下:

LOWER_CAMEL_CASE

LOWER_CAMEL_CASE效果和UPPER_CAMEL_CASE正好相反,其首字母会变成小写,详见LowerCamelCaseStrategy

效果图如下:

LOWER_CASE

LOWER_CASE从命名来看很明显,将属性名 全部转为小写,详见LowerCaseStrategy

KEBAB_CASE

KEBAB_CASE策略和SNAKE_CASE规则类似,只是下划线变成了横线-,详见KebabCaseStrategy

效果图如下:

LOWER_DOT_CASE

LOWER_DOT_CASE策略和KEBAB_CASE规则相似,只是由横线变成了点.,详见LowerDotCaseStrategy

效果图如下:

总结:看了上面这么多属性名称的策略,其实每一种类型只是不同的场景下才需要,如果上面jackson给定的默认策略名称无法满足,我们从源码中也能看到,通过自定义实现类,也能满足企业的个性化需求,非常方便。

3.6 mapper通用功能开关配置

mapper属性是一个Map类型,主要是针对MapperFeature定义开关属性,是否启用这些特性

/**
* Jackson general purpose on/off features.
*/
private final Map<MapperFeature, Boolean> mapper = new EnumMap<>(MapperFeature.class);

MapperFeature.java中,我们可以跟踪源码来看:

/**
* Enumeration that defines simple on/off features to set
* for {@link ObjectMapper}, and accessible (but not changeable)
* via {@link ObjectReader} and {@link ObjectWriter} (as well as
* through various convenience methods through context objects).
*<p>
* Note that in addition to being only mutable via {@link ObjectMapper},
* changes only take effect when done <b>before any serialization or
* deserialization</b> calls -- that is, caller must follow
* "configure-then-use" pattern.
*/
public enum MapperFeature implements ConfigFeature
{
//.......
}

MapperFeature是一个枚举类型,对当前jackson的一些特性通过枚举变量的方式来定义开关属性,也是方便使用者来使用的。

主要包含以下枚举变量:

  • USE_ANNOTATIONS:
  • USE_GETTERS_AS_SETTERS
  • PROPAGATE_TRANSIENT_MARKER
  • AUTO_DETECT_CREATORS
  • AUTO_DETECT_FIELDS
  • AUTO_DETECT_GETTERS
  • AUTO_DETECT_IS_GETTERS
  • AUTO_DETECT_SETTERS
  • REQUIRE_SETTERS_FOR_GETTERS
  • ALLOW_FINAL_FIELDS_AS_MUTATORS
  • INFER_PROPERTY_MUTATORS
  • INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES
  • CAN_OVERRIDE_ACCESS_MODIFIERS
  • OVERRIDE_PUBLIC_ACCESS_MODIFIERS
  • USE_STATIC_TYPING
  • USE_BASE_TYPE_AS_DEFAULT_IMPL
  • DEFAULT_VIEW_INCLUSION
  • SORT_PROPERTIES_ALPHABETICALLY
  • ACCEPT_CASE_INSENSITIVE_PROPERTIES
  • ACCEPT_CASE_INSENSITIVE_ENUMS
  • ACCEPT_CASE_INSENSITIVE_VALUES
  • USE_WRAPPER_NAME_AS_PROPERTY_NAME
  • USE_STD_BEAN_NAMING
  • ALLOW_EXPLICIT_PROPERTY_RENAMING
  • ALLOW_COERCION_OF_SCALARS
  • IGNORE_DUPLICATE_MODULE_REGISTRATIONS
  • IGNORE_MERGE_FOR_UNMERGEABLE
  • BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES

3.7 serialization序列化特性开关配置

serialization属性同mapper类似,也是一个Map类型的属性

/**
* Jackson on/off features that affect the way Java objects are serialized.
*/
private final Map<SerializationFeature, Boolean> serialization = new EnumMap<>(SerializationFeature.class);

3.8 deserialization反序列化开关配置

deserialization反序列化配置

/**
* Jackson on/off features that affect the way Java objects are deserialized.
*/
private final Map<DeserializationFeature, Boolean> deserialization = new EnumMap<>(DeserializationFeature.class);

3.9 parser配置

3.10 generator配置

3.11 defaultPropertyInclusion序列化包含的属性配置

该属性是一个枚举配置,主要包含:

  • ALWAYS:顾名思义,始终包含,和属性的值无关
  • NON_NULL:值非空的属性才会包含属性
  • NON_ABSENT:值非空的属性,或者Optional类型的属性非空
  • NON_EMPTY: 空值的属性不包含
  • NON_DEFAULT:不使用jackson的默认规则对该字段进行序列化,详见示例
  • CUSTOM:自定义规则
  • USE_DEFAULTS:配置使用该规则的属性字段,将会优先使用class上的注解规则,否则会使用全局的序列化规则,详见示例

CUSTOM自定义规则是需要开发者在属性字段上使用@JsonInclude注解,并且指定valueFilter属性,该属性需要传递一个Class,示例如下:

//User.java
//指定value级别是CUSTOM
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = StringFilter.class)
private String name;

StringFilter则是判断非空的依据,该依据由开发者自己定义,返回true将会被排除,false则不会排除,示例如下:

//自定义非空判断规则
public class StringFilter {
@Override
public boolean equals(Object other) {
if (other == null) {
// Filter null's.
return true;
} // Filter "custom_string".
return "custom_string".equals(other);
}
}

4.Spring Boot针对Jackson的约定配置做的事情

在前面的文章中,我们已经详细的了解了Jackson在Spring Boot框架中的各个配置项,那么Spring Boot针对Jackson框架在约定配置时会做哪些事情呢?

在Spring Boot的spring-boot-autoconfigure-x.x.jar包中,我们可以看到Spring Boot框架针对jackson的处理源码,如下图:

主要包含三个类:

  • JacksonProperties:Spring Boot框架提供jackson的配置属性类,即开发者在application.yml配置文件中的配置项属性
  • JacksonAutoConfiguration:Jackson的默认注入配置类
  • Jackson2ObjectMapperBuilderCustomizer:自定义用于注入jackson的配置辅助接口

核心类是JacksonAutoConfiguration.java,该类是Spring Boot框架将Jackson相关实体Bean注入Spring容器的关键配置类。其主要作用:

  • 注入Jackson的ObjectMapper实体Bean到Spring容器中
  • 注入ParameterNamesModule实体Bean到Spring容器中
  • 注入Jackson2ObjectMapperBuilder实体Bean
  • 注入JsonComponentModule实体Bean
  • 注入StandardJackson2ObjectMapperBuilderCustomizer实体Bean,该类是上面Jackson2ObjectMapperBuilderCustomizer的实现类,主要用于接收JacksonProperties属性,将Jackson的外部配置属性接收,然后最终执行customize方法,构建ObjectMapper所需要的Jackson2ObjectMapperBuilder属性,最终为ObjectMapper属性赋值准备

源码如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration { private static final Map<?, Boolean> FEATURE_DEFAULTS; static {
Map<Object, Boolean> featureDefaults = new HashMap<>();
featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
featureDefaults.put(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults);
} @Bean
public JsonComponentModule jsonComponentModule() {
return new JsonComponentModule();
} @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration { @Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
} } @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ParameterNamesModule.class)
static class ParameterNamesModuleConfiguration { @Bean
@ConditionalOnMissingBean
ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);
} } @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperBuilderConfiguration { @Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
customize(builder, customizers);
return builder;
} private void customize(Jackson2ObjectMapperBuilder builder,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
} }
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
@EnableConfigurationProperties(JacksonProperties.class)
static class Jackson2ObjectMapperBuilderCustomizerConfiguration { @Bean
StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
ApplicationContext applicationContext, JacksonProperties jacksonProperties) {
return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
} static final class StandardJackson2ObjectMapperBuilderCustomizer
implements Jackson2ObjectMapperBuilderCustomizer, Ordered { private final ApplicationContext applicationContext; private final JacksonProperties jacksonProperties; StandardJackson2ObjectMapperBuilderCustomizer(ApplicationContext applicationContext,
JacksonProperties jacksonProperties) {
this.applicationContext = applicationContext;
this.jacksonProperties = jacksonProperties;
} @Override
public int getOrder() {
return 0;
} @Override
public void customize(Jackson2ObjectMapperBuilder builder) { if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
}
if (this.jacksonProperties.getTimeZone() != null) {
builder.timeZone(this.jacksonProperties.getTimeZone());
}
configureFeatures(builder, FEATURE_DEFAULTS);
configureVisibility(builder, this.jacksonProperties.getVisibility());
configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureFeatures(builder, this.jacksonProperties.getSerialization());
configureFeatures(builder, this.jacksonProperties.getMapper());
configureFeatures(builder, this.jacksonProperties.getParser());
configureFeatures(builder, this.jacksonProperties.getGenerator());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
configureLocale(builder);
} //more configure methods...
}
}

总结:通过一系列的方法,最终构造一个生产级别可用的ObjectMapper对象,供在Spring Boot框架中对Java对象实现序列化与反序列化操作。

5.Jackson常见注解使用示例

备注:本小结内容来源https://www.baeldung.com/jackson-annotations,如果工作中对于jackson的注解使用较少的情况下,可以看看该篇文章,是一个非常好的补充。

5.1 序列化

5.1.1 @JsonAnyGetter

@JsonAnyGetter注解运行可以灵活的使用Map类型的作为属性字段

实体类如下:

public class ExtendableBean {

    public String name;
private Map<String, String> properties; @JsonAnyGetter
public Map<String, String> getProperties() {
return properties;
} public ExtendableBean(String name) {
this.name = name;
this.properties=new HashMap<String, String>();
} public void add(String key,String value){
this.properties.put(key,value);
}
}

通过序列化该实体Bean,我们将会得到Map属性中的所有Key作为属性值,测试序列化代码如下:

@Test
public void whenSerializingUsingJsonAnyGetter_thenCorrect()
throws JsonProcessingException { ExtendableBean bean = new ExtendableBean("My bean");
bean.add("attr1", "val1");
bean.add("attr2", "val2"); String result = new ObjectMapper().writeValueAsString(bean); assertThat(result, containsString("attr1"));
assertThat(result, containsString("val1"));
}

最终输出结果如下:

{
"name":"My bean",
"attr2":"val2",
"attr1":"val1"
}

如果不使用@JsonAnyGetter注解,那么最终序列化结果将会在properties属性下面,结果如下:

{
"name": "My bean",
"properties": {
"attr2": "val2",
"attr1": "val1"
}
}

5.1.2 @JsonGetter

@JsonGetter注解是一个替代@JsonProperty的注解,可以将一个方法标注为getter方法

例如下面的示例中,我们通过注解@JsonGetter将方法getTheName()作为属性namegetter方法

public class MyBean {
public int id;
private String name; @JsonGetter("name")
public String getTheName() {
return name;
}
}

5.1.3 @JsonPropertyOrder

可以通过使用@JsonPropertyOrder注解来指定属性的序列化顺序

实体bean定义如下:

@JsonPropertyOrder({ "name", "id" })
public class MyBean {
public int id;
public String name;
}

最终序列化结果为:

{
"name":"My bean",
"id":1
}

也可以通过@JsonPropertyOrder(alphabetic=true)来指定按照字母排序,那么响应结果将是:

{
"id":1,
"name":"My bean"
}

5.1.4 @JsonRawValue

@JsonRawValue注解可以指定字符串属性类为json,如下代码:

public class RawBean {
public String name; @JsonRawValue
public String json;
}

创建RawBean的示例,给属性json赋值,代码如下:

 RawBean bean = new RawBean("My bean", "{\"attr\":false}");

String result = new ObjectMapper().writeValueAsString(bean);

最终序列化结果如下:

{
"name":"My bean",
"json":{
"attr":false
}
}

5.1.5 @JsonValue

@JsonValue注解主要用于序列化整个实例对象的单个方法,例如,在一个枚举类中,@JsonValue注解进行标注,代码如下:

public enum  TypeEnumWithValue {
TYPE1(1, "Type A"), TYPE2(2, "Type 2"); private Integer id;
private String name; TypeEnumWithValue(Integer id, String name) {
this.id = id;
this.name = name;
} @JsonValue
public String getName() {
return name;
}
}

测试代码如下:

String enumAsString = new ObjectMapper()
.writeValueAsString(TypeEnumWithValue.TYPE1);
System.out.println(enumAsString);

最终通过序列化代码得到的结果将是:

"Type A"

5.1.6 @JsonRootName

@JsonRootName注解旨在给当前序列化的实体对象加一层包裹对象。

举例如下:

//RootUser.java
public class RootUser { private String name;
private String title; public RootUser(String name, String title) {
this.name = name;
this.title = title;
} //getter and setters
}

在上面的实体类中,正常情况下,如果要序列号RootUser对象,其结果格式为:

{
"name": "name1",
"title": "title1"
}

RootUser加上@JsonRootName注解后,该类改动如下:

//RootUser.java
@JsonRootName(value = "root")
public class RootUser { private String name;
private String title; public RootUser(String name, String title) {
this.name = name;
this.title = title;
} //getter and setters
}

启用ObjectMapper对象的WRAP_ROOT_VALUE特性,测试代码如下:

ObjectMapper objectMapper=new ObjectMapper();
objectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
String result=objectMapper.writeValueAsString(new RootUser("name1","title1"));

最终序列化JSON结果如下:

{
"root": {
"name": "name1",
"title": "title1"
}
}

5.1.7 @JsonSerialize

@JsonSerialize注解允许开发者自定义序列化实现,来看代码实现

public class EventWithSerializer {
public String name; @JsonSerialize(using = CustomDateSerializer.class)
public Date eventDate; public Date publishDate; //getter and setter...
}

在上面的代码中,针对eventDate字段,我们通过使用@JsonSerialize注解,自定义了一个序列化实现类CustomDateSerializer,该类实现如下:

//CustomDateSerializer.java
public class CustomDateSerializer extends StdSerializer<Date> { private static SimpleDateFormat formatter
= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss"); public CustomDateSerializer() {
this(null);
} public CustomDateSerializer(Class<Date> t) {
super(t);
} @Override
public void serialize(
Date value, JsonGenerator gen, SerializerProvider arg2)
throws IOException, JsonProcessingException {
gen.writeString(formatter.format(value));
}
}

最终序列化的结果格式如下:

{
"name": "名称",
"eventDate": "24-03-2021 06:14:32",
"publishDate": 1616580872574
}

从结果我们可以得知,针对某个特定的字段序列化的方式,我们可以完全自定义,非常的方便。

5.2 反序列化

5.2.1 @JsonCreator

@JsonCreator配合@JsonProperty注解能到达在反序列化实体对象时,指定不变更属性名称的效果

例如有如下JSON:

{
"id":1,
"theName":"My bean"
}

在实体类中,我们没有属性名称是theName,但我们想把theName属性反序列化时赋值给name,此时实体类对象结构如下:

public class BeanWithCreator {
public int id;
public String name; @JsonCreator
public BeanWithCreator(
@JsonProperty("id") int id,
@JsonProperty("theName") String name) {
this.id = id;
this.name = name;
}
}

BeanWithCreator的构造函数中添加@JsonCreator注解,并且配合@JsonProperty注解进行属性指向,最终反序列化代码如下:

@Test
public void whenDeserializingUsingJsonCreator_thenCorrect()
throws IOException { String json = "{\"id\":1,\"theName\":\"My bean\"}"; BeanWithCreator bean = new ObjectMapper()
.readerFor(BeanWithCreator.class)
.readValue(json);
assertEquals("My bean", bean.name);
}

5.2.2 @JacksonInject

@JacksonInject注解可以指定反序列化对象时,属性值不从来源JSON获取,而从injection中获取

实体类如下:

public class BeanWithInject {
@JacksonInject
public int id; public String name;
}

反序列化代码

@Test
public void whenDeserializingUsingJsonInject_thenCorrect()
throws IOException { String json = "{\"name\":\"My bean\"}"; InjectableValues inject = new InjectableValues.Std()
.addValue(int.class, 1);
BeanWithInject bean = new ObjectMapper().reader(inject)
.forType(BeanWithInject.class)
.readValue(json); assertEquals("My bean", bean.name);
assertEquals(1, bean.id);
}

5.2.3 @JsonAnySetter

@JsonAnySetter@JsonAnyGetter注解意思一致,只不过是针对序列化与反序列化而言,@JsonAnySetter注解可以将来源JSON最终转化为Map类型的属性结构

实体代码如下:

public class ExtendableBean {
public String name;
private Map<String, String> properties; @JsonAnySetter
public void add(String key, String value) {
properties.put(key, value);
}
}

JSON源如下:

{
"name":"My bean",
"attr2":"val2",
"attr1":"val1"
}

通过@JsonAnySetter的注解标注,最终attr1attr2的值将会添加到properties的Map对象中

示例代码如下:

@Test
public void whenDeserializingUsingJsonAnySetter_thenCorrect()
throws IOException {
String json
= "{\"name\":\"My bean\",\"attr2\":\"val2\",\"attr1\":\"val1\"}"; ExtendableBean bean = new ObjectMapper()
.readerFor(ExtendableBean.class)
.readValue(json); assertEquals("My bean", bean.name);
assertEquals("val2", bean.getProperties().get("attr2"));
}

5.2.4 @JsonSetter

@JsonSetter注解是@JsonProperty的替代注解,用于标注该方法为setter方法

当我们需要读取一些JSON数据时,但是目标实体类与该数据不完全匹配是,该注解是非常有用的。

示例代码如下:

public class MyBean {
public int id;
private String name; @JsonSetter("name")
public void setTheName(String name) {
this.name = name;
}
}

通过指定setTheName作为属性namesetter方法,反序列化时可以达到最终效果

示例如下:

@Test
public void whenDeserializingUsingJsonSetter_thenCorrect()
throws IOException { String json = "{\"id\":1,\"name\":\"My bean\"}"; MyBean bean = new ObjectMapper()
.readerFor(MyBean.class)
.readValue(json);
assertEquals("My bean", bean.getTheName());
}

5.2.5 @JsonDeserialize

@JsonDeserialize注解和序列化注解@JsonSerialize的效果是一致的,作用与反序列化时,针对特定的字段,存在差异化的发序列化效果

public class EventWithSerializer {
public String name; @JsonDeserialize(using = CustomDateDeserializer.class)
public Date eventDate;
}

CustomDateDeserializer代码如下:

public class CustomDateDeserializer
extends StdDeserializer<Date> { private static SimpleDateFormat formatter
= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss"); public CustomDateDeserializer() {
this(null);
} public CustomDateDeserializer(Class<?> vc) {
super(vc);
} @Override
public Date deserialize(
JsonParser jsonparser, DeserializationContext context)
throws IOException { String date = jsonparser.getText();
try {
return formatter.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}

最终,反序列化JSON,时,得到eventDate字段,测试代码如下:

@Test
public void whenDeserializingUsingJsonDeserialize_thenCorrect()
throws IOException { String json
= "{"name":"party","eventDate":"20-12-2014 02:30:00"}"; SimpleDateFormat df
= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
EventWithSerializer event = new ObjectMapper()
.readerFor(EventWithSerializer.class)
.readValue(json); assertEquals(
"20-12-2014 02:30:00", df.format(event.eventDate));
}

5.2.6 @JsonAlias

@JsonAlias注解作用于可以指定一个别名与JSON数据中的字段进行对于,最终反序列化时,能将该值最终反序列化时赋值给对象

实体如下:

public class AliasBean {
@JsonAlias({ "fName", "f_name" })
private String firstName;
private String lastName;
}

上面的代码中,firstName字段通过@JsonAlias注解指定了两个别名字段,意思是反序列化时可以从JSON中读取fName或者f_name的值赋值到firstName

测试代码如下:

@Test
public void whenDeserializingUsingJsonAlias_thenCorrect() throws IOException {
String json = "{\"fName\": \"John\", \"lastName\": \"Green\"}";
AliasBean aliasBean = new ObjectMapper().readerFor(AliasBean.class).readValue(json);
assertEquals("John", aliasBean.getFirstName());
}

5.3 属性注解

5.3.1 @JsonIgnoreProperties

使用@JsonIgnoreProperties注解作用于class级别中可以达到在序列化时忽略一个或多个字段的效果

实体代码如下:

@JsonIgnoreProperties({ "id" })
public class BeanWithIgnore {
public int id;
public String name;
}

最终在序列化BeanWithIgnore实体对象时,字段id将会被忽略

5.3.2 @JsonIgnore

@JsonIgnore注解作用与属性级别中,在序列化时可以忽略该字段

实体代码如下:

public class BeanWithIgnore {
@JsonIgnore
public int id; public String name;
}

最终在序列化BeanWithIgnore实体对象时,字段id将会被忽略

5.3.3 @JsonIgnoreType

@JsonIgnoreType指定忽略类型属性

public class User {
public int id;
public Name name; @JsonIgnoreType
public static class Name {
public String firstName;
public String lastName;
}
}

在上面的示例中,类型Name将会被忽略

5.3.4 @JsonInclude

使用@JsonInclude注解可以排除属性值中包含empty/null/default的属性

@JsonInclude(Include.NON_NULL)
public class MyBean {
public int id;
public String name;
}

MyBean中使用了Include.NON_NULL则代表该实体对象序列化时不会包含空值

5.3.5 @JsonAutoDetect

@JsonAutoDetect可以覆盖实体对象属性中的默认可见级别,比如私有属性可见与不可见

实体对象如下:

public class PrivateBean {
private int id;
private String name; public PrivateBean(int id, String name) {
this.id = id;
this.name = name;
}
}

PrivateBean中,没有给属性字段idname设置公共的getter方法,此时,如果我们如果直接对该实体对象进行序列化时,jackson会提示错误

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.xiaoymin.boot.action.jackson.model.PrivateBean and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

我们修改PrivateBean中的代码,增加@JsonAutoDetect注解,代码如下:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class PrivateBean {
private int id;
private String name; public PrivateBean(int id, String name) {
this.id = id;
this.name = name;
}
}

此时,在序列化该实体对象,将会得到响应结果

PrivateBean bean = new PrivateBean(1, "My bean");
String result = new ObjectMapper().writeValueAsString(bean);
System.out.println(result);

5.4 常规注解

5.4.1 @JsonProperty

我们可以添加@JsonProperty批注以在JSON中指示属性名称。

当实体对象中没有标准的getter/setter方法时,我们可以使用该注解进行指定属性名称已方便jackson框架进行序列化/反序列化

public class MyBean {
public int id;
private String name; @JsonProperty("name")
public void setTheName(String name) {
this.name = name;
} @JsonProperty("name")
public String getTheName() {
return name;
}
}

5.4.2 @JsonFormat

针对日期字段可以通过使用@JsonFormat注解进行格式化输出

public class EventWithFormat {
public String name; @JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "dd-MM-yyyy hh:mm:ss")
public Date eventDate;
}

5.4.3 @JsonUnwrapped

@JsonUnwrapped注解可以指定jackson框架在序列化/反序列化时是否需要对该字段进行wrapped操作

示例代码:

public class UnwrappedUser {
public int id; @JsonUnwrapped
public Name name; //getter and setter... public static class Name {
public String firstName;
public String lastName;
//getter and setter
}
}

通过注解@JsonUnwrapped标注name属性,最终序列化该对象时,会和正常情况下有所区别

UnwrappedUser.Name name = new UnwrappedUser.Name("John", "Doe");
UnwrappedUser user = new UnwrappedUser(1, name); String result = new ObjectMapper().writeValueAsString(user);

我们得到的结果如下:

{
"id": 1,
"firstName": "John",
"lastName": "Doe"
}

5.4.4 @JsonView

通过View的方式来指定序列化/反序列化时是否包含属性

示例代码如下:

View定义

public class Views {
public static class Public {}
public static class Internal extends Public {}
}

实体代码:

public class Item {
@JsonView(Views.Public.class)
public int id; @JsonView(Views.Public.class)
public String itemName; @JsonView(Views.Internal.class)
public String ownerName;
//getter and setter.. }

最终序列化代码示例:

Item item = new Item(2, "book", "John");

String result = new ObjectMapper().writerWithView(Views.Public.class).writeValueAsString(item);
System.out.println(result);

最终序列化结果输出:

{"id":2,"itemName":"book"}

Spring Boot框架中使用Jackson的处理总结的更多相关文章

  1. 在Spring Boot项目中使用Spock框架

    转载:https://www.jianshu.com/p/f1e354d382cd Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring B ...

  2. 在Spring Boot项目中使用Spock测试框架

    本文首发于个人网站:在Spring Boot项目中使用Spock测试框架 Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目 ...

  3. 初识Spring Boot框架(二)之DIY一个Spring Boot的自动配置

    在上篇博客初识Spring Boot框架中我们初步见识了SpringBoot的方便之处,很多小伙伴可能也会好奇这个Spring Boot是怎么实现自动配置的,那么今天我就带小伙伴我们自己来实现一个简单 ...

  4. (Spring Boot框架)快速入门

    Spring Boot 系列文章推荐 Spring Boot 入门 Spring Boot 属性配置和使用 Spring Boot 集成MyBatis Spring Boot 静态资源处理 今天介绍一 ...

  5. Spring Boot 框架@Temporal(TemporalType.DATE)

    使用spring boot框架开发项目时,遇到这样一个问题: 查询pgSQL数据库中表A中某date数据类型的列B,想得到YYYY-MM-DD格式的日期,结果返回的为时间戳(长整型数据). 解决办法: ...

  6. spring boot 框架 启动更新项目,以及生成 "实体_"文件

    1.更新项目 clean  --->  更新项目 ---> package--->refresh 即可.(这几个步骤一个不能够少) 2.项目中的类的依赖关系存在,但是无法导入依赖 m ...

  7. 在Spring Boot框架下使用WebSocket实现聊天功能

    上一篇博客我们介绍了在Spring Boot框架下使用WebSocket实现消息推送,消息推送是一对多,服务器发消息发送给所有的浏览器,这次我们来看看如何使用WebSocket实现消息的一对一发送,模 ...

  8. 在Spring Boot框架下使用WebSocket实现消息推送

    Spring Boot的学习持续进行中.前面两篇博客我们介绍了如何使用Spring Boot容器搭建Web项目(使用Spring Boot开发Web项目)以及怎样为我们的Project添加HTTPS的 ...

  9. spring boot 框架设计步骤

    spring boot 框架设计步骤: 1.poem.xml配置 2.application.yml配置 3.entiry实体 4.realm.Myrealm extends AuthorizingR ...

  10. spring boot 学习(二)spring boot 框架整合 thymeleaf

    spring boot 框架整合 thymeleaf spring boot 的官方文档中建议开发者使用模板引擎,避免使用 JSP.因为若一定要使用 JSP 将无法使用. 注意:本文主要参考学习了大神 ...

随机推荐

  1. es6 快速入门 系列 —— Symbol

    其他章节请看: es6 快速入门 系列 Symbol es6新增的一种原始类型 试图解决的问题 唯一的属性名 给对象新增一个属性,如何保证这个属性名是独一无二的? 更改 instanceof 的运行方 ...

  2. Moon.Orm版本维护及下载

    MoonOrm最新版及代码生成器 (2020-8-29)

  3. 【Unity3D】缩放、平移、旋转场景

    1 前言 ​ 场景缩放.平移.旋转有两种实现方案,一种是对场景中所有物体进行同步变换,另一种方案是对相机的位置和姿态进行变换. ​ 对于方案一,如果所有物体都在同一个根对象下(其子对象或孙子对象),那 ...

  4. Linux实现指定用户sftp传输,静止ssh登录

    1.环境 名称 ip server 192.168.1.1 client 192.168.1.2 2.服务器创建repl用户 useradd -m -d /home/repl -s /usr/sbin ...

  5. 掌握云容器网络:何为ipvs

    本文分享自华为云社区<[理解云容器网络]2-基础篇-ipvs介绍>,作者: 可以交个朋友. IPVS简介 ipvs是工作在Linux内核态的4层负载均衡:和用户态的负载均衡软件(如ngin ...

  6. 【Android 逆向】frida 检测绕过

    1. aaa.apk 安装到手机,是一个叫玩吧的应用 ./hooker ...... 23248 浏 览 器 com.browser2345_oem 32541 玩吧 com.wodi.who 244 ...

  7. 解决pip install时出现的Could not fetch URL https://pypi.org/simple/pip/问题

    打开windows的我的电脑,在最上方目录栏输入%APPDATA%,回车,接着会定位到一个新的目录, 目录路径为C:\Users\Administrator\AppData\Roaming,在这个目录 ...

  8. 通过命令修改git提交的注释信息

    1.修改最新一条 commit 注释信息 通过 git commit --amend 命令修改注释信息,然后:wq 进行保存,再重新提交 2. 修改多条 commit 注释信息 输入命令:git re ...

  9. 如何在矩池云上安装语音识别模型 Whisper

    如何在矩池云上安装语音识别模型 Whisper Whisper 是 OpenAI 近期开源的一个语音识别的模型,研究人员基于 680,000 小时的标记音频数据进行训练,它同时也是一个多任务模型,可以 ...

  10. 【Azure App Service】同一个App Service下创建多个测试站点的方式

    问题描述 在一个App Service中,部署多个应用,每个应用相互独立,类似与IIS中在根目录下创建多个子应用的情况. 问题解答 可以的.通过App Service Configuration页面, ...