情景复现
- 当用Feign调用另外一个服务的
GET
方法,入参POJO对象有数据类型为LocalDateTime的属性时;
// 服务调用方入参 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
这是我的全局配置
@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 注解
// 方法一:把 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
方法中被引用。
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
方法中完成的。
- BuildTemplateByResolvingArgs#create(Object[] argv)
- BuildTemplateByResolvingArgs#toQueryMap(Object value)
- BuildTemplateByResolvingArgs#addQueryMapQueryParameters(Map<String, Object> queryMap, RequestTemplate mutable)
@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请求包括都是经过序列化器序列化。我的项目用了全局序列化器,也可以自定义序列化器。