從LocalDateTime序列化探討全域性一致性序列化

和耳朵發表於2020-07-21
日拱一卒無有盡,功不唐捐終入海。

楔子

前兩週發了三篇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做序列化的時候沒有特殊處理,直接呼叫了LocalDateTimetoString()方法,所以這個序列化結果中間有個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;
    }

這段程式碼是緊跟上一步,對Jackson2ObjectMapperBuilderbuilder出來的ObjectMapper做一些操作,設定一系列自己想要的屬性。

程式碼中註釋那一塊也是做一個序列化轉換,如果你的專案中用到了比較長的LONG型別數字,可能會導致JS拿不到完全的數字,因為java中的long型別要比JS的number型別長一點,這個時候你必須要轉換成String給前臺,它才能拿到正確的數字,如果你有需要可以開啟這一段。

最後一句就是我們比較關鍵的了,把builder出來的ObjectMapper賦值給我們工具類中的ObjectMapper,這樣的話它倆其實指向一個地址,也就是使用同一個物件進行序列化,所得出的結果當然就是相同的了。

後記

今天的從LocalDateTime序列化探討全域性一致性序列化就到這裡了,希望對大家有所幫助。

本文的程式碼我也放在之前的SpringSecruity的demo中了,大家可以直接去裡面搜尋類名即可找到。

本文程式碼: 碼雲地址GitHub地址

日拱一卒無有盡,功不唐捐終入海。

你們的每個點贊收藏與評論都是對我知識輸出的莫大肯定,如果有文中有什麼錯誤或者疑點或者對我的指教都可以在評論區下方留言,一起討論。

我是耳朵,一個一直想做知識輸出的偽文藝程式設計師,下期見。

相關文章