@[toc]
1. 博客編寫背景
本文章的編寫背景:由于在 JDK 8 中,Date、Timestamp 對象已經(jīng)不推薦使用,所以在公司的新項目上,我計劃將 LocalDateTime 使用在新項目中。
由于本人所在項目組,統(tǒng)一的前后端時間的交互方式為時間戳,而時間戳并不能直接被fastJson 或 jackson 直接轉(zhuǎn)換,所以踩了不少的坑,個人建議有耐心的看完。
實現(xiàn)的效果如下:
- 前端傳遞時間戳
{
"localDateTime": 1584700466000
}
- 后端返回時間戳
{
"code": "0",
"desc": "請求成功",
"data": {
"localDateTime": 1584700466000
}
}
========================================================
若是感覺廢話比較多,那么直接看標(biāo)注了【★★★】的即可
個人寫這個博客,并不想直接寫結(jié)論,更多的是想給讀者分享踩坑的過程
========================================================
2. LocalDateTime 前端交互
2.1 LocalDateTime 向前端寫入時間戳
2.1.1 fastJson 默認(rèn)的寫入格式
本項目使用的是 fastJson 會寫前端,我們先看下以下代碼
- 回寫前端的
VO對象
@Data
public class LocalDateTimeVO {
private LocalDateTime localDateTime;
}
- 測試方法
public static void main(String[] args) {
LocalDateTimeVO localDateTimeVO = new LocalDateTimeVO();
localDateTimeVO.setLocalDateTime(LocalDateTime.now());
String json = JSON.toJSONString(localDateTimeVO);
System.out.println(json);
}
- 控制臺輸出
{"localDateTime":"2020-03-12T23:00:28.747"}
從上圖中可以看出,服務(wù)端并不能正常的返回時間戳給前端。并不符合需求。
2.1.2 更改 fastJson 寫入格式,讓其回寫時間戳 (★★★)
-
fastJson提供了自定義json轉(zhuǎn)換的方法@JSONFiled,我們添加serializeUsing,將其指定到我們自定義的序列化控制器即可。 - 自定義
fastJson序列化轉(zhuǎn)換器,重寫ObjectSerializer
/**
* 由于 LocalDateTime 類型在轉(zhuǎn)換 JSON 的時候,并不能被轉(zhuǎn)換為字符串,使用 @JsonFormat 只能轉(zhuǎn)換為指定的 pattern 類型,因此我們需要自定義一個序列化執(zhí)行器
* LocalDateTime 序列化(將 LocalDateTime類型 轉(zhuǎn)換為 時間戳 返回給前端 )
*
* @author Chimm Huang
* @date 2020/3/7
*/
public class LocalDateTimeSerializer implements ObjectSerializer {
@Override
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
if (object != null) {
LocalDateTime localDateTime = (LocalDateTime) object;
//將localDateTime轉(zhuǎn)換為中國區(qū)(+8)時間戳。
serializer.write(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
} else {
serializer.write(null);
}
}
}
- 使用我們自己寫的
fastJson序列化轉(zhuǎn)換器
@Data
public class LocalDateTimeVO {
@JSONField(serializeUsing = LocalDateTimeSerializer.class)
private LocalDateTime localDateTime;
}
- 再次執(zhí)行測試方法,控制臺輸出
{"localDateTime":1584026032912}
可以看出,LocalDateTime 已經(jīng)成功被轉(zhuǎn)換為了時間戳,并且可以返回給前端。
2.2 接收前端傳遞的時間戳為 LocalDateTimme
2.2.1 Post 請求參數(shù)封裝
1. LocalDateTime 默認(rèn)接收的格式
不管我們傳遞時間戳(1584026032912),還是傳遞自定義格式("2020-03-13"),在服務(wù)端接受的時候,都會報錯400。也就是說,傳入的格式是錯誤的,無法被 spring 轉(zhuǎn)換為 LocalDateTime
經(jīng)過我的粗略測試,我發(fā)現(xiàn),默認(rèn)的接受格式為 LocalDateTime 特有的格式,即:2020-03-12T23:00:28.747,除此之外都會報400。這種格式與 Date 格式的唯一區(qū)別就在于,Date 在日與時之間是用空格區(qū)分的,而 LocalDateTime 是用 T 來區(qū)分的。
2. 更改 fastJson 反序列化方法,讓其能夠轉(zhuǎn)換時間戳為 LocalDateTime(★★★)
-
fastJson提供的@JSONField注解包括了反序列化轉(zhuǎn)換器的指定,因此,我們重寫其方法ObjectDeserializer
/**
* 由于 時間戳 并不能直接被 fastJSON 轉(zhuǎn)換為 LocalDateTime 類型,因此我們需要自定義一個序列化執(zhí)行器
* LocalDateTime 反序列化(將前端傳遞的 時間戳 轉(zhuǎn)換為 LocalDateTime 類型)
*
* @author Chimm Huang
* @date 2020/3/7
*/
public class LocalDateTimeDeserializer implements ObjectDeserializer {
@Override
@SuppressWarnings("unchecked")
public LocalDateTime deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
String timestampStr = parser.getLexer().numberString();
if (timestampStr == null || "".equals(timestampStr)) {
return null;
}
timestampStr = timestampStr.replaceAll("\"", "");
long timestamp = Long.parseLong(timestampStr);
if(timestamp == 0) {
return null;
}
return Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.ofHours(8)).toLocalDateTime();
}
@Override
public int getFastMatchToken() {
return 0;
}
}
- 使用我們自己寫的
fastJson反序列化轉(zhuǎn)換器
@Data
pubcli class LocalDateTimeVO {
@JSONField(serializeUsing = LocalDateTimeSerializer.class, deserializeUsing = LocalDateTimeDeserializer.class)
private LocalDateTime localDateTime;
}
- 測試方法
public static void main(String[] args) {
String json = "{\"localDateTime\":1584026032912}";
LocalDateTimeVO localDateTimeVO = JSON.parseObject(json, LocalDateTimeVO.class);
System.out.println(localDateTimeVO);
}
- 控制臺執(zhí)行結(jié)果展示
LocalDateTimeVO(localDateTime=2020-03-12T23:13:52.912)
可以看出,時間戳成功被 fastJson 接受,并轉(zhuǎn)換為了 LocalDateTime。
3. 【坑】更改 SpringBoot 的 @RequestBody 為 fastJson 接收(★★★)
當(dāng)你看到這個小標(biāo)題時,肯定會很疑惑,我們項目目前不就是使用的 fastJson
嗎?
實際情況經(jīng)過我測試,得出的結(jié)論是,我們在回寫前端的時候,是使用 fastJson 進(jìn)行轉(zhuǎn)換的,但是在接受 Json 的時候,是使用 Spring 默認(rèn)的 jackson 來接受的,所以這會導(dǎo)致,我們重寫了 fastJson 的反序列化方法并未執(zhí)行。前端傳遞時間戳給后端,后端報錯400。
因此,我們需要更改 spring 默認(rèn)提供的 jackson 為 fastJson
/**
* springboot 默認(rèn)使用的是 jackson 進(jìn)行 requestBody 請求的封裝,該項目切換為使用 fastJson 進(jìn)行請求封裝和響應(yīng)
* 配置 springboot 使用 fastJson 進(jìn)行數(shù)據(jù)的請求接受和響應(yīng)
*
* @author Chimm Huang
* @date 2020/3/7
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
public HttpMessageConverter<String> stringConverter() {
return new StringHttpMessageConverter(StandardCharsets.UTF_8);
}
public FastJsonHttpMessageConverter fastConverter() {
//1、定義一個convert轉(zhuǎn)換消息的對象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//2、添加fastJson的配置信息
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteNullNumberAsZero,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteNullBooleanAsFalse);
fastJsonConfig.setCharset(StandardCharsets.UTF_8);
//2-1 處理中文亂碼問題
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastConverter.setSupportedMediaTypes(fastMediaTypes);
//3、在convert中添加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);
return fastConverter;
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.clear();
converters.add(stringConverter());
converters.add(fastConverter());
}
}
配置完成之后,后端與前端使用時間戳進(jìn)行交互已完成。
2.2.2 GET 請求參數(shù)封裝
只需自定義一個轉(zhuǎn)換類即可
/**
* LocalDateTime 作為 作為 RequestParam 或者 PathVariable 時,將前端傳遞的時間戳轉(zhuǎn)換
*
* @author Chimm Huang
* @date 2020/04/16
*/
@Configuration
public class LocalDateTimeGetConverter {
@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
return new Converter<String, LocalDateTime>() {
@Override
public LocalDateTime convert(String source) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(source)), ZoneId.systemDefault());
}
};
}
}
實例代碼:
@GetMapping("/soutTime")
public void soutTimeByGet(LocalDateTime localDateTime) {
System.out.println(localDateTime);
}
前端請求:
localhost:8001/api/demo/soutTime?localDateTime=1587026916000
控制臺輸出:
2020-04-16T16:48:36
3. LocalDateTime 與數(shù)據(jù)庫交互(★★★)
與數(shù)據(jù)庫交互比較簡單,我們使用的 mybatis 的版本為 3.4.5。且數(shù)據(jù)庫時間類型為:datetime
我們只需要在 pom 文件中引入 jsr310 坐標(biāo)即可
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.2</version>
</dependency>
3.1 【坑】數(shù)據(jù)庫交互LocalDateTime被四舍五入(★★★)
LocalDateTime 是可以精確到納秒的,但是數(shù)據(jù)庫datetime類型如果不指定長度的話,默認(rèn)是精確到秒的。這就會造成,在LocalDateTime為最大值的時候,如:2020-04-01T23:59:59.999999999,存入數(shù)據(jù)庫的時候被四舍五入為了2020-04-02 00:00:00。
解決方案一:
重置一下LocalDateTime的最大時間,將最大精度設(shè)置為秒。
解決方案二:
將數(shù)據(jù)庫的datetime類型長度設(shè)置為6(datetime(6)即微秒),然后將LocalDateTime的最大精度重置為對應(yīng)的微妙即可。
以上兩種方案調(diào)用LocalDateTime的withNano()方法即可
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime todayMax = LocalDateTime.of(now.toLocalDate(), LocalTime.MAX);
// 輸出當(dāng)天的時間
System.out.println(now);
// 輸出當(dāng)天的最大時間(默認(rèn)最大)
System.out.println(todayMax);
// 輸出當(dāng)天的最大時間(datetime精度為秒的時候)
System.out.println(todayMax.withNano(0));
// 輸出當(dāng)天的最大時間(datetime精度為毫秒的時候) datetime(3)
System.out.println(todayMax.withNano(999000000));
// 輸出當(dāng)天的最大時間(datetime精度為微秒的時候) datetime(6)
System.out.println(todayMax.withNano(999999000));
}
控制臺輸出
2020-04-01T09:50:46.830845400
2020-04-01T23:59:59.999999999
2020-04-01T23:59:59
2020-04-01T23:59:59.999
2020-04-01T23:59:59.999999