在SpringBoot接口中正確地序列化時(shí)間字段的方法
在 Java 項(xiàng)目中處理時(shí)間序列化從來(lái)就不是容易的事。一方面要面臨多變的時(shí)間格式,年月日時(shí)分秒,毫秒,納秒,周,還有討厭的時(shí)區(qū),稍不注意就可能收獲一堆異常,另一方面,Java 又提供了 Date 和 LocalDateTime 兩個(gè)版本的時(shí)間類(lèi)型,二者分別對(duì)應(yīng)著不同的序列化配置,光是弄清楚這些配置,就是一件麻煩事。但是在大多數(shù)時(shí)候,我們又不得不和這一堆配置打交道。
作為開(kāi)始,讓我們來(lái)了解一下可用的配置和相應(yīng)的效果。
時(shí)間字符串配置
準(zhǔn)備一個(gè)簡(jiǎn)單接口來(lái)展示效果。
@Slf4j
@RestController
@RequestMapping("/boom")
public class BoomController {
@Operation(summary = "boom")
@GetMapping
public BoomData getBoomData() {
return new BoomData(Clock.systemDefaultZone());
}
@Operation(summary = "boom")
@PostMapping
public BoomData postBoomData(@RequestBody BoomData boomData) {
log.info("boomData: {}", boomData);
return boomData;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BoomData {
private Date date;
private LocalDateTime localDateTime;
private LocalDate localDate;
private LocalTime localTime;
public BoomData(Clock clock) {
this.date = new Date(clock.millis());
this.localDateTime = LocalDateTime.now(clock);
this.localDate = LocalDate.now(clock);
this.localTime = LocalTime.now(clock);
}
}
上面涉及兩種時(shí)間類(lèi)型:
Date代表老版本日期類(lèi)型,類(lèi)似的還有Calendar,陪著 Java 度過(guò)了漫長(zhǎng)歲月,使用面極廣。但相對(duì)而言,設(shè)計(jì)不太跟得上時(shí)代了,比如值可變導(dǎo)致線(xiàn)程不安全,月份從 0 開(kāi)始有點(diǎn)不正常。LocalDateTime代表java.time包的新版時(shí)間類(lèi)型,JDK 8 中引入。新的時(shí)間類(lèi)型解決老版本類(lèi)型的設(shè)計(jì)缺陷,同時(shí)增加了豐富的 API 來(lái)提高易用性。
兩種類(lèi)型在記錄的信息方面有一點(diǎn)區(qū)別:
Date的時(shí)間精度為毫秒,內(nèi)部實(shí)際是一個(gè) long 類(lèi)型時(shí)間戳。此外還記錄了時(shí)區(qū)信息,簡(jiǎn)單記錄為Date = timestamp + timezone。如果沒(méi)有提供時(shí)區(qū),默認(rèn)使用系統(tǒng)時(shí)區(qū)。LocalDateTime時(shí)間精度為納秒,內(nèi)部用 7 個(gè)整數(shù)來(lái)記錄時(shí)間:- int year
- short month
- short day
- byte hour
- byte minute
- byte second
- int nano
可以簡(jiǎn)單記錄為
LocalDateTime = year + month + day + hour + minute + second + nano。(實(shí)際上應(yīng)該是LocalDateTime = LocalDate + LocalTime,LocalDate = year + month + day,LocalTime = hour + minute + second + nano。)LocalDateTime 沒(méi)有時(shí)區(qū)信息,這也是類(lèi)名中 Local 的含義,代表使用本地時(shí)區(qū)。如果需要時(shí)區(qū)信息,可以用
ZonedDateTime類(lèi)型,ZonedDateTime = LocalDateTime + tz。
了解了兩個(gè)版本時(shí)間類(lèi)型的區(qū)別,再看它們的序列化差異。
JSON 序列化
調(diào)用 GET 接口,得到默認(rèn)的序列化結(jié)果。
{
"date": "2024-10-10T21:07:08.781+08:00",
"localDateTime": "2024-10-10T21:07:08.781283",
"localDate": "2024-10-10",
"localTime": "21:07:08.781263"
}
默認(rèn)配置下,時(shí)間字段被序列化為時(shí)間字符串,但格式不盡相同。Spring Boot 使用 Jackson 進(jìn)行 JSON 序列化,對(duì)不同的時(shí)間類(lèi)型有不同的格式化規(guī)則:
Date默認(rèn)按照 ISO 標(biāo)準(zhǔn)格式化LocalDateTime也按照 ISO 標(biāo)準(zhǔn)處理,精確到微秒,少了時(shí)區(qū)LocalDate和LocalTime與 LocalDateTime 處理方式相似
所謂 ISO 標(biāo)準(zhǔn),指的是 ISO 8601 標(biāo)準(zhǔn),一種專(zhuān)門(mén)處理日期時(shí)間格式的國(guó)際標(biāo)準(zhǔn)。將時(shí)間日期組合按 yyyy-MM-dd'T'HH:mm:ss.SSSXXX 格式處理,比如 2024-10-10T21:07:08.781+08:00,其中字母 T 為日期和時(shí)間分隔符,日期表示為年-月-日,時(shí)間表示為時(shí):分:秒.毫秒。格式中的 XXX 指的是時(shí)區(qū)信息,對(duì)于東 8 區(qū),表示為 +08:00。
默認(rèn)情況下,調(diào)用 POST 接口,也需要保證 body 中的 JSON 串按照 ISO 8601 的格式處理時(shí)間字段,才能正常反序列化,否則 Spring Boot 會(huì)拋出異常。當(dāng)然,時(shí)間格式的要求也沒(méi)那么嚴(yán)格,可以省略時(shí)區(qū)、微秒、毫秒、秒,都能正常反序列化,但 T 不能省略,年月日時(shí)分不能省略。
在接口調(diào)用兩端統(tǒng)一標(biāo)準(zhǔn)時(shí),ISO 8601 表現(xiàn)不壞,但是,碰到國(guó)內(nèi)互聯(lián)網(wǎng)偏愛(ài)的 yyyy-MM-dd HH:mm:ss 格式,就會(huì)收獲一個(gè) HttpMessageNotReadableException,JVM 會(huì)提示你 JSON parse error: Cannot deserialize value of type XXX ...。
如果想要加入 yyyy-MM-dd HH:mm:ss 大家庭,最簡(jiǎn)單的辦法是使用 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。@JsonFormat 注解用于指定時(shí)間類(lèi)型的序列格式,對(duì) Date 類(lèi)型和 LocalDateTime 類(lèi)型都有效。
public class BoomData {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date date;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate localDate;
@JsonFormat(pattern = "HH:mm:ss")
private LocalTime localTime;
}
此時(shí)能 GET 到滿(mǎn)足格式的時(shí)間字符串
{
"date": "2024-11-20 15:15:57",
"localDateTime": "2024-11-20 23:15:57",
"localDate": "2024-11-20",
"localTime": "23:15:57"
}
POST 請(qǐng)求也正常處理。
看樣子 @JsonFormat 效果不壞。問(wèn)題是,稍微有點(diǎn)繁瑣,每個(gè)時(shí)間字段都要配置一遍。幸運(yùn)的是,spring boot 支持全局設(shè)置 Jackson 時(shí)間序列化格式:
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss # 全局時(shí)間格式
time-zone: GMT+8 # 指定默認(rèn)時(shí)區(qū),除非時(shí)間字段已指定時(shí)區(qū),否則 JSON 序列化時(shí)都會(huì)使用此時(shí)區(qū)
更加幸運(yùn)的是,@JsonFormat 優(yōu)先級(jí)比全局配置更高,讓我們可以實(shí)現(xiàn)某些要求特殊格式的需求。
似乎只要組合 spring.jackson.date-format 和 @JsonFormat,我們就可以無(wú)所不能了。沒(méi)有人比我更懂時(shí)間序列化!
可惜的是,spring.jackson.date-format 不支持新版時(shí)間類(lèi)型。是的,在 2024 年,距離 java.time 包發(fā)布已經(jīng)十年了,Spring 的序列化配置仍然不支持 LocalDateTime 類(lèi)型。如果你要序列化 LocalDateTime 類(lèi)型,最簡(jiǎn)單的辦法就是使用 @JsonFormat。因?yàn)?@JsonFormat 是 Jackson 提供的注解。Spring 對(duì)此毫無(wú)作為。
發(fā)完牢騷,考慮如何全局配置 LocalDateTime 的格式化規(guī)則。方案有很多種,最簡(jiǎn)單的就是明確地告訴 Jackson,LocalDateTime 等類(lèi)型按照某某格式序列化和反序列化。
// 大概是 JacksonConfig 之類(lèi)的類(lèi)
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// formatter
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
// deserializers
builder.deserializers(new LocalDateDeserializer(dateFormatter));
builder.deserializers(new LocalDateTimeDeserializer(dateTimeFormatter));
builder.deserializers(new LocalTimeDeserializer(timeFormatter));
// serializers
builder.serializers(new LocalDateSerializer(dateFormatter));
builder.serializers(new LocalDateTimeSerializer(dateTimeFormatter));
builder.serializers(new LocalTimeSerializer(timeFormatter));
};
}
上述代碼為三種類(lèi)型構(gòu)建了不同的 DateTimeFormatter(java.time 包提供的格式化工具,線(xiàn)程安全),然后為每種類(lèi)型綁定序列化器(Serializer)和反序列化器(Deserializer)。
現(xiàn)在 Local 系統(tǒng)的日期類(lèi)型就跟 Date 表現(xiàn)一致了。
總結(jié)一下,在 JSON 序列化時(shí):
- 如果使用了 Date 類(lèi)型,可以用
spring.jackson.date-format和 @JsonFormat 的組合來(lái)適應(yīng)不同格式化要求 - 如果使用了 LocalDateTime 等類(lèi)型,需要配置 Jackson,綁定序列化器和反序列化器,再結(jié)合 @JsonFormat 方能從心所欲
但此時(shí)還沒(méi)結(jié)束,也并非結(jié)束的開(kāi)始,只是開(kāi)始的結(jié)束~
請(qǐng)求參數(shù)
除了 JSON 序列化,還有一種場(chǎng)景,也會(huì)涉及時(shí)間序列化。那就是請(qǐng)求參數(shù)中的時(shí)間字段,最常見(jiàn)的就是 Controller 方法中沒(méi)有用 @RequestBody 標(biāo)記的對(duì)象參數(shù),比如 GET 請(qǐng)求,比如表單提交(application/x-www-form-urlencoded)的 POST 請(qǐng)求。
為了便于展示,在 BoomController 中添加一個(gè)新的接口方法。
@GetMapping("query")
public BoomData queryBoomData(BoomData boomData) {
log.info("boomData: {}", boomData);
return boomData;
}
一個(gè)比較常用的 Query 接口的寫(xiě)法。試著調(diào)用一下。
GET http://localhost:8080/boom/query?localDateTime=2024-10-10T21:07:08.781283&date=2024-10-10T21:07:08.781+08:00
報(bào)錯(cuò),field 'date': rejected value [2024-10-10T21:07:08.781+08:00]。
再試試
GET http://localhost:8080/boom/query?localDateTime=2024-10-10T21:07:08.781283&date=2024-10-10 21:07:08
還是報(bào)錯(cuò),field 'date': rejected value [2024-10-10 21:07:08]。
什么格式才能不報(bào)錯(cuò)?
GET http://localhost:8080/boom/query?localDateTime=2024-10-10T21:07:08.781283&date=10/10/2024 21:07:08
沒(méi)錯(cuò),要用 dd/MM/yyyy 的格式。因?yàn)檎?qǐng)求參數(shù)不歸 JSON 序列化管,而是由 Spring MVC 處理。Spring MVC 默認(rèn)的 Date 類(lèi)型格式就是 dd/MM/yyyy。
要修改也簡(jiǎn)單,@DateTimeFormat,Spring 提供,專(zhuān)門(mén)處理時(shí)間參數(shù)格式化。
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date date; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime localDateTime; @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate localDate; @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalTime localTime;
現(xiàn)在,正常處理請(qǐng)求 http://localhost:8080/boom/query?localDateTime=2024-10-10 21:07:08&date=2024-10-10 21:07:08&localDate=2024-10-10&localTime=11:09:15。
又到了尋找全局配置的時(shí)間了。
spring:
mvc:
format:
date: yyyy-MM-dd HH:mm:ss # 對(duì) Date 和 LocalDate 類(lèi)型有效,LocalDate 會(huì)忽略時(shí)間部分
time: HH:mm:ss # 對(duì) LocalTime 和 OffsetTime 有效
date-time: yyyy-MM-dd HH:mm:ss # LocalDateTime, OffsetDateTime, and ZonedDateTime
按需選擇即可。
總結(jié)一下,對(duì)于 GET 請(qǐng)求參數(shù)中的時(shí)間字段,和表單提交 POST 請(qǐng)求中的時(shí)間字段,可以通過(guò) spring.mvc.format.date/time/date-time 來(lái)配置全局格式。
- 請(qǐng)求中只使用了 Date 類(lèi)型,只需要配置
spring.mvc.format.date - 如果使用了
java.time包中的類(lèi)型,需要根據(jù)類(lèi)型選擇不同配置項(xiàng)
對(duì)于不使用全局配置的場(chǎng)景,用 @DateTimeFormat 指定單獨(dú)的時(shí)間格式。
一起來(lái)用時(shí)間戳吧
以上是使用時(shí)間字符串傳遞時(shí)間的情況,接下來(lái),我們討論一下用時(shí)間戳格式。
先理解一下有關(guān)時(shí)間戳的概念:
GMT 時(shí)間,用來(lái)表示時(shí)區(qū),比如 GMT+8,就是指東 8 區(qū)的時(shí)間。單獨(dú)的 GMT 也可以看作 GMT+0,即 0 時(shí)區(qū)的時(shí)間,這個(gè)時(shí)區(qū)位于英國(guó)格林威治
UTC 時(shí)間,與 GMT 是相同的概念,也用來(lái)表示時(shí)區(qū),只不過(guò) UTC 更精確一些。同樣,UTC+8 可以表示東 8 區(qū),單獨(dú) UTC 表示 0 時(shí)區(qū)
Unix 紀(jì) 元(Unix Epoch),一個(gè)特定的時(shí)間點(diǎn),1970 年 1 月 1 日 00:00:00 UTC(+0),也就是 0 時(shí)區(qū)中 1970 年元旦。這個(gè)時(shí)間點(diǎn)常用于計(jì)算機(jī)系統(tǒng)的時(shí)間起點(diǎn),如同坐標(biāo)軸上的 0。
指導(dǎo)了上述 3 個(gè)概念,時(shí)間戳的含義就容易解釋了,從 Unix 紀(jì) 元開(kāi)始經(jīng)過(guò)的毫秒數(shù)(或秒數(shù),計(jì)算機(jī)常用毫秒)。把時(shí)間想象為一條長(zhǎng)長(zhǎng)的坐標(biāo)軸,0 的位置是 Unix 紀(jì) 元,在那之后,真實(shí)世界的每一毫秒,都對(duì)應(yīng)時(shí)間軸上的一個(gè)點(diǎn)。
時(shí)間戳用整數(shù)表示,一個(gè)長(zhǎng)整數(shù),具備時(shí)間字符串一樣的功能。因此,也可以用時(shí)間戳來(lái)傳遞時(shí)間信息。
如果我信誓旦旦地宣稱(chēng)時(shí)間戳優(yōu)于時(shí)間字符串,肯定是十分主觀的判斷,但在接口中使用時(shí)間戳確實(shí)有一些亮晶晶的優(yōu)點(diǎn)。
- 時(shí)區(qū)無(wú)關(guān)性,時(shí)間戳的值固定為 UTC+0 時(shí)區(qū),無(wú)論位于哪個(gè)時(shí)區(qū),同一時(shí)刻,同一時(shí)間戳。這樣一來(lái),就可以?xún)H展示時(shí)考慮時(shí)區(qū),其他時(shí)候都不需要考慮時(shí)區(qū)
- 體積小,一個(gè) long 值足矣,比時(shí)間字符串更簡(jiǎn)短
- 兼容性好,不必考慮復(fù)雜的格式化規(guī)則
一些不可忽視的缺點(diǎn):
- 可讀性差,時(shí)間戳沒(méi)有時(shí)間字符串直觀,需要一些輔助轉(zhuǎn)換工具,比如瀏覽器控制臺(tái)
- 秒級(jí)時(shí)間戳和毫秒時(shí)間戳可能混淆,使用前要約定好
用 long 型時(shí)間戳也不需要考慮序列化問(wèn)題,大多數(shù)平臺(tái)都可以妥善處理 long 類(lèi)型的序列化。但有些時(shí)候,在代碼中用 Date 和 LocalDateTime 等明確的類(lèi)型還是比 long 更方便。所以可能有這么一個(gè)需求:在代碼中使用時(shí)間類(lèi)型,在序列化時(shí)使用時(shí)間戳。也就是在 DTO 類(lèi)中用 Date,在 JSON 字符串中用 long。
和使用時(shí)間字符串類(lèi)型,這個(gè)需求也分為兩種情況:
- JSON 序列化轉(zhuǎn)換
- 請(qǐng)求參數(shù)轉(zhuǎn)換
二者要分開(kāi)處理。
JSON 序列化中的時(shí)間戳
Spring 提供了一個(gè)配置項(xiàng),控制 Jackson 在序列化時(shí)將時(shí)間類(lèi)型處理為時(shí)間戳。
spring.jackson.serialization.write-dates-as-timestamps=true
此時(shí),GET 請(qǐng)求中的 date 就會(huì)變成了 "date": 1728572627475,POST 時(shí)也能正確地識(shí)別時(shí)間戳。
但是,只有 Date 才有這種優(yōu)渥的待遇,java.time 包的類(lèi)型仍然面臨自己動(dòng)手豐衣足食的窘境。
開(kāi)啟 write-dates-as-timestamps 后,LocalDateTime 等類(lèi)型會(huì)被序列化為整形數(shù)組(回憶一下 LocalDateTime 的簡(jiǎn)單公式)。
{
"date": 1728572627475,
"localDateTime": [
2024,
10,
10,
23,
3,
47,
475519000
],
"localDate": [
2024,
10,
10
],
"localTime": [
23,
3,
47,
475564000
]
}
也不能說(shuō)有問(wèn)題,畢竟 LocalDateTime 精確到納秒,直接轉(zhuǎn)換為毫秒時(shí)間戳,會(huì)丟失精度??傊?,要實(shí)現(xiàn)和諧轉(zhuǎn)換,需要設(shè)置 Jackson。
// 仍然是 JacksonConfig 之類(lèi)的什么地方
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// deserializers
builder.deserializers(new LocalDateDeserializer());
builder.deserializers(new LocalDateTimeDeserializer());
// serializers
builder.serializers(new LocalDateSerializer());
builder.serializers(new LocalDateTimeSerializer());
};
}
public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
/**
* 如果沒(méi)有重寫(xiě) handledType() 方法,會(huì)報(bào)錯(cuò)
* @return LocalDateTime.class
*/
@Override
public Class<LocalDateTime> handledType() {
return LocalDateTime.class;
}
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (value != null) {
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
}
}
public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
@Override
public Class<?> handledType() {
return LocalDateTime.class;
}
@Override
public LocalDateTime deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException {
long timestamp = parser.getValueAsLong();
return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDateTime();
}
}
public static class LocalDateSerializer extends JsonSerializer<LocalDate> {
@Override
public Class<LocalDate> handledType() {
return LocalDate.class;
}
@Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (value != null) {
gen.writeNumber(value.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
}
}
public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
@Override
public Class<?> handledType() {
return LocalDate.class;
}
@Override
public LocalDate deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException {
long timestamp = parser.getValueAsLong();
return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate();
}
}
這里我們進(jìn)行了一些生硬的強(qiáng)制措施,定義了一系列 Deserializer 和 Serializer,實(shí)現(xiàn)了 LocalDateTime 和 long 之間的序列化規(guī)則。
沒(méi)有處理 LocalTime,因?yàn)閱为?dú)的時(shí)間轉(zhuǎn)換為時(shí)間戳不那么契合,時(shí)間戳有明確地年月日,這部分對(duì)于 LocalTime 顯得多余,而且時(shí)間通常與時(shí)區(qū)有關(guān),處理時(shí)要更謹(jǐn)慎一些。可以根據(jù)需求選擇,如果明確需要使用時(shí)間戳來(lái)表示 LocalTime,可以采用類(lèi)似的方法,注冊(cè) Deserializer 和 Serializer。
以上是在 JSON 序列化時(shí)將 Date、LocalDateTime 轉(zhuǎn)化為時(shí)間戳需要的配置:
- 如果只使用 Date,使用 Spring 提供的配置項(xiàng)
spring.jackson.serialization.write-dates-as-timestamps=true即可 - 如果使用了 LocalDateTime,需要進(jìn)行額外的配置,明確地指定 Jackson 將 LocalDateTime 轉(zhuǎn)換為時(shí)間戳
請(qǐng)求參數(shù)中的時(shí)間戳
在請(qǐng)求參數(shù)中使用時(shí)間戳復(fù)雜一些,因?yàn)椴幌駮r(shí)間字符串一樣有現(xiàn)成的配置,需要手動(dòng)實(shí)現(xiàn)轉(zhuǎn)換規(guī)則。
可以利用 Converter 接口來(lái)解決這個(gè)問(wèn)題。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new LongStringToDateConverter());
registry.addConverter(new LongStringToLocalDateTimeConverter());
registry.addConverter(new LongStringToLocalDateConverter());
// registry.addConverter(new LongStringToLocalTimeConverter()); // 按需
}
private static class LongStringToDateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
try {
long timestamp = Long.parseLong(source);
return new Date(timestamp);
} catch (NumberFormatException e) {
return null;
}
}
}
private static class LongStringToLocalDateTimeConverter implements Converter<String, LocalDateTime> {
@Override
public LocalDateTime convert(String source) {
try {
long timestamp = Long.parseLong(source);
return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDateTime();
} catch (NumberFormatException e) {
return null;
}
}
}
private static class LongStringToLocalDateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String source) {
try {
long timestamp = Long.parseLong(source);
return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate();
} catch (NumberFormatException e) {
return null;
}
}
}
private static class LongStringToLocalTimeConverter implements Converter<String, LocalTime> {
@Override
public LocalTime convert(String source) {
try {
long timestamp = Long.parseLong(source);
return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalTime();
} catch (NumberFormatException e) {
return null;
}
}
}
}
注意 Source 類(lèi)型為 String 而不是 Long,因?yàn)?Spring MVC 會(huì)將所有的接口請(qǐng)求參數(shù)類(lèi)型統(tǒng)一視為 String,然后調(diào)用 Converter 轉(zhuǎn)換為其他類(lèi)型。有許多內(nèi)置的 Converter,比如轉(zhuǎn)換為 long 類(lèi)型時(shí),就使用了內(nèi)置的 StringToNumber 轉(zhuǎn)換類(lèi)。我們定義的 LongStringToDateConverter 與 StringToNumber 是平級(jí)的關(guān)系。
以上是在接口參數(shù)中將 Date、LocalDateTime 轉(zhuǎn)化為時(shí)間戳需要的處理:很簡(jiǎn)單,注冊(cè) Converter 即可。
Swagger UI 中的類(lèi)型
使用 SwaggerUI 時(shí),默認(rèn)會(huì)使用 DTO 字段類(lèi)型作為請(qǐng)求參數(shù)類(lèi)型,也就是接收時(shí)間字符串。序列化時(shí)改為時(shí)間戳后,還需要在 Swagger UI 中統(tǒng)一。
Java 項(xiàng)目有兩種集成 Swagger 的方式,Springdoc 和 Spring Fox。Springdoc 更新,對(duì)應(yīng)的配置如下:
@Bean
public OpenAPI customOpenAPI() {
// 關(guān)鍵是要調(diào)用這個(gè)靜態(tài)方法進(jìn)行 replace
SpringDocUtils.getConfig()
.replaceWithClass(Date.class, Long.class)
.replaceWithClass(LocalDateTime.class, Long.class)
.replaceWithClass(LocalDate.class, Long.class);
return new OpenAPI();
}
如果使用 Spring Fox,則需要使用另一種配置:
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
...
.build()
// 重點(diǎn)是這句
.directModelSubstitute(LocalDateTime.class, Long.class);
}
此時(shí),在 Swagger UI 頁(yè)面調(diào)試接口時(shí),時(shí)間類(lèi)型的參數(shù)就顯示為整數(shù)了。
The Only Neat Thing to Do
回顧一下,在 Spring Boot 接口中處理時(shí)間字段序列化,涉及兩個(gè)場(chǎng)景:
- JSON 序列化
- GET 請(qǐng)求和表單提交請(qǐng)求中的參數(shù)
兩種情況要分開(kāi)設(shè)置。
在 Java 類(lèi)型選擇方面,Spring 對(duì) Date 類(lèi)型的支持比 LocalDateTime 好,有很多內(nèi)置的配置,能省去很多麻煩。
如果要使用 LocalDateTime 等類(lèi)型,在 JSON 序列化時(shí)要指定時(shí)間格式,在請(qǐng)求參數(shù)中也要指定時(shí)間格式。前者需要手動(dòng)配置,后者可以使用 Spring 提供的配置項(xiàng)。
如果想要用時(shí)間戳傳遞數(shù)據(jù),也需要分別設(shè)置,在 JSON 序列化時(shí)指定序列化器和反序列化器,在請(qǐng)求參數(shù)中綁定對(duì)應(yīng)的 Converter 實(shí)現(xiàn)類(lèi)。此外,統(tǒng)一 Swagger UI 的類(lèi)型體驗(yàn)更佳。
以上就是在Spring Boot接口中正確地序列化時(shí)間字段的方法的詳細(xì)內(nèi)容,更多關(guān)于Spring Boot序列化時(shí)間字段的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot項(xiàng)目中Date類(lèi)型數(shù)據(jù)在接口返回的時(shí)間不正確的問(wèn)題解決
- springboot配置請(qǐng)求超時(shí)時(shí)間(Http會(huì)話(huà)和接口訪(fǎng)問(wèn))
- SpringBoot根據(jù)各地區(qū)時(shí)間設(shè)置接口有效時(shí)間的實(shí)現(xiàn)方式
- SpringBoot優(yōu)化接口響應(yīng)時(shí)間的九個(gè)技巧
- Springboot項(xiàng)目長(zhǎng)時(shí)間不進(jìn)行接口操作,提示HikariPool-1警告的解決
- SpringBoot接口返回的數(shù)據(jù)時(shí)間與實(shí)際相差8小時(shí)問(wèn)題排查方式
相關(guān)文章
Jenkins一鍵打包部署SpringBoot應(yīng)用
本文主要介紹了Jenkins一鍵打包部署SpringBoot應(yīng)用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
Java回調(diào)函數(shù)實(shí)例代碼詳解
這篇文章主要介紹了Java回調(diào)函數(shù)實(shí)例代碼詳解,需要的朋友可以參考下2017-10-10
Mybatis多個(gè)字段模糊匹配同一個(gè)值的案例
這篇文章主要介紹了Mybatis多個(gè)字段模糊匹配同一個(gè)值的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09
Java實(shí)現(xiàn)ATM系統(tǒng)超全面步驟解讀建議收藏
這篇文章主要為大家詳細(xì)介紹了用Java實(shí)現(xiàn)簡(jiǎn)單ATM機(jī)功能,文中實(shí)現(xiàn)流程寫(xiě)的非常清晰全面,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Spring實(shí)現(xiàn)定時(shí)任務(wù)的幾種方式總結(jié)
Spring Task 是 Spring 框架提供的一種任務(wù)調(diào)度和異步處理的解決方案,可以按照約定的時(shí)間自動(dòng)執(zhí)行某個(gè)代碼邏輯它可以幫助開(kāi)發(fā)者在 Spring 應(yīng)用中輕松地實(shí)現(xiàn)定時(shí)任務(wù)、異步任務(wù)等功能,提高應(yīng)用的效率和可維護(hù)性,需要的朋友可以參考下本文2024-07-07
SpringBoot操作Mongodb的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot操作Mongodb的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06

