• 当用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.
    @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请求包括都是经过序列化器序列化。我的项目用了全局序列化器,也可以自定义序列化器。

    分类:
    后端
  • 仲文1992
    15小时前
  •