javascript
Spring Boot框架中使用Jackson的处理总结
1.前言
通常我們在使用Spring Boot框架時,如果沒有特別指定接口的序列化類型,則會使用Spring Boot框架默認集成的Jackson框架進行處理,通過Jackson框架將服務端響應的數(shù)據(jù)序列化成JSON格式的數(shù)據(jù)。
本文主要針對在Spring Boot框架中使用Jackson進行處理的經(jīng)驗進行總結,同時也結合在實際開發(fā)場景中碰到的問題以及解決方案進行陳述。
本文涉及到的源碼地址:https://gitee.com/dt_research_institute/code-in-action
PS:目前市面上針對JSON序列化的框架很多,比較出名的就是Jackson、Gson、FastJson。如果開發(fā)者對序列化框架沒有特別的要求的情況下,個人建議是直接使用Spring Boot框架默認集成的Jackson,沒有必要進行更換。
2.統(tǒng)一序列化時間格式
在我們的接口中,針對時間類型的字段序列化是最常見的需求之一,一般前后端開發(fā)人員會針對時間字段統(tǒng)一進行約束,這樣有助于在編碼開發(fā)時,統(tǒng)一編碼規(guī)范。
在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());} }如果我們對框架代碼沒有任何的配置,此時我們通過調(diào)用接口/queryOne,拿到的返回結果數(shù)據(jù)如下圖:
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... }此時,我們再通過調(diào)用接口,拿到的返回結果如下圖:
通過對birthday字段標注@JsonFormat注解,最終Jackson框架會將該字段序列化為我們標注的格式類型。
2.2 配置全局application.yml
通過@JsonFormat注解的方式雖然能解決問題,但是我們在實際的開發(fā)當中,涉及到的時間字段會非常多,如果全部都用注解的方式對項目中的時間字段進行標注,那開發(fā)的工作量也會很大,并且多團隊一起協(xié)同編碼時,難免會存在遺漏的情況,因此,@JsonFormat注解只適用于針對特定的接口,特定的場景下,對序列化響應的時間字段進行約束,而在全局的角度來看,開發(fā)者應該考慮通過在application.yml配置文件中進行全局配置
針對Spring Boot框架中Jackson的全局配置,我們在application.yml進行配置時,IDEA等編輯器會給出相應的提示,包含的屬性如下圖:
開發(fā)者可以通過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時區(qū)屬性,不過該屬性不配置的情況下,Jackson會使用系統(tǒng)默認時區(qū)。
我們從Spring Boot的源碼中可以看到對Jackson的時間處理邏輯,JacksonAutoConfiguration.java中部分代碼如下:
private void configureDateFormat(Jackson2ObjectMapperBuilder builder) {// We support a fully qualified class name extending DateFormat or a date// pattern string valueString 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// defaultTimeZone timeZone = this.jacksonProperties.getTimeZone();if (timeZone == null) {timeZone = new ObjectMapper().getSerializationConfig().getTimeZone();}simpleDateFormat.setTimeZone(timeZone);builder.dateFormat(simpleDateFormat);}} }從上面的代碼中,我們可以看到的處理邏輯:
- 從yml配置文件中拿到dateFormat屬性字段
- 首先通過ClassUtils.forName方法來判斷開發(fā)者配置的是否是格式化類,如果配置的是格式化類,則直接配置dateFormat屬性
- 類找不到的情況下,捕獲ClassNotFoundException異常,默認使用JDK自帶的SimpleDateFormat類進行初始化
最終,我們在application.yml配置文件中配置了全局的Jackson針對日期處理的格式化信息,此時我們再看/queryOne接口響應的內(nèi)容是什么情況呢?如下圖:
從圖中我們可以發(fā)現(xiàn),除了LocalDate類型的字段,包含時分秒類型的日期類型:LocalDateTime、Date、Calendar全部按照我們的要求將日期序列化成了yyyy-MM-dd HH:mm:ss格式,達到了我們的要求。
3.Jackson在Spring Boot框架中的配置選項
在上面的時間字段序列化處理,我們已經(jīng)知道了如何配置,那么在Spring Boot的框架中,針對Jackson的各個配置項主要包含哪些呢?我們通過IDEA的提示可以看到,配置如下圖:
在上面的12個屬性中,每個屬性的配置都會對Jackson產(chǎn)生不同的效果,接下來,我們逐一詳解每個屬性配置的作用
3.1 date-format日期格式化
date-format在前面我們已經(jīng)知道了該屬性的作用,主要是針對日期字段的格式化
3.2 time-zone時區(qū)
time-zone字段也是和日期字段類型,使用不同的時區(qū),最終日期類型字段響應的結果會不一樣
時區(qū)的表示方法有兩種:
- 指定時區(qū)的名稱,例如:Asia/Shanghai,America/Los_Angeles
- 通過格林威治平時GMT針對時分秒做+或者-自定義操作
通過指定時區(qū)的名稱,假設我們指定當前的項目是America/Los_Angeles,那么接口響應的數(shù)據(jù)是什么效果呢?
PS:時區(qū)名稱如果不是很清楚的話,一般在Linux服務器的/usr/share/zoneinfo目錄可以進行查看,如下圖:
application.yml:
spring:jackson:date-format: yyyy-MM-dd HH:mm:sstime-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; }由于洛杉磯時區(qū)與上海時區(qū)相差16個小時,因此,Jackson框架針對日期的序列化時,分別做了不同類型的處理,但我們也能看出差別
- LocalDateTime、LocalDate類型的字段,Jackson的時區(qū)設置不會對該字段產(chǎn)生影響(因為這兩個日期類型自帶時區(qū)屬性)
- Date、Calendar類型的字段受Jackson序列化框架的時區(qū)設置影響
另外一種方式是通過格林威治平時(GMT)做加減法,主要有兩種格式支持:
- GMT+HHMM或者GMT-HHMM或者GMT+H:其中HH代表的是小時數(shù),MM代表的是分鐘數(shù),取值范圍是0-9,例如我們常見的GMT+8代表東八區(qū),也就是北京時間
- GMT+HH:MM或者GMT-HH:MM:其中HH代表的是小時數(shù),MM代表的是分鐘數(shù),取值范圍是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修改為privateprivate String getName() {return name;}//other setter and getter }此時,我們通過調(diào)用/queryOne接口響應結果如下:
從結果中我們可以看到,由于我們將name屬性的getter方法設置為了private,因此jackson在序列化時,沒有拿到該字段
此時,我們再修改application.yml的配置,如下:
spring:jackson:visibility:getter: any我們通過將getter設置為any級別的類型,再調(diào)用/queryOne接口,響應結果如下:
從圖中可以看出,jackson序列化結果中又出現(xiàn)了name屬性,這代表即使name字段的屬性和getter方法都是private,但是jackson還是獲取到了該成員變量的值,并且進行了序列化處理。
通過設置visibility屬性即可達到上面的效果。開發(fā)者根據(jù)自己的需要自行進行選擇。
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 importpublic class PropertyNamingStrategy // NOTE: was abstract until 2.7implements 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:sslocale: zh_CNtime-zone: GMT+8visibility:getter: anyproperty-naming-strategy: LOWER_CAMEL_CASESNAKE_CASE
SNAKE_CASE主要包含的規(guī)則,詳見SnakeCaseStrategy:
- java屬性名稱中所有大寫的字符都會轉(zhuǎn)換為兩個字符,下劃線和該字符的小寫形式,例如userName會轉(zhuǎn)換為user_name,對于連續(xù)性的大寫字符,近第一個進行下劃線轉(zhuǎn)換,后面的大小字符則是小寫,例如theWWW會轉(zhuǎn)換為the_www
- 對于首字母大寫的情況,近轉(zhuǎn)成小寫,例如:Results會轉(zhuǎn)換為results,并不會轉(zhuǎn)換為_results
- 針對屬性中已經(jīng)包含下劃線的情況,僅做小寫轉(zhuǎn)換處理
- 下劃線出現(xiàn)在首位的情況下,會被去除處理,例如屬性名:_user會被轉(zhuǎn)換為user
真實效果如下圖:
UPPER_CAMEL_CASE
UPPER_CAMEL_CASE顧名思義,駝峰命名法的規(guī)則,只是首字母會轉(zhuǎn)換為大寫,詳見UpperCamelCaseStrategy
真實效果圖如下:
LOWER_CAMEL_CASE
LOWER_CAMEL_CASE效果和UPPER_CAMEL_CASE正好相反,其首字母會變成小寫,詳見LowerCamelCaseStrategy
效果圖如下:
LOWER_CASE
LOWER_CASE從命名來看很明顯,將屬性名 全部轉(zhuǎn)為小寫,詳見LowerCaseStrategy
KEBAB_CASE
KEBAB_CASE策略和SNAKE_CASE規(guī)則類似,只是下劃線變成了橫線-,詳見KebabCaseStrategy
效果圖如下:
LOWER_DOT_CASE
LOWER_DOT_CASE策略和KEBAB_CASE規(guī)則相似,只是由橫線變成了點.,詳見LowerDotCaseStrategy
效果圖如下:
總結:看了上面這么多屬性名稱的策略,其實每一種類型只是不同的場景下才需要,如果上面jackson給定的默認策略名稱無法滿足,我們從源碼中也能看到,通過自定義實現(xiàn)類,也能滿足企業(yè)的個性化需求,非常方便。
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的默認規(guī)則對該字段進行序列化,詳見示例
- CUSTOM:自定義規(guī)則
- USE_DEFAULTS:配置使用該規(guī)則的屬性字段,將會優(yōu)先使用class上的注解規(guī)則,否則會使用全局的序列化規(guī)則,詳見示例
CUSTOM自定義規(guī)則是需要開發(fā)者在屬性字段上使用@JsonInclude注解,并且指定valueFilter屬性,該屬性需要傳遞一個Class,示例如下:
//User.java //指定value級別是CUSTOM @JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = StringFilter.class) private String name;StringFilter則是判斷非空的依據(jù),該依據(jù)由開發(fā)者自己定義,返回true將會被排除,false則不會排除,示例如下:
//自定義非空判斷規(guī)則 public class StringFilter {@Overridepublic boolean equals(Object other) {if (other == null) {// Filter null's.return true;}// Filter "custom_string".return "custom_string".equals(other);} }4.Spring Boot針對Jackson的約定配置做的事情
在前面的文章中,我們已經(jīng)詳細的了解了Jackson在Spring Boot框架中的各個配置項,那么Spring Boot針對Jackson框架在約定配置時會做哪些事情呢?
在Spring Boot的spring-boot-autoconfigure-x.x.jar包中,我們可以看到Spring Boot框架針對jackson的處理源碼,如下圖:
主要包含三個類:
- JacksonProperties:Spring Boot框架提供jackson的配置屬性類,即開發(fā)者在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的實現(xiàn)類,主要用于接收JacksonProperties屬性,將Jackson的外部配置屬性接收,然后最終執(zhí)行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);}@Beanpublic JsonComponentModule jsonComponentModule() {return new JsonComponentModule();}@Configuration(proxyBeanMethods = false)@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)static class JacksonObjectMapperConfiguration {@Bean@Primary@ConditionalOnMissingBeanObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {return builder.createXmlMapper(false).build();}}@Configuration(proxyBeanMethods = false)@ConditionalOnClass(ParameterNamesModule.class)static class ParameterNamesModuleConfiguration {@Bean@ConditionalOnMissingBeanParameterNamesModule parameterNamesModule() {return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);}}@Configuration(proxyBeanMethods = false)@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)static class JacksonObjectMapperBuilderConfiguration {@Bean@Scope("prototype")@ConditionalOnMissingBeanJackson2ObjectMapperBuilder 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 {@BeanStandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(ApplicationContext applicationContext, JacksonProperties jacksonProperties) {return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);}static final class StandardJackson2ObjectMapperBuilderCustomizerimplements Jackson2ObjectMapperBuilderCustomizer, Ordered {private final ApplicationContext applicationContext;private final JacksonProperties jacksonProperties;StandardJackson2ObjectMapperBuilderCustomizer(ApplicationContext applicationContext,JacksonProperties jacksonProperties) {this.applicationContext = applicationContext;this.jacksonProperties = jacksonProperties;}@Overridepublic int getOrder() {return 0;}@Overridepublic 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...} }總結:通過一系列的方法,最終構造一個生產(chǎn)級別可用的ObjectMapper對象,供在Spring Boot框架中對Java對象實現(xiàn)序列化與反序列化操作。
5.Jackson常見注解使用示例
備注:本小結內(nèi)容來源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;@JsonAnyGetterpublic 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;@JsonRawValuepublic String json; }創(chuàng)建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;}@JsonValuepublic 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注解允許開發(fā)者自定義序列化實現(xiàn),來看代碼實現(xiàn)
public class EventWithSerializer {public String name;@JsonSerialize(using = CustomDateSerializer.class)public Date eventDate;public Date publishDate;//getter and setter... }在上面的代碼中,針對eventDate字段,我們通過使用@JsonSerialize注解,自定義了一個序列化實現(xiàn)類CustomDateSerializer,該類實現(xiàn)如下:
//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); }@Overridepublic 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;@JsonCreatorpublic BeanWithCreator(@JsonProperty("id") int id, @JsonProperty("theName") String name) {this.id = id;this.name = name;} }在BeanWithCreator的構造函數(shù)中添加@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 {@JacksonInjectpublic 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最終轉(zhuǎn)化為Map類型的屬性結構
實體代碼如下:
public class ExtendableBean {public String name;private Map<String, String> properties;@JsonAnySetterpublic 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數(shù)據(jù)時,但是目標實體類與該數(shù)據(jù)不完全匹配是,該注解是非常有用的。
示例代碼如下:
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的效果是一致的,作用與反序列化時,針對特定的字段,存在差異化的發(fā)序列化效果
public class EventWithSerializer {public String name;@JsonDeserialize(using = CustomDateDeserializer.class)public Date eventDate; }CustomDateDeserializer代碼如下:
public class CustomDateDeserializerextends StdDeserializer<Date> {private static SimpleDateFormat formatter= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");public CustomDateDeserializer() { this(null); } public CustomDateDeserializer(Class<?> vc) { super(vc); }@Overridepublic 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數(shù)據(jù)中的字段進行對于,最終反序列化時,能將該值最終反序列化時賦值給對象
實體如下:
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 {@JsonIgnorepublic int id;public String name; }最終在序列化BeanWithIgnore實體對象時,字段id將會被忽略
5.3.3 @JsonIgnoreType
@JsonIgnoreType指定忽略類型屬性
public class User {public int id;public Name name;@JsonIgnoreTypepublic 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方法,此時,如果我們?nèi)绻苯訉υ搶嶓w對象進行序列化時,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 常規(guī)注解
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;@JsonUnwrappedpublic Name name;//getter and setter...public static class Name {public String firstName;public String lastName;//getter and setter} }通過注解@JsonUnwrapped標注name屬性,最終序列化該對象時,會和正常情況下有所區(qū)別
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的处理总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 工业用微型计算机(18)-指令系统(13
- 下一篇: mybatis里mapper.xml中S