Spring Boot2中如何優雅地個性化客製化Jackson

2023-03-03 18:01:21

概述

本文的編寫初衷,是想了解一下Spring Boot2中,具體是怎麼序列化和反序列化JSR 310日期時間體系的,Spring MVC應用場景有如下兩個:

  1. 使用@RequestBody來獲取JSON引數並封裝成實體物件;
  2. 使用@ResponseBody來把返回給前端的資料轉換成JSON資料。

對於一些Integer、String等基礎型別的資料,Spring MVC可以通過一些內建轉換器來解決,無需使用者關心,但是日期時間型別(例如LocalDateTime),由於格式多變,沒有內建轉換器可用,就需要使用者自己來設定和處理了。

閱讀本文,假設讀者初步瞭解瞭如何使用Jackson。

測試環境

本文使用Spring Boot2.6.6版本,鎖定的Jackson版本如下:

<jackson-bom.version>2.13.2.20220328</jackson-bom.version>

Jackson處理JSR 310日期時間需要引入依賴:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.13.2</version>
</dependency>

Spring Boot自動設定

在spring-boot-autoconfigure包中,自動設定了Jackson:

package org.springframework.boot.autoconfigure.jackson;

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {
    // 詳細程式碼略
}

其中有一段程式碼設定了ObjectMapper

@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
   return builder.createXmlMapper(false).build();
}

可以看到ObjectMapper是由Jackson2ObjectMapperBuilder構建的。

再往下會看到如下程式碼:

@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);
      }
   }

}

發現在這裡建立了Jackson2ObjectMapperBuilder,並且呼叫了customize(builder, customizers)方法,傳入Lis<Jackson2ObjectMapperBuilderCustomizer> 進行客製化ObjectMapper。

Jackson2ObjectMapperBuilderCustomizer是個介面,只有一個方法,原始碼如下:

@FunctionalInterface
public interface Jackson2ObjectMapperBuilderCustomizer {

   /**
    * Customize the JacksonObjectMapperBuilder.
    * @param jacksonObjectMapperBuilder the JacksonObjectMapperBuilder to customize
    */
   void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder);

}

簡單點說,Spring Boot會收集容器裡面所有的Jackson2ObjectMapperBuilderCustomizer實現類,統一對Jackson2ObjectMapperBuilder進行設定,從而實現客製化ObjectMapper。因此,如果我們想個性化客製化ObjectMapper,只需要實現Jackson2ObjectMapperBuilderCustomizer介面並註冊到容器就可以了。

自定義Jackson設定類

廢話不多說,直接上程式碼:

@Component
public class JacksonConfig implements Jackson2ObjectMapperBuilderCustomizer, Ordered {

    /** 預設日期時間格式 */
    private final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
    /** 預設日期格式 */
    private final String dateFormat = "yyyy-MM-dd";
    /** 預設時間格式 */
    private final String timeFormat = "HH:mm:ss";

    @Override
    public void customize(Jackson2ObjectMapperBuilder builder) {
        // 設定java.util.Date時間類的序列化以及反序列化的格式
        builder.simpleDateFormat(dateTimeFormat);

        // JSR 310日期時間處理
        JavaTimeModule javaTimeModule = new JavaTimeModule();

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat);
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));

        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormatter));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormatter));

        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(timeFormat);
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormatter));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormatter));

        builder.modules(javaTimeModule);

        // 全域性轉化Long型別為String,解決序列化後傳入前端Long型別精度丟失問題
        builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
        builder.serializerByType(Long.class,ToStringSerializer.instance);
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

這個設定類實現了三種個性化設定:

  1. 設定java.util.Date時間類的序列化以及反序列化的格式;
  2. JSR 310日期時間處理;
  3. 全域性轉化Long型別為String,解決序列化後傳入前端Long型別缺失精度問題。

當然,讀者還可以按自己的需求繼續進行客製化其他設定。

測試

這裡用JSR 310日期時間進行測試。

建立實體類User

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private LocalDate localDate;
    private LocalTime localTime;
    private LocalDateTime localDateTime;
}

建立控制器UserController

@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("test")
    public User test(@RequestBody User user){
        System.out.println(user.toString());
        return user;
    }

}

前端傳參

{
  "id": 184309536616640512,
  "name": "八卦程式",
  "localDate": "2023-03-01",
  "localTime": "09:35:50",
  "localDateTime": "2023-03-01 09:35:50"
}

後端返回資料

{
  "id": "184309536616640512",
  "name": "八卦程式",
  "localDate": "2023-03-01",
  "localTime": "09:35:50",
  "localDateTime": "2023-03-01 09:35:50"
}

可以看到,前端傳入了什麼資料,後端就返回了什麼資料,唯一的區別就是後端返回的id是字串了,可以防止前端(例如JavaScript)出現精度丟失問題。

同時也證明LocalDateTime等日期時間型別,到後端參觀了一圈,又正常返回了(沒有被拒,也沒有遭到後端毒打變形,例如變成時間戳回來,導致親媽都不認識了)。

前端表白被拒

如果不設定JacksonConfig呢,Spring MVC在嘗試內建轉換器無果後,會報異常如下:
JSON parse error: Cannot deserialize value of type java.time.LocalDateTime

返回給前端的資料如下:

{
  "timestamp": "2023-03-01T09:53:02.158+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/user/test"
}

你懂的,被拒了。

總結

核心類ObjectMapper

ObjectMapper是jackson-databind模組最為重要的一個類,它完成了資料處理的幾乎所有功能。
儘管Spring MVC在處理前端傳遞的JSON引數時,進行了一系列眼花繚亂的操作,但是一頓操作猛如虎,最終還是靠ObjectMapper來完成序列化和反序列化。因此,只需要對Spring Boot預設提供的ObjectMapper進行個性化客製化即可。

不要覆蓋預設設定

我們通過實現Jackson2ObjectMapperBuilderCustomizer介面並註冊到容器,進行個性化客製化,Spring Boot不會覆蓋預設ObjectMapper的設定,而是進行了合併增強,具體還會根據Jackson2ObjectMapperBuilderCustomizer實現類的Order優先順序進行排序,因此上面的JacksonConfig設定類還實現了Ordered介面。

預設的Jackson2ObjectMapperBuilderCustomizerConfiguration優先順序是0,因此如果我們想要覆蓋設定,設定優先順序大於0即可。

注意:在SpringBoot2環境下,不要將自定義的ObjectMapper物件注入容器,這樣會將原有的ObjectMapper設定覆蓋!

QueryString格式引數

需要注意的是,Jackson不能解決QueryString格式引數的問題,因為Spring對於這類引數用的是Converter型別轉換機制,那就是另一條引數繫結之路了(不好意思,Jackson沒在這條路上幫忙)。
需要自定義引數型別轉換器來處理日期時間型別,需要另寫文章介紹了。

個人網站,點選圍觀:八卦程式