日拱一卒無有盡,功不唐捐終入海。
楔子
前兩周發了三篇SpringSecurity和一篇征文,這周打算寫點簡單有用易上手的文章,換換腦子,休息一下。
今天要寫的是這篇:從LocalDateTime序列化來看全局一致性序列化體驗
。
這個標題看起來蠻不像人話的,有種挺官方的感覺,我先給大家翻譯翻譯我們的主題是什么:通過講解LocalDateTime
的序列化從而引出整個項目中的所有序列化處理,並讓他們保持一致。
在我們項目中一般存在着兩種序列化,
一個呢是SpringMVC
官方的序列化,也就是Spring
幫你做的序列化,比如你在一個接口上面打了一個ResponseBody
注解,SpringMVC
中的消息轉換器會幫你做序列化。
另一個就是我們項目內的序列化,自己定義的JsonUtil也好,還是你引入的第三方JSON處理工具(比如FastJson
)也好,都可以說做是我們項目內部的序列化。
這兩者如果不一樣,有時候序列化出來的數據可能會出現結果不大一樣的結果,為了防止這種情況,今天我們就來探討一下項目中的序列化。
1. 💡舉個例子
我們先來舉個例子,來看看如果序列化不一致會出現啥樣的效果。
@GetMapping("/api/anon")
public ApiResult test01() { return ApiResult.ok("匿名訪問成功"); }
這是一段很普通的訪問接口,返回的結果如下:
{
"code": 200, "msg": "請求成功", "data": { "請求成功": "匿名訪問成功" }, "timestamp": "2020-07-19T23:07:07.738", "fail": false, "success": true }
這里大家只需要注意一下timestamp的序列化結果,timestamp是一個LocalDateTime
類型,在SpringMVC
中的消息轉換器對LocalDateTime
做序列化的時候沒有特殊處理,直接調用了LocalDateTime
的**toString()**方法,所以這個序列化結果中間有個T
。
但是如果這里的序列化用了其他方案,可能這個序列化結果會是不一樣的體驗,在我的項目中我也采用了Jackson
來做序列化(Spring
中也用的它),我們可以看看我們自己定義的一個JsonUtil對LocalDateTime
做序列化會是什么結果。
@Slf4j
public class JacksonUtil { public static ObjectMapper objectMapper = new ObjectMapper(); /** * Java對象轉JSON字符串 * * @param object * @return */ public static String toJsonString(Object object) { try { return objectMapper.writeValueAsString(object); } catch (JsonProcessingException e) { log.error("The JacksonUtil toJsonString is error : \n", e); throw new RuntimeException(); } } }
我們序列化工具類長這樣,和上面一樣,我們序列化一個ApiResult
看看會是什么結果:
{
"code": 400, "msg": "請求失敗", "timestamp": { "month": "JULY", "year": 2020, "dayOfMonth": 19, "hour": 23, "minute": 25, "monthValue": 7, "nano": 596000000, "second": 2, "dayOfYear": 201, "dayOfWeek": "SUNDAY", "chronology": { "id": "ISO", "calendarType": "iso8601" } }, "fail": true, "success": false }
在Jackson
默認的ObjectMapper
下序列化出來的結果就是這個鬼樣子,因為是序列化最后倒是轉化成字符串了,那這樣的數據前端如果拿到了肯定是不能正常轉成時間類型的,
LocalDateTime
只是一個縮影,哪怕對於字符串,不同的序列化配置也是有着不同的影響,字符串里面可能會有轉義字符,有引號,不同的方案出來的結果可能是不一樣的,
在實際項目中對第三方接口進行HTTP對接一般來說都是需要的,其中傳輸過去的數據一般會經過我們項目中JSON工具類的序列化為字符串之后再傳輸過去,如果序列化方案不同可能會在序列化過程中傳過去的數據不是我們想要的。
還有些接口是我們直接往HttpServeletResponse
里面寫數據,這種時候一般也是寫JSON數據,比如:
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setHeader("Cache-Control", "no-cache"); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().println(JacksonUtil.toJsonString(ApiResult.fail(authException.getMessage()))); response.getWriter().flush(); }
這里我用工具類直接去序列化這個ApiResult
,傳給前台的數據就會也出現上面例子中的情況,LocalDateTime
序列化結果不是我們想要的。
所以在項目中的序列化和Spring
中的序列化保持一致還是很有必要的。
2. 📃實操方案
上面說過了項目中保持序列化的一致性的必要性(我認為是必要的哈哈)。
那我們下面就可以說說如果去做這個一致性。
我們知道,如果你想要在Spring
的序列化中將你返回的那個對象某個LocalDateTime
類型變量進行序列化的話,很簡單,可以這樣:
public class ApiResult implements Serializable {
private static final Map<String, String> map = new HashMap<>(1); private int code; private String msg; private Object data; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime timestamp;
就很簡單的在這個變量上面加一個JsonFormat
注解就ok了,但這樣不是全局的,哪個變量加哪個變量就生效。
想做到全局生效,我們需要在Spring
的配置去修改Spring
中使用的ObjectMapper
,了解Jackson的小伙伴應該都知道,序列化的各種配置都在配置在這個ObjectMapper
中的,不知道也沒關系,你現在知道了。
那么我們可以通過去配置Spring
中的ObjectMapper
做到全局生效:
@Configuration
public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder -> { builder.locale(Locale.CHINA); builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss"); JavaTimeModule javaTimeModule = new JavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); builder.modules(javaTimeModule); }; } }
通過在Jackson2ObjectMapperBuilderCustomizer
之中加入一些序列化方案就可以達到這個效果,上文的代碼就是做了這些操作,這樣之后我們再次訪問最開始那個接口,就會出現如下效果:
{
"code": 200, "msg": "請求成功", "data": { "請求成功": "匿名訪問成功" }, "timestamp": "2020-07-20 00:06:12", "fail": false, "success": true }
timestamp
中間那個T不存在了,因為我們已經加入了LocalDateTime
的序列化方案了。
但是僅僅如此還不行,這只是做了LocalDateTime
的全局序列化,我們還需要讓自己的工具類也和Spring
的保持一致:
@Bean
@Primary @ConditionalOnMissingBean(ObjectMapper.class) public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); // 通過該方法對mapper對象進行設置,所有序列化的對象都將按改規則進行系列化 // Include.Include.ALWAYS 默認 // Include.NON_DEFAULT 屬性為默認值不序列化 // Include.NON_EMPTY 屬性為 空("") 或者為 NULL 都不序列化,則返回的json是沒有這個字段的 // Include.NON_NULL 屬性為NULL 不序列化 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 允許出現特殊字符和轉義符 objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); // 允許出現單引號 objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); /** * 將Long,BigInteger序列化的時候,轉化為String */ // SimpleModule simpleModule = new SimpleModule(); // // simpleModule.addSerializer(Long.class, ToStringSerializer.instance); // simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); // simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); // // objectMapper.registerModule(simpleModule); // 將工具類中的 objectMapper 換為 Spring 中的 objectMapper JacksonUtil.objectMapper = objectMapper; return objectMapper; }
這段代碼是緊跟上一步,對Jackson2ObjectMapperBuilder
builder出來的ObjectMapper
做一些操作,設置一系列自己想要的屬性。
代碼中注釋那一塊也是做一個序列化轉換,如果你的項目中用到了比較長的LONG類型數字,可能會導致JS拿不到完全的數字,因為java中的long類型要比JS的number類型長一點,這個時候你必須要轉換成String給前台,它才能拿到正確的數字,如果你有需要可以打開這一段。
最后一句就是我們比較關鍵的了,把builder出來的ObjectMapper
賦值給我們工具類中的ObjectMapper
,這樣的話它倆其實指向一個地址,也就是使用同一個對象進行序列化,所得出的結果當然就是相同的了。
后記
今天的從LocalDateTime序列化探討全局一致性序列化
就到這里了,希望對大家有所幫助。
本文的代碼我也放在之前的SpringSecruity
的demo中了,大家可以直接去里面搜索類名即可找到。
日拱一卒無有盡,功不唐捐終入海。
你們的每個點贊收藏與評論都是對我知識輸出的莫大肯定,如果有文中有什么錯誤或者疑點或者對我的指教都可以在評論區下方留言,一起討論。