Spring Boot框架中使用Jackson的處理總結


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,拿到的返回結果數據如下圖:

image-20210312085839202

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...
}

此時,我們再通過調用接口,拿到的返回結果如下圖:

image-20210312090417967

通過對birthday字段標注@JsonFormat注解,最終Jackson框架會將該字段序列化為我們標注的格式類型。

2.2 配置全局application.yml

通過@JsonFormat注解的方式雖然能解決問題,但是我們在實際的開發當中,涉及到的時間字段會非常多,如果全部都用注解的方式對項目中的時間字段進行標注,那開發的工作量也會很大,並且多團隊一起協同編碼時,難免會存在遺漏的情況,因此,@JsonFormat注解只適用於針對特定的接口,特定的場景下,對序列化響應的時間字段進行約束,而在全局的角度來看,開發者應該考慮通過在application.yml配置文件中進行全局配置

針對Spring Boot框架中Jackson的全局配置,我們在application.yml進行配置時,IDEA等編輯器會給出相應的提示,包含的屬性如下圖:

image-20210312092003557

開發者可以通過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接口響應的內容是什么情況呢?如下圖:

image-20210312094014588

從圖中我們可以發現,除了LocalDate類型的字段,包含時分秒類型的日期類型:LocalDateTimeDateCalendar全部按照我們的要求將日期序列化成了yyyy-MM-dd HH:mm:ss格式,達到了我們的要求。

3.Jackson在Spring Boot框架中的配置選項

在上面的時間字段序列化處理,我們已經知道了如何配置,那么在Spring Boot的框架中,針對Jackson的各個配置項主要包含哪些呢?我們通過IDEA的提示可以看到,配置如下圖:

image-20210312092003557

在上面的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目錄可以進行查看,如下圖:

image-20210312131802521

application.yml:

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

效果圖如下:

image-20210312130547087

我們在結合代碼來分析:

//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接口響應結果如下:

image-20210314191124147

從結果中我們可以看到,由於我們將name屬性的getter方法設置為了private,因此jackson在序列化時,沒有拿到該字段

此時,我們再修改application.yml的配置,如下:

spring:
  jackson:
    visibility:
      getter: any

我們通過將getter設置為any級別的類型,再調用/queryOne接口,響應結果如下:

image-20210314191405490

從圖中可以看出,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

真實效果如下圖:

image-20210315125556270

UPPER_CAMEL_CASE

UPPER_CAMEL_CASE顧名思義,駝峰命名法的規則,只是首字母會轉換為大寫,詳見UpperCamelCaseStrategy

真實效果圖如下:

image-20210315131430536

LOWER_CAMEL_CASE

LOWER_CAMEL_CASE效果和UPPER_CAMEL_CASE正好相反,其首字母會變成小寫,詳見LowerCamelCaseStrategy

效果圖如下:

image-20210315131729151

LOWER_CASE

LOWER_CASE從命名來看很明顯,將屬性名 全部轉為小寫,詳見LowerCaseStrategy

KEBAB_CASE

KEBAB_CASE策略和SNAKE_CASE規則類似,只是下划線變成了橫線-,詳見KebabCaseStrategy

效果圖如下:

image-20210315132557384

LOWER_DOT_CASE

LOWER_DOT_CASE策略和KEBAB_CASE規則相似,只是由橫線變成了點.,詳見LowerDotCaseStrategy

效果圖如下:

image-20210315132720262

總結:看了上面這么多屬性名稱的策略,其實每一種類型只是不同的場景下才需要,如果上面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的處理源碼,如下圖:

image-20210320122535467

主要包含三個類:

  • 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"}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM