日期格式化時註解@DateTimeFormat無效的問題分析

湯圓學Java發表於2021-05-15

作者:湯圓

個人部落格:javalover.cc

背景

有時候我們在寫介面時,需要把前臺傳來的日期String型別轉為Date型別

這時我們可能會用到@DateTimeFormat註解

在請求資料為非JSON格式時,這個註解是沒有問題的,可用的;

但是當請求資料為JSON格式時,問題就出現了

  • 此時如果請求引數沒有加@RequestBody註解,那麼請求引數不會執行型別轉換操作,資料都是預設為空(基本型別比如int = 0, 物件引用比如Date date= null)
  • 此時如果請求引數有加@RequestBody註解,那麼請求引數會執行JSON型別轉換操作,但是轉換會提示異常

所以文章題目中所說的有時無效,指的就是上面這兩種情況

目錄

本文分三步走,如下所示,其中會穿插著介紹@DateTimeFormat、@RequestBody、@JsonFormat註解

註解:日期格式化

分析

1. 基礎程式碼:

AnnationApplication.java:主程式兼控制器

package com.jalon.annation;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class AnnationApplication {

	public static void main(String[] args) {
		SpringApplication.run(AnnationApplication.class, args);
	}

	@PostMapping("/personPost")
	public Person personPost(Person person){
		System.out.println(person);
		return person;
	}
}

Person.java 實體類

package com.jalon.annation;

import com.fasterxml.jackson.annotation.JacksonAnnotation;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Component;

import java.util.Date;

public class Person {

    private int age;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date birth;

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", birth=" + birth +
                '}';
    }
		// 省略getter/setter
}

2. 案例分析:

這裡我們用的是PostMan進行測試,請求示例如下

所有示例全程都有@DateTimeFormat註解

示例1:

  • 請求方式:Post請求

  • 資料格式:非JSON格式,比如form-data

  • 請求資源:personPost(Person person),無@RequestBody註解

    具體請求內容和返回結果如下所示

image-20210515133940137

可以看到,前臺返回正常(資料無誤),說明@DateTimeFormat有效,成功解析了日期字串

這裡返回的資料都是經過@ResponseBody處理過的,因為我們沒有配置返回資料的日期格式化,所以這裡返回的日期格式是預設的

@ResponseBody對應於@RequestBody;

  • 前者負責將Java物件序列號成JSON資料進行返回
  • 後者負責解析請求過來的JSON資料,解析成對應的Java物件

我們再來看下後臺,列印如下:

Person{age=1, birth=Wed Jan 01 00:00:00 CST 2020}

可以看到,後臺列印正常(資料無誤,日期格式忽略,因為這裡的date.toString用的Date的預設方法)

從上面的結果我們可以看到,@DateTimeFormat只是負責解析傳來的日期字串,轉為對應的日期物件;

但是並不會修改原有的日期物件的格式(從前臺返回和後臺輸出可以看到,日期格式不受@DateTimeFormat的影響)

示例2:

  • 請求方式:Post請求

  • 資料格式:JSON格式,比如application/json

  • 請求資源:personPost(Person person),無@RequestBody註解

    具體請求內容和返回結果如下所示

post-json-no@RequestBody

可以看到,返回資料都為空(預設的初始值),說明資料都沒有傳過去,不止是date,連基本型別int都沒過去

我們再來看下後臺,列印如下

Person{age=0, birth=null} // 跟前臺返回的資料一致

可以看到,後臺解析到的資料也是空的,所以上面返回的當然是空的

原因就是預設的型別轉換器是沒有轉化成JSON格式的對應轉換類的,部分轉換器如下所示,(core.convert.support包)

convert-support-classes

解決:所以這裡對應的解決辦法就是,自己建立一個JSON轉換器

但是實際上這個已經有實現了,只是沒有觸發,如下所示的構建工具(http.converter.json包),就是用來配置相關的json序列化和反序列化的

convert-json-classes

現在我們可以通過@RequestBody註解來觸發,它在接收到JSON格式的資料時,會自動呼叫對應的JSON轉換器

下面的示例3就是這個例子

加了@RequestBody後,預設只接受application/json格式的資料,如果傳入其他格式,會報415不支援的型別

示例3:

  • 請求方式:Post請求

  • 資料格式:JSON格式,比如application/json

  • 請求資源:personPost(@RequestBody Person person),有@RequestBody註解

    具體請求內容和返回結果如下所示

image-20210515134858894

可以看到,報錯了,提示400,這種一般屬於客戶端錯誤(比如資料格式不正確,資料過大等)

我們再來看下後臺,列印如下

2021-05-15 13:48:41.578  WARN 38426 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2020-01-01 00:00:00": not a valid representation (error: Failed to parse Date value '2020-01-01 00:00:00': Cannot parse date "2020-01-01 00:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSX', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2020-01-01 00:00:00": not a valid representation (error: Failed to parse Date value '2020-01-01 00:00:00': Cannot parse date "2020-01-01 00:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSX', parsing fails (leniency? null))
 at [Source: (PushbackInputStream); line: 3, column: 14] (through reference chain: com.jalon.annation.Person["birth"])]

這裡我們提取關鍵的部分來看:

1. nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2020-01-01 00:00:00"
 
2. Cannot parse date "2020-01-01 00:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSX'

首先這裡跟示例2不同,這裡起碼做了嘗試轉換,只是沒有找到對應的格式,所以轉換失敗了

可以看到,它並沒有按照上面我們的@DateTimeFormat註解去解析,而是按照''yyyy-MM-dd'T'HH:mm:ss.SSSX"這個格式去解析

這裡如果想投機的話,可以在前臺直接傳入''yyyy-MM-dd'T'HH:mm:ss.SSSX'格式的資料,如下:

post-json-@RequestBody-front-change-dateformat

但是這種辦法對於前端很不友好(極其不好)

所以下面還是給出正常的解決辦法

解決:所以這裡的解決辦法就是自己定義日期格式

  • 方案一:區域性註解來解決,比如在date欄位新增@JsonFormat()註解
// 這個註解用來解析JSON資料中的日期字串,會序列化返回資料
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date birth;

區域性的特點:靈活,但是配置繁瑣,不統一(每個欄位都要加)

  • 方案二:全域性配置來解決,比如配置一個Jackson2ObjectMapperBuilderCustomizer,然後自定義日期反序列化格式
package com.jalon.annation;

import com.fasterxml.jackson.databind.deser.std.DateDeserializers;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.text.SimpleDateFormat;
import java.util.Date;

@Configuration
public class MyDateConvertCustoms implements Jackson2ObjectMapperBuilderCustomizer {
    @Override
    public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
			// 覆蓋預設的Date反序列化,第一個引數為需要反序列化的類,第二個為具體的序列化格式
      jacksonObjectMapperBuilder.deserializerByType(
                Date.class
                ,new DateDeserializers.DateDeserializer(
                        DateDeserializers.DateDeserializer.instance
                        , new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                        , null));
    }
}

全域性的特點:不靈活,但是直觀清晰,配置統一

3. 結論分析:

主要根據請求的資料型別來對比

  • 請求非JSON資料,建議用@DateTimeFormat即可(比如get請求,當然get請求也可以請求JSON資料,只是不推薦)
  • 請求JSON資料,建議用@ReqeustBody來轉換資料,然後搭配區域性註解@JsonFormat或者全域性配置來修改預設的日期解析格式(預設"yyyy-MM-dd'T'HH:mm:ss.SSSX")

總結

註解相關:

  1. @DateTimeFormat註解:適用於請求資料為非JSON資料,不會格式化返回資料
  2. @JsonFormat註解:適用於請求資料為JSON資料(尤其有日期資料時),且需在請求方法的引數前加@RequestBody`註解,會格式化返回資料
  3. @RequestBody註解:解析傳來的JSON資料,轉換成對應的Java物件
  4. @ResponseBody註解:轉換Java物件為JSON資料,用來作為返回資料輸出到前端

日期格式化相關:

  1. 請求非JSON資料,建議用@DateTimeFormat即可,此時不會格式化返回資料(比如get請求,當然get請求也可以請求JSON資料,只是不推薦)

  2. 請求JSON資料,建議用@ReqeustBody來轉換資料,然後搭配區域性註解@JsonFormat(會格式化返回資料)或者全域性配置來修改預設的日期解析格式(預設"yyyy-MM-dd'T'HH:mm:ss.SSSX");全域性配置也可以格式化返回資料,需配置builder.serializerByType

  3. 如果日期格式化出錯,先看傳來的資料是否為JSON資料(可以通過consumes來限制),然後再看有沒有對於的註解或日期格式化全域性配置

參考內容:

後記

學習之路漫漫,共勉之

寫在最後:

願你的意中人亦是中意你之人

相關文章