概述
本文的編寫初衷,是想了解一下Spring Boot2中嗤朴,具體是怎么序列化和反序列化JSR 310日期時(shí)間體系的,Spring MVC應(yīng)用場(chǎng)景有如下兩個(gè):
- 使用@RequestBody來獲取JSON參數(shù)并封裝成實(shí)體對(duì)象壁榕;
- 使用@ResponseBody來把返回給前端的數(shù)據(jù)轉(zhuǎn)換成JSON數(shù)據(jù)萌丈。
對(duì)于一些Integer、String等基礎(chǔ)類型的數(shù)據(jù)揍很,Spring MVC可以通過一些內(nèi)置轉(zhuǎn)換器來解決郎楼,無需用戶關(guān)心,但是日期時(shí)間類型(例如LocalDateTime)窒悔,由于格式多變呜袁,沒有內(nèi)置轉(zhuǎn)換器可用,就需要用戶自己來配置和處理了蛉迹。
閱讀本文傅寡,假設(shè)讀者初步了解了如何使用Jackson放妈。
測(cè)試環(huán)境
本文使用Spring Boot2.6.6版本北救,鎖定的Jackson版本如下:
<jackson-bom.version>2.13.2.20220328</jackson-bom.version>
Jackson處理JSR 310日期時(shí)間需要引入依賴:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.2</version>
</dependency>
Spring Boot自動(dòng)配置
在spring-boot-autoconfigure包中荐操,自動(dòng)配置了Jackson:
package org.springframework.boot.autoconfigure.jackson;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {
// 詳細(xì)代碼略
}
其中有一段代碼配置了ObjectMapper:
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
可以看到ObjectMapper是由Jackson2ObjectMapperBuilder構(gòu)建的。
再往下會(huì)看到如下代碼:
@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);
}
}
}
發(fā)現(xiàn)在這里創(chuàng)建了Jackson2ObjectMapperBuilder珍策,并且調(diào)用了customize(builder, customizers)方法托启,傳入Lis<Jackson2ObjectMapperBuilderCustomizer> 進(jìn)行定制ObjectMapper。
Jackson2ObjectMapperBuilderCustomizer是個(gè)接口攘宙,只有一個(gè)方法屯耸,源碼如下:
@FunctionalInterface
public interface Jackson2ObjectMapperBuilderCustomizer {
/**
* Customize the JacksonObjectMapperBuilder.
* @param jacksonObjectMapperBuilder the JacksonObjectMapperBuilder to customize
*/
void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder);
}
簡(jiǎn)單點(diǎn)說,Spring Boot會(huì)收集容器里面所有的Jackson2ObjectMapperBuilderCustomizer實(shí)現(xiàn)類蹭劈,統(tǒng)一對(duì)Jackson2ObjectMapperBuilder進(jìn)行設(shè)置疗绣,從而實(shí)現(xiàn)定制ObjectMapper。因此铺韧,如果我們想個(gè)性化定制ObjectMapper多矮,只需要實(shí)現(xiàn)Jackson2ObjectMapperBuilderCustomizer接口并注冊(cè)到容器就可以了。
自定義Jackson配置類
廢話不多說哈打,直接上代碼:
@Component
public class JacksonConfig implements Jackson2ObjectMapperBuilderCustomizer, Ordered {
/** 默認(rèn)日期時(shí)間格式 */
private final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
/** 默認(rèn)日期格式 */
private final String dateFormat = "yyyy-MM-dd";
/** 默認(rèn)時(shí)間格式 */
private final String timeFormat = "HH:mm:ss";
@Override
public void customize(Jackson2ObjectMapperBuilder builder) {
// 設(shè)置java.util.Date時(shí)間類的序列化以及反序列化的格式
builder.simpleDateFormat(dateTimeFormat);
// JSR 310日期時(shí)間處理
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);
// 全局轉(zhuǎn)化Long類型為String塔逃,解決序列化后傳入前端Long類型精度丟失問題
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
builder.serializerByType(Long.class,ToStringSerializer.instance);
}
@Override
public int getOrder() {
return 1;
}
}
這個(gè)配置類實(shí)現(xiàn)了三種個(gè)性化配置:
- 設(shè)置java.util.Date時(shí)間類的序列化以及反序列化的格式;
- JSR 310日期時(shí)間處理料仗;
- 全局轉(zhuǎn)化Long類型為String湾盗,解決序列化后傳入前端Long類型缺失精度問題。
當(dāng)然立轧,讀者還可以按自己的需求繼續(xù)進(jìn)行定制其他配置格粪。
測(cè)試
這里用JSR 310日期時(shí)間進(jìn)行測(cè)試。
創(chuàng)建實(shí)體類User
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private LocalDate localDate;
private LocalTime localTime;
private LocalDateTime localDateTime;
}
創(chuàng)建控制器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"
}
后端返回?cái)?shù)據(jù)
{
"id": "184309536616640512",
"name": "八卦程序",
"localDate": "2023-03-01",
"localTime": "09:35:50",
"localDateTime": "2023-03-01 09:35:50"
}
可以看到氛改,前端傳入了什么數(shù)據(jù)匀借,后端就返回了什么數(shù)據(jù),唯一的區(qū)別就是后端返回的id是字符串了平窘,可以防止前端(例如JavaScript)出現(xiàn)精度丟失問題吓肋。
同時(shí)也證明LocalDateTime等日期時(shí)間類型,到后端參觀了一圈瑰艘,又正常返回了(沒有被拒是鬼,也沒有遭到后端毒打變形,例如變成時(shí)間戳回來紫新,導(dǎo)致親媽都不認(rèn)識(shí)了)均蜜。
前端表白被拒
如果不配置JacksonConfig呢,Spring MVC在嘗試內(nèi)置轉(zhuǎn)換器無果后芒率,會(huì)報(bào)異常如下:
JSON parse error: Cannot deserialize value of type java.time.LocalDateTime
返回給前端的數(shù)據(jù)如下:
{
"timestamp": "2023-03-01T09:53:02.158+00:00",
"status": 400,
"error": "Bad Request",
"path": "/user/test"
}
你懂的囤耳,被拒了。
總結(jié)
核心類ObjectMapper
ObjectMapper是jackson-databind模塊最為重要的一個(gè)類,它完成了數(shù)據(jù)處理的幾乎所有功能充择。
盡管Spring MVC在處理前端傳遞的JSON參數(shù)時(shí)德玫,進(jìn)行了一系列眼花繚亂的操作,但是一頓操作猛如虎椎麦,最終還是靠ObjectMapper來完成序列化和反序列化宰僧。因此,只需要對(duì)Spring Boot默認(rèn)提供的ObjectMapper進(jìn)行個(gè)性化定制即可观挎。
不要覆蓋默認(rèn)配置
我們通過實(shí)現(xiàn)Jackson2ObjectMapperBuilderCustomizer接口并注冊(cè)到容器琴儿,進(jìn)行個(gè)性化定制,Spring Boot不會(huì)覆蓋默認(rèn)ObjectMapper的配置嘁捷,而是進(jìn)行了合并增強(qiáng)造成,具體還會(huì)根據(jù)Jackson2ObjectMapperBuilderCustomizer實(shí)現(xiàn)類的Order優(yōu)先級(jí)進(jìn)行排序,因此上面的JacksonConfig配置類還實(shí)現(xiàn)了Ordered接口雄嚣。
默認(rèn)的Jackson2ObjectMapperBuilderCustomizerConfiguration優(yōu)先級(jí)是0谜疤,因此如果我們想要覆蓋配置,設(shè)置優(yōu)先級(jí)大于0即可现诀。
注意:在SpringBoot2環(huán)境下夷磕,不要將自定義的ObjectMapper對(duì)象注入容器,這樣會(huì)將原有的ObjectMapper配置覆蓋仔沿!
QueryString格式參數(shù)
需要注意的是坐桩,Jackson不能解決QueryString格式參數(shù)的問題,因?yàn)镾pring對(duì)于這類參數(shù)用的是Converter類型轉(zhuǎn)換機(jī)制封锉,那就是另一條參數(shù)綁定之路了(不好意思绵跷,Jackson沒在這條路上幫忙)。
需要自定義參數(shù)類型轉(zhuǎn)換器來處理日期時(shí)間類型成福,需要另寫文章介紹了碾局。