优化项目中LocalDateTime类型的应用

本次优化的目标:

  1. Request、Response中不需要标注JSONField,序列化和反序列化时可自动完成Long类型到LocalDateTime类型的转换

  2. 当用对象接受参数中的传递的对象信息时,自动完成Long类型到LocalDateTime类型的转换,如下代码:

1
2
3
4
5
6

@Controller(/user)
public void postUser(User user){
    // todo something
}

  1. 实现调用fastjson的序列化和反序列化化方法是,自动完成LocalDateTime类型到Long类型的转换

  2. 实体类上无需标注typeHandler,即可完成在数据出库入库时自动完成Long类型和LocalDateTime类型的转换(只需配置mybatis-plus的type-handler-packages,技术含量不好,所以不整理了)

  3. Feign中的时间转换(临时加的这个目标,我只是将Fiegn的HttpMessageConverts换成了FastjsonHttpMessageConvert,我对Feign的了解还不深入,所以暂时不整理了)。

实现目标一

为了让实验环境了我们项目环境一致,我们进行如下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 清除掉默认的HttpMessageConvert
        converters.clear();

        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();

        fastJsonHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(
                MediaType.APPLICATION_JSON,
                MediaType.ALL));

        converters.add(fastJsonHttpMessageConverter);

    }
}

并准备如下的测试类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

@RestController
public class TestController {

    @Data
    public static class User {
        private String username;
        private String password;
        private LocalDateTime time;
    }

    @PostMapping("/createUser")
    public User createUser(@RequestBody User user) {
        return user;
    }
}

接下来进行如下实验:

  1. Request中不加JSONField,完成用户输入的Long型到LocalDateTime类型的转换(实验中不需要任何配置,即可完成该目标)。

  2. Response中不加入JSONFiled,可以将LocalDateTime类型转换成Long型。

第二个小实验

实验中,我们的请求和返回值分别如下:

请求:

1
2
3
4
5
6
7

{
    "username": "username",
    "password": "password",
    "time": 1626696114000
}

返回值:

1
2
3
4
5
6
7

{
    "password": "password",
    "time": "2021-07-19T20:01:54",
    "username": "username"
}

很显然time字段的返回目标并不符合我们的需求,我们期待该字段为number类型的时间戳。因此,我进行了如下配置(此处代码并不代表我最终的编码)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    // 清除掉默认的HttpMessageConvert
    converters.clear();

    FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();

    fastJsonHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(
            MediaType.APPLICATION_JSON,
            MediaType.ALL));
    
    SerializeConfig serializeConfig = SerializeConfig.globalInstance;

    // 添加处理LocalDateTime的处理器
    serializeConfig.put(LocalDateTime.class, (JSONSerializer serializer,
                                                Object object,
                                                Object fieldName,
                                                Type fieldType,
                                                int features) -> {

        LocalDateTime fieldValue = (LocalDateTime) object;

        // 针对1.2.79进行的调整
        if (object == null) {
            serializer.writeNull();
            return;
        }

        ZoneId systemDefaultZoneId = ZoneId.systemDefault();
        ZoneOffset offset = systemDefaultZoneId.getRules().getOffset(fieldValue);

        serializer.write(fieldValue.toInstant(offset).toEpochMilli());
    });

    FastJsonConfig fastJsonConfig = new FastJsonConfig();
    fastJsonConfig.setSerializeConfig(serializeConfig);

    converters.add(fastJsonHttpMessageConverter);

}

实验结果表明,response中的LocalDateTime能够按照我们的需求,序列化成long型的时间戳(这段代码写的并不是很好,很容易看出来,写这段代码对Fastjson理解不足)。

实现目标二

目标二和目标一看似是一样的,实际上千差万别。目标一中,请求数据放在请求体中,所以我们接受的时候必须使用@RequestBody,controller层方法由RequestMappingHandlerAdapter进行包装(我目前接触的几乎都是有这个适配器包装的),在RequestMappingHandlerAdapter中,经过层层处理后,我们寻找到了RequestResponseBodyMethodProcessor作为我们的参数处理器,然后由RequestResponseBodyMethodProcessor完成将请求体中的参数解析成我们方法中要的User对象。具体实现是根据MediaType寻找一个合适的HttpMessageConvert,所以我们只需要想办法在HttpMessageConvert做文章,就可以完成我们的目标。

目标二中,我们构建实体的数据都是通过请求参数传递进来的。前面大部分内容时和实现目标一时一致的,但是到了参数绑定阶段,我们寻找到的是ServletModeAttributeMethodProcessor。在这个处理器的resolveArgument方法中,会寻找一些convert或者formatter完成String类型到目标类型的转换(我还没有找到相应的源码,但是原理上应该是这样的)。所以解决这个问题的方案就是增加一些我们自己的Convert或者Formatter,为了将所有MVC的配置集中在一起,我选择了实现Formatter,具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addFormatter(new Formatter<LocalDateTime>() {

        @Override
        public String print(LocalDateTime object, Locale locale) {

            ZoneId systemDefaultZoneId = ZoneId.systemDefault();
            ZoneOffset offset = systemDefaultZoneId.getRules().getOffset(object);

            return String.valueOf(object.toInstant(offset).toEpochMilli());
        }

        @Override
        public LocalDateTime parse(String text, Locale locale) throws ParseException {

            long timestamp = Long.parseLong(text);

            Instant instant = Instant.ofEpochMilli(timestamp);
            ZoneId zone = ZoneId.systemDefault();

            return LocalDateTime.ofInstant(instant, zone);
        }
    });

    WebMvcConfigurer.super.addFormatters(registry);
}

在我实际开发中,通过请求参数构建一个请求对象的需求比较少,可能只有刚接触SpringBoot的同学会不小心忘记写@RequestBody导致用参数接受数据。

实现目标三

在进行目标一时,我已经很好的完成了该目标。在这个过程中,我意识到我对Fastjson的配置是全局的,所以不应该写在对MVC的配置类中,所以我仅仅进行了一些代码的重构,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

@Configuration
public class FastjsonConfig {

    @PostConstruct
    public void configFastjson() {
        SerializeConfig serializeConfig = SerializeConfig.globalInstance;

        // 添加处理LocalDateTime的处理器
        serializeConfig.put(LocalDateTime.class, (JSONSerializer serializer,
                                                  Object object,
                                                  Object fieldName,
                                                  Type fieldType,
                                                  int features) -> {
            LocalDateTime fieldValue = (LocalDateTime) object;

            // 针对1.2.79进行的调整
            if (object == null) {
                serializer.writeNull();
                return;
            }

            ZoneId systemDefaultZoneId = ZoneId.systemDefault();
            ZoneOffset offset = systemDefaultZoneId.getRules().getOffset(fieldValue);

            serializer.write(fieldValue.toInstant(offset).toEpochMilli());
        });
    }
}

我选择了用PostConstruct对Fastjson进行配置。实际上,这种全局配置我一直在担心一个问题,如果哪位同学在业务代码中不小心进行了该类的配置,那么这个功能类后续的表现将都不是我们想要的,这该怎么办呢?

实现目标四

目标四的实现也非常的简单,我们仅仅只需要将MyBatis-Plus的配置项中增加如下配置即可:

1
2
3
4

mybatis-plus:
  type-handlers-package: fun.junjie.mybatis.type

这时候,只要我们的字段为LocalDateTime类型,则会自动调用该TypeHandler,非常的优雅和舒服。

20220106后续

因为公司将fastjson的版本从1.2.60升级到1.2.79,导致原来的代码出现了错误,所以我也相应做了一些调整,调整已经呈现在代码中了,主要为如下内容:

1
2
3
4
5
6
7

// 针对1.2.79进行的调整
if (object == null) {
    serializer.writeNull();
    return;
}

完整的代码

完整的配置代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

package com.sdstc.core.configuration.web;

import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.validation.constraints.NotNull;
import java.text.ParseException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/**
 * 对Spring MVC进行配置:
 * 1.配置HttpMessageConverts
 * 2.配置针对LocalDateTime的转换器
 */
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final FastJsonHttpMessageConverter fastJsonHttpMessageConverter;

    /**
     * 配置HttpMessageConverts
     *
     * @param converters 当前SpringBoot实例已有的转换器
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 这部分代码还需要斟酌,需要确认下会不会影响到文件上传
        converters.clear();

        converters.add(fastJsonHttpMessageConverter);
    }

    /**
     * 配置针对LocalDateTime的转换器
     *
     * @param registry formatter注册中心
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new Formatter<LocalDateTime>() {
            @Override
            public String print(LocalDateTime localDateTime, Locale locale) {

                ZoneId systemDefaultZoneId = ZoneId.systemDefault();
                ZoneOffset offset = systemDefaultZoneId.getRules().getOffset(localDateTime);

                return String.valueOf(localDateTime.toInstant(offset).toEpochMilli());
            }

            @Override
            public LocalDateTime parse(String text, Locale locale) throws ParseException {

                long timestamp = Long.parseLong(text);

                Instant instant = Instant.ofEpochMilli(timestamp);
                ZoneId zone = ZoneId.systemDefault();

                return LocalDateTime.ofInstant(instant, zone);
            }
        });

        WebMvcConfigurer.super.addFormatters(registry);
    }

}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

package com.sdstc.core.configuration.fastjson;

import com.alibaba.fastjson.serializer.JSONSerializer;
import com.alibaba.fastjson.serializer.SerializeConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.sdstc.core.utils.FastJsonUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;

import javax.annotation.PostConstruct;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Arrays;


/**
 * 对Fastjson进行全局配置,使用其可以自动将LocalDateTime类型转换成Number类型时间戳
 *
 * @author wujj
 */
@Configuration
public class FastjsonConfiguration {
    @PostConstruct
    public void configFastjson() {
        SerializeConfig serializeConfig = SerializeConfig.globalInstance;

        // 添加处理LocalDateTime的处理器
        serializeConfig.put(LocalDateTime.class, (JSONSerializer serializer,
                                                  Object object,
                                                  Object fieldName,
                                                  Type fieldType,
                                                  int features) -> {
            LocalDateTime fieldValue = (LocalDateTime) object;

            // 针对1.2.79进行的调整
            if (object == null) {
                serializer.writeNull();
                return;
            }

            ZoneId systemDefaultZoneId = ZoneId.systemDefault();
            ZoneOffset offset = systemDefaultZoneId.getRules().getOffset(fieldValue);

            serializer.write(fieldValue.toInstant(offset).toEpochMilli());
        });
    }

    @Bean
    public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() {
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();

        // 配置FastJsonConfig
        FastJsonConfig config = new FastJsonConfig();

        config.setSerializerFeatures(
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteNullListAsEmpty,
                SerializerFeature.WriteNullStringAsEmpty,
                SerializerFeature.WriteDateUseDateFormat,
                SerializerFeature.DisableCircularReferenceDetect);

        fastJsonHttpMessageConverter.setFastJsonConfig(config);

        fastJsonHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(
                MediaType.APPLICATION_JSON,
                MediaType.ALL));

        return fastJsonHttpMessageConverter;
    }
}