本次优化的目标:
-
Request、Response中不需要标注JSONField,序列化和反序列化时可自动完成Long类型到LocalDateTime类型的转换
-
当用对象接受参数中的传递的对象信息时,自动完成Long类型到LocalDateTime类型的转换,如下代码:
1
2
3
4
5
6
|
@Controller(/user)
public void postUser(User user){
// todo something
}
|
-
实现调用fastjson的序列化和反序列化化方法是,自动完成LocalDateTime类型到Long类型的转换
-
实体类上无需标注typeHandler,即可完成在数据出库入库时自动完成Long类型和LocalDateTime类型的转换(只需配置mybatis-plus的type-handler-packages,技术含量不好,所以不整理了)
-
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;
}
}
|
接下来进行如下实验:
-
Request中不加JSONField,完成用户输入的Long型到LocalDateTime类型的转换(实验中不需要任何配置,即可完成该目标)。
-
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;
}
}
|