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

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

楔子

前兩週發了三篇SpringSecurity一篇徵文,這周打算寫點簡單有用易上手的文章,換換腦子,休息一下。前端

今天要寫的是這篇:從LocalDateTime序列化來看全局一致性序列化體驗java

這個標題看起來蠻不像人話的,有種挺官方的感受,我先給你們翻譯翻譯咱們的主題是什麼:經過講解LocalDateTime的序列化從而引出整個項目中的全部序列化處理,並讓他們保持一致。git

在咱們項目中通常存在着兩種序列化,程序員

一個呢是SpringMVC官方的序列化,也就是Spring幫你作的序列化,好比你在一個接口上面打了一個ResponseBody註解,SpringMVC中的消息轉換器會幫你作序列化。github

另外一個就是咱們項目內的序列化,本身定義的JsonUtil也好,仍是你引入的第三方JSON處理工具(好比FastJson)也好,均可以說作是咱們項目內部的序列化。spring

這二者若是不同,有時候序列化出來的數據可能會出現結果不大同樣的結果,爲了防止這種狀況,今天咱們就來探討一下項目中的序列化。json

1. 💡舉個例子

咱們先來舉個例子,來看看若是序列化不一致會出現啥樣的效果。api

@GetMapping("/api/anon")
    public ApiResult test01() {
        return ApiResult.ok("匿名訪問成功");
    }

這是一段很普通的訪問接口,返回的結果以下:app

{
    "code": 200,
    "msg": "請求成功",
    "data": {
        "請求成功": "匿名訪問成功"
    },
    "timestamp": "2020-07-19T23:07:07.738",
    "fail": false,
    "success": true
}

這裏你們只須要注意一下timestamp的序列化結果,timestamp是一個LocalDateTime類型,在SpringMVC中的消息轉換器對LocalDateTime作序列化的時候沒有特殊處理,直接調用了LocalDateTimetoString()方法,因此這個序列化結果中間有個Tspring-boot

可是若是這裏的序列化用了其餘方案,可能這個序列化結果會是不同的體驗,在個人項目中我也採用了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地址

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

大家的每一個點贊收藏與評論都是對我知識輸出的莫大確定,若是有文中有什麼錯誤或者疑點或者對個人指教均可以在評論區下方留言,一塊兒討論。

我是耳朵,一個一直想作知識輸出的僞文藝程序員,下期見。

相關文章
相關標籤/搜索