Feign调用GET方法,入参POJO对象有LocalDat

情景复现

  • 当用Feign调用另外一个服务的GET方法,入参POJO对象有数据类型为LocalDateTime的属性时;
1
2
3
java复制代码    // 服务调用方入参 OrderReq 类中有一个属性为 LocalDateTime 
@RequestMapping(value = "find-all", method = RequestMethod.GET)
ServerResponse<PageImpl<OrderDTO>> findAll(@SpringQueryMap OrderReq req);

url 经过解码之后,会莫名其妙的转成默认的ISO-8601的日期格式即中间多了个T

/find-all?startTime=2020-05-23T23:23:23

这是我的全局配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码        @Bean
public Jackson2ObjectMapperBuilderCustomizer customJackson() {
return jacksonObjectMapperBuilder -> {
//若POJO对象的属性值为null,序列化时不进行显示
/*jacksonObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL);*/

//针对于Date类型,文本格式化
jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");

//针对于JDK新时间类。序列化时带有T的问题,自定义格式化字符串
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
jacksonObjectMapperBuilder.modules(javaTimeModule);
};
}

当时尝试了很多办法,被调用方还是接不到数据类型为LocalDateTime的属性。

解决办法

经过和同事的讨论以及同事 debugger feign 对GET方法的实现。解决方案如下:

  • 方法一:把 GET 方法变成 POST ,这是最简单的。(手动狗头)
  • 方法二:加上 @org.springframework.format.annotation.DateTimeFormat 注解
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
java复制代码// 方法一:把 GET 方法变成 POST ,这是最简单的。(手动狗头)
// 方法二:加上 @org.springframework.format.annotation.DateTimeFormat 注解。

enum ISO {
/**
* The most common ISO Date Format {@code yyyy-MM-dd},
* e.g. "2000-10-31".
*/
DATE,

/**
* The most common ISO Time Format {@code HH:mm:ss.SSSXXX},
* e.g. "01:30:00.000-05:00".
*/
TIME,

/**
* The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX},
* e.g. "2000-10-31T01:30:00.000-05:00".
* <p>This is the default if no annotation value is specified.
*/
DATE_TIME,

/**
* Indicates that no ISO-based format pattern should be applied.
*/
NONE
}
@ApiModelProperty(value = "开始时间", name = "startTime")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime startTime;

解析

知识点:spring 框架提供的org.springframework.format.annotation.DateTimeFormat和 jackson 提供的com.fasterxml.jackson.annotation.JsonFormat

feign 对 GET 方法的处理

feign.Feign中的静态内部类Builder中的 queryMapEncoder 属性。FieldQueryMapEncoder实现了QueryMapEncoder接口并实现了Map<String, Object> encode(Object object)方法。这个方法的作用是把GET方法的入参变成Map<String, Object>格式。这个方法仅在BuildTemplateByResolvingArgs#toQueryMap方法中被引用。

1
java复制代码private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();

重点是BuildTemplateByResolvingArgs#addQueryMapQueryParameters的这个方法。从这个方法中可以看到Map<String, Object>被遍历解析并判断数据类型currValue instanceof Iterable<?>是不是可迭代的。最终所有的参数都是通过currValue.toString()方法被解析的。这也就是为什么LocalDateTime数据类型的属性被解析成url字符串中间带了一个T所以最终的解决办法也就是加上注解@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)

整个的调用流程是:其实都是在create方法中完成的。

  1. BuildTemplateByResolvingArgs#create(Object[] argv)
  2. BuildTemplateByResolvingArgs#toQueryMap(Object value)
  3. BuildTemplateByResolvingArgs#addQueryMapQueryParameters(Map<String, Object> queryMap, RequestTemplate mutable)
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
java复制代码@SuppressWarnings("unchecked")
private RequestTemplate addQueryMapQueryParameters(Map<String, Object> queryMap,
RequestTemplate mutable) {
for (Entry<String, Object> currEntry : queryMap.entrySet()) {
Collection<String> values = new ArrayList<String>();

boolean encoded = metadata.queryMapEncoded();
Object currValue = currEntry.getValue();
if (currValue instanceof Iterable<?>) {
Iterator<?> iter = ((Iterable<?>) currValue).iterator();
while (iter.hasNext()) {
Object nextObject = iter.next();
values.add(nextObject == null ? null
: encoded ? nextObject.toString()
: UriUtils.encode(nextObject.toString()));
}
} else {
values.add(currValue == null ? null
: encoded ? currValue.toString() : UriUtils.encode(currValue.toString()));
}

mutable.query(encoded ? currEntry.getKey() : UriUtils.encode(currEntry.getKey()), values);
}
return mutable;
}

@Override
public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = RequestTemplate.from(metadata.template());
mutable.feignTarget(target);
if (metadata.urlIndex() != null) {
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
mutable.target(String.valueOf(argv[urlIndex]));
}
Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
int i = entry.getKey();
Object value = argv[entry.getKey()];
if (value != null) { // Null values are skipped.
if (indexToExpander.containsKey(i)) {
value = expandElements(indexToExpander.get(i), value);
}
for (String name : entry.getValue()) {
varBuilder.put(name, value);
}
}
}

RequestTemplate template = resolve(argv, mutable, varBuilder);
if (metadata.queryMapIndex() != null) {
// add query map parameters after initial resolve so that they take
// precedence over any predefined values
Object value = argv[metadata.queryMapIndex()];
Map<String, Object> queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
}

if (metadata.headerMapIndex() != null) {
template =
addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
}

return template;
}

private Map<String, Object> toQueryMap(Object value) {
if (value instanceof Map) {
return (Map<String, Object>) value;
}
try {
// 就是在这里被引用了
return queryMapEncoder.encode(value);
} catch (EncodeException e) {
throw new IllegalStateException(e);
}
}

POST 方法

jackson序列化,post请求包括都是经过序列化器序列化。我的项目用了全局序列化器,也可以自定义序列化器。

@ResponseBody 响应

jackson序列化,post请求包括都是经过序列化器序列化。我的项目用了全局序列化器,也可以自定义序列化器。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%