乐趣区

关于后端:从LocalDateTime序列化探讨全局一致性序列化

日拱一卒无有尽,功不唐捐终入海。

楔子

前两周发了三篇 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 地址

日拱一卒无有尽,功不唐捐终入海。

你们的每个点赞珍藏与评论都是对我常识输入的莫大必定,如果有文中有什么谬误或者疑点或者对我的指教都能够在评论区下方留言,一起探讨。

我是耳朵,一个始终想做常识输入的伪文艺程序员,下期见。

退出移动版