Feign调用GET方法,入参POJO对象有LocalDateTime属性遇到的问题和解决办法

4,301 阅读3分钟

情景复现

  • 当用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方法中完成的。

  1. BuildTemplateByResolvingArgs#create(Object[] argv)
  2. BuildTemplateByResolvingArgs#toQueryMap(Object value)
  3. 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请求包括都是经过序列化器序列化。我的项目用了全局序列化器,也可以自定义序列化器。