• OpenFeign不支持{}特殊字符的header解决


    一、环境

        <properties>
            <spring.version>5.3.22spring.version>
            <spring-boot.version>2.7.3spring-boot.version>
            <spring-cloud.version>3.1.3spring-cloud.version>
            <spring-cloud-dependencies.version>2021.0.3spring-cloud-dependencies.version>
            <spring-cloud-starter-alibaba.version>2021.0.1.0spring-cloud-starter-alibaba.version>
        properties>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中feign包的版本号

          <dependency>
            <groupId>io.github.openfeigngroupId>
            <artifactId>feign-bomartifactId>
            <version>11.8version>
            <type>pomtype>
            <scope>importscope>
          dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    二、场景描述

    feign需要传递一些json格式的数据,代码如下

    @Slf4j
    public class AuthFeignInterceptor implements RequestInterceptor {
    
        @Override
        public void apply(RequestTemplate requestTemplate) {
            UserDetail userDetail = UserContext.getUserDetail();
    
            if (Objects.nonNull(appDetail)) {
                String userJson = JsonUtils.toJsonString(userDetail);
                requestTemplate.header(Constant.User.HEADER_NAME, userJson);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    三、 问题定位

    userJson携带了特殊符号$:断点调试的时候参数都是正常设置,尝试定位是生产者还是消费者的问题,使用postman模拟消费者调用,生产者可以正常收到信息并解析成功,那么问题就在消费者

    尝试源码断点

    
    public final class RequestTemplate implements Serializable {
        public RequestTemplate header(String name, String... values) {
          return header(name, Arrays.asList(values));
        }
    
      public RequestTemplate header(String name, Iterable<String> values) {
        if (name == null || name.isEmpty()) {
          throw new IllegalArgumentException("name is required.");
        }
        if (values == null) {
          values = Collections.emptyList();
        }
    
        return appendHeader(name, values);
      }
    
      private RequestTemplate appendHeader(String name, Iterable<String> values) {
        if (!values.iterator().hasNext()) {
          /* empty value, clear the existing values */
          this.headers.remove(name);
          return this;
        }
        if (name.equals("Content-Type")) {
          // a client can only produce content of one single type, so always override Content-Type and
          // only add a single type
          this.headers.remove(name);
          this.headers.put(name,
              HeaderTemplate.create(name, Collections.singletonList(values.iterator().next())));
          return this;
        }
        this.headers.compute(name, (headerName, headerTemplate) -> {
          if (headerTemplate == null) {
            return HeaderTemplate.create(headerName, values);
          } else {
            return HeaderTemplate.append(headerTemplate, values);
          }
        });
        return this;
      }
    }
    
    • 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

    可以看到最终是调用HeaderTemplate来实现header设置,继续查看HeaderTemplate源码

    public final class HeaderTemplate {
    
      public static HeaderTemplate create(String name, Iterable<String> values) {
        if (name == null || name.isEmpty()) {
          throw new IllegalArgumentException("name is required.");
        }
    
        if (values == null) {
          throw new IllegalArgumentException("values are required");
        }
    
        return new HeaderTemplate(name, values, Util.UTF_8);
      }
    
      private HeaderTemplate(String name, Iterable<String> values, Charset charset) {
        this.name = name;
    
        for (String value : values) {
          if (value == null || value.isEmpty()) {
            /* skip */
            continue;
          }
    
          this.values.add(
              new Template(
                  value,
                  ExpansionOptions.REQUIRED,
                  EncodingOptions.NOT_REQUIRED,
                  false,
                  charset));
        }
      }
    }
    
    
    
    • 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

    可以看到HeaderTemplate只是对Template进行封装

    
    public class Template {
    
      Template(
          String value, ExpansionOptions allowUnresolved, EncodingOptions encode, boolean encodeSlash,
          Charset charset) {
        if (value == null) {
          throw new IllegalArgumentException("template is required.");
        }
        this.template = value;
        this.allowUnresolved = ExpansionOptions.ALLOW_UNRESOLVED == allowUnresolved;
        this.encode = encode;
        this.encodeSlash = encodeSlash;
        this.charset = charset;
        // 解析${}占位符
        this.parseTemplate();
      }
    
      private void parseTemplate() {
    
         // 解析{}占位符
        this.parseFragment(this.template);
      }
    
      private void parseFragment(String fragment) {
        // 解析每个{}占位符
        ChunkTokenizer tokenizer = new ChunkTokenizer(fragment);
    
        while (tokenizer.hasNext()) {
          /* check to see if we have an expression or a literal */
          String chunk = tokenizer.next();
          // 如果占位符以{起始,则默认使用模板解析
          if (chunk.startsWith("{")) {
            Expression expression = Expressions.create(chunk);
            if (expression == null) {
              this.templateChunks.add(Literal.create(this.encodeLiteral(chunk)));
            } else {
              this.templateChunks.add(expression);
            }
          } else {
            this.templateChunks.add(Literal.create(this.encodeLiteral(chunk)));
          }
        }
      }
    }
    
    
    • 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

    到这里基本已经定位到问题是feign实现了高级特性,占位符和模板解析造成的问题,具体还是:的特殊处理,暂时不展开

    四、解决

    定位到了问题,尝试找解决方案

    4.1 占位符设置参数进行替换

    查看RestTemplate源码发现有一个resolve方法可以对uriTemplatequeriesheaders进行参数替换

    public RequestTemplate resolve(Map<String, ?> variables) {
    
        StringBuilder uri = new StringBuilder();
    
        /* create a new template form this one, but explicitly */
        RequestTemplate resolved = RequestTemplate.from(this);
    
        if (this.uriTemplate == null) {
          /* create a new uri template using the default root */
          this.uriTemplate = UriTemplate.create("", !this.decodeSlash, this.charset);
        }
    
        String expanded = this.uriTemplate.expand(variables);
        if (expanded != null) {
          uri.append(expanded);
        }
    
        /*
         * for simplicity, combine the queries into the uri and use the resulting uri to seed the
         * resolved template.
         */
        if (!this.queries.isEmpty()) {
          /*
           * since we only want to keep resolved query values, reset any queries on the resolved copy
           */
          resolved.queries(Collections.emptyMap());
          StringBuilder query = new StringBuilder();
          Iterator<QueryTemplate> queryTemplates = this.queries.values().iterator();
    
          while (queryTemplates.hasNext()) {
            QueryTemplate queryTemplate = queryTemplates.next();
            String queryExpanded = queryTemplate.expand(variables);
            if (Util.isNotBlank(queryExpanded)) {
              query.append(queryExpanded);
              if (queryTemplates.hasNext()) {
                query.append("&");
              }
            }
          }
    
          String queryString = query.toString();
          if (!queryString.isEmpty()) {
            Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri);
            if (queryMatcher.find()) {
              /* the uri already has a query, so any additional queries should be appended */
              uri.append("&");
            } else {
              uri.append("?");
            }
            uri.append(queryString);
          }
        }
    
        /* add the uri to result */
        resolved.uri(uri.toString());
    
        /* headers */
        if (!this.headers.isEmpty()) {
          /*
           * same as the query string, we only want to keep resolved values, so clear the header map on
           * the resolved instance
           */
          resolved.headers(Collections.emptyMap());
          for (HeaderTemplate headerTemplate : this.headers.values()) {
            /* resolve the header */
            String header = headerTemplate.expand(variables);
            if (!header.isEmpty()) {
              /* append the header as a new literal as the value has already been expanded. */
              resolved.header(headerTemplate.getName(), header);
            }
          }
        }
    
        if (this.bodyTemplate != null) {
          resolved.body(this.bodyTemplate.expand(variables));
        }
    
        /* mark the new template resolved */
        resolved.resolved = true;
        return resolved;
      }
    
    
    • 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
    • 81
    • 82

    但是RequestTemplate对象是新生成的,无法进行传递

    4.2 替换RequestTemplate的HeaderTemplate

    查看源码发现RequestTemplate并没有开发对HeaderTemplate直接注入方法,所有header都是使用header()方法进行处理,而对RequestTemplate的修改都是新生成一个RequestTemplate,放弃这个方案

    4.3 参数编码

    所有签名都是基于Base64进行字节数组编码,那么该方案的适应性是最好的,对于http协议支持最好,修改源码

    @Slf4j
    public class AuthFeignInterceptor implements RequestInterceptor {
    
        @Override
        public void apply(RequestTemplate requestTemplate) {
            UserDetail userDetail = UserContext.getUserDetail();
    
            if (Objects.nonNull(appDetail)) {
                String userJson = JsonUtils.toJsonString(userDetail);
                String encodeAppJson = Base64.encode(appJson);
                requestTemplate.header(Constant.User.HEADER_NAME, encodeAppJson);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    参数解析可以Base64.decode完成解析,还可以兼容中文,不要再进行UTF-8编码,如果考虑历史兼容性,可以先判断header是否以{起始,如果不是则使用Base64解析,如果考虑协议层的可变更性,可以在header中接入类似content-type的编码类型,来实现对变化的支持

    五、参考

    issue

  • 相关阅读:
    聊聊微服务架构思想
    在Linux上安装Oracle相关产品,常用文件的默认路径
    Elasticsearch Search API说明
    上海钢联朱军红:产业互联网的“双创”之路
    C++的string容器->基本概念、构造函数、赋值操作、字符串拼接、查找和替换、字符串比较、字符存取、插入和删除、子串
    R语言使用qcauchy函数生成柯西分布分位数函数数据、使用plot函数可视化柯西分布分位数函数数据(Cauchy distribution)
    java栈和自定义栈
    UI控件Telerik UI for WinForms新主题——VS2022启发式主题
    在线论坛系统
    学习太极创客 — MQTT 第二章(九)本章测试
  • 原文地址:https://blog.csdn.net/yu542771572/article/details/127827546