SpringBoot+MyBatis Plus對Map中Date格式轉換的處理

2022-10-10 12:00:30

在 SpringBoot 專案中, 如何統一 JSON 格式化中的日期格式

問題

現在的關係型資料庫例如PostgreSQL/MySQL, 都已經對 JSON 型別提供相當豐富的功能, 專案中對於不需要檢索但是又需要結構化的儲存, 會在資料庫中產生很多 JSON 型別的欄位, 與 Jackson 做物件的序列化和反序列化配合非常方便.

如果 JSON 都是類定義的, 這個序列化和反序列化就非常透明 -- 不需要任何干預, 寫進去是什麼, 讀出來就是什麼. 但是如果 JSON 在 Java 程式碼中是定義為一個 Map, 例如 Map<String, Object> 那麼就有問題了, 對於 Date 型別的資料, 在存入之前是 Date, 取出來之後就變成 Long 了.

SomePO po = new SomePO();
//...
Map<String, Object> map = new HashMap<>();
map.put("k1", new Date());
po.setProperties(map);
//...
mapper.insert(po);
//...
SomePO dummy = mapper.select(po.id);
// 這裡的k1已經變成了 Long 型別
Object k1 = dummy.getProperties().get("k1");

原因

不管是使用原生的 MyBatis 還是包裝後的 MyBatis Plus, 在對 JSON 型別欄位進行序列化和反序列化時, 都需要藉助型別判斷, 呼叫對應的處理邏輯, 大部分情況, 使用的是預設的 Jackson 的 ObjectMapper, 而 ObjectMapper 對 Date 型別預設的序列化方式就是取時間戳, 對於早於1970年之前的日期, 生成的是一個負的長整數, 對於1970年之後的日期, 生成的是一個正的長整數.

檢視 ObjectMapper 的原始碼, 可以看到其對Date格式的序列化和反序列化方式設定於_serializationConfig 和 _deserializationConfig 這兩個成員變數中, 可以通過 setDateFormat() 進行修改

public class ObjectMapper extends ObjectCodec implements Versioned, Serializable {
    //...
    protected SerializationConfig _serializationConfig;
    protected DeserializationConfig _deserializationConfig;
    //...

    public ObjectMapper setDateFormat(DateFormat dateFormat) {
        this._deserializationConfig = (DeserializationConfig)this._deserializationConfig.with(dateFormat);
        this._serializationConfig = this._serializationConfig.with(dateFormat);
        return this;
    }

    public DateFormat getDateFormat() {
        return this._serializationConfig.getDateFormat();
    }
}

預設的序列化反序列化選項, 使用了一個常數 WRITE_DATES_AS_TIMESTAMPS, 在類 SerializationConfig 中進行判斷, 未指定時使用的是時間戳

public SerializationConfig with(DateFormat df) {
	SerializationConfig cfg = (SerializationConfig)super.with(df);
	return df == null ? cfg.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) : cfg.without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

實際的轉換工作在 SerializerProvider 類中, 轉換方法為

public final void defaultSerializeDateValue(long timestamp, JsonGenerator gen) throws IOException {
	if (this.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)) {
		gen.writeNumber(timestamp);
	} else {
		gen.writeString(this._dateFormat().format(new Date(timestamp)));
	}
}

public final void defaultSerializeDateValue(Date date, JsonGenerator gen) throws IOException {
	if (this.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)) {
		gen.writeNumber(date.getTime());
	} else {
		gen.writeString(this._dateFormat().format(date));
	}
}

解決

區域性方案

1. 欄位註解

這種方式可以用在固定的類成員變數上, 不改變整體行為

public class Event {
    public String name;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    public Date eventDate;
}

另外還可以自定義序列化反序列化方法, 實現 StdSerializer

public class CustomDateSerializer extends StdSerializer<Date> {
    //...
}

就可以在 @JsonSerialize 註解中使用

public class Event {
    public String name;

    @JsonSerialize(using = CustomDateSerializer.class)
    public Date eventDate;
}

2. 修改 ObjectMapper

通過 ObjectMapper.setDateFormat() 設定日期格式, 改變預設的日期序列化反序列化行為. 這種方式只對呼叫此ObjectMapper的場景有效

private static ObjectMapper createObjectMapper() {
	ObjectMapper objectMapper = new ObjectMapper();
	SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	objectMapper.setDateFormat(df);
	return objectMapper;
}

因為 ObjectMapper 一般是當作執行緒安全使用的, 而 SimpleDateFormat 並非執行緒安全, 在這裡使用是否會有問題? 關於這個疑慮, 可以檢視 這個連結

@StaxMan: I am a bit concerned if ObjectMapper is still thread-safe after ObjectMapper#setDateFormat() is called. It is known that SimpleDateFormat is not thread safe, thus ObjectMapper won't be unless it clones e.g. SerializationConfig before each writeValue() (I doubt). Could you debunk my fear? – dma_k Aug 2, 2013 at 12:09

DateFormat is indeed cloned under the hood. Good suspicion there, but you are covered.