1.前言
通常我們在使用Spring Boot框架時,如果沒有特別指定接口的序列化類型,則會使用Spring Boot框架默認集成的Jackson框架進行處理,通過Jackson框架將服務端響應的數據序列化成JSON格式的數據。
本文主要針對在Spring Boot框架中使用Jackson進行處理的經驗進行總結,同時也結合在實際開發場景中碰到的問題以及解決方案進行陳述。
本文涉及到的源碼地址:https://gitee.com/dt_research_institute/code-in-action
PS:目前市面上針對JSON序列化的框架很多,比較出名的就是Jackson、Gson、FastJson。如果開發者對序列化框架沒有特別的要求的情況下,個人建議是直接使用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
類型的字段,包含時分秒類型的日期類型:LocalDateTime
、Date
、Calendar
全部按照我們的要求將日期序列化成了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框架針對日期的序列化時,分別做了不同類型的處理,但我們也能看出差別
LocalDateTime
、LocalDate
類型的字段,Jackson的時區設置不會對該字段產生影響(因為這兩個日期類型自帶時區屬性)Date
、Calendar
類型的字段受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()
作為屬性name
的getter
方法
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
的注解標注,最終attr1
及attr2
的值將會添加到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
作為屬性name
的setter
方法,反序列化時可以達到最終效果
示例如下:
@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
中,沒有給屬性字段id
、name
設置公共的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"}