使用feign对x-www-form-urlencode类型的encode和decode问题

关键词:urlencode、feign-core、RequestTemplate、x-www-form-urlencode、;\:@&=+$,#

记录一下开发过程中遇到的一个问题。

问题场景:使用feign调用另一服务b时,在feign-client包里跑单测能调用成功,在另一项目a引入该feign-client时使用同样的参数调用失败。content-type为application/x-www-form-urlencode  POST请求

问题原因:入参中有一个String,数据是jsonArray,包含","和":",在打印请求的参数发现,feign-client包里对参数encode之后,“,” 和“:"不变,而项目a调用feign-client对参数encode会把“,” 和“:"encode成%2C和%3A,导致服务b decode失败。

后来debug对比两次的不同点,发现关键点在于feign中生成的RequestTemplate不同;一步一步调试发现,feign-client包中 feign-core版本是10.2.3,项目a的feign-core版本是9.5.1,两者在生成RequestTemplate中底层对参数encode的方法不同,低版本使用的JDK1.8的URLEncode,高版本使用的feign里的UriUtils.encodeReserved。

feign.template.UriUtils.encodeReserved对参数编码时,会将参数列表中key-value的value分割为byte数组,然后依次对每个byte进行encode,根据isAllowed方法判断是否需要encode,pctEncode(b, encoded)方法是真正去encode的地方。下面的代码可以看到UriUtils.encodeReserved保留了字母数字逗号冒号等字符。而java.net.URLEncode的encode方法不会保留逗号冒号等字符。

private static String encodeChunk(String value, FragmentType type, Charset charset) {
    byte[] data = value.getBytes(charset);
    ByteArrayOutputStream encoded = new ByteArrayOutputStream();
    // 依次对每个byte编码
    for (byte b : data) {
      // 对于一些字符不进行编码
      if (type.isAllowed(b)) {
        encoded.write(b);
      } else {
        /* percent encode the byte */
        pctEncode(b, encoded);
      }
    }
    return new String(encoded.toByteArray());
  }

boolean isAllowed(int c) {
        return this.isPchar(c) || (c == '/');
      }

protected boolean isPchar(int c) {
      return this.isUnreserved(c) || this.isSubDelimiter(c) || c == ':' || c == '@';
    }

protected boolean isUnreserved(int c) {
      return this.isAlpha(c) || this.isDigit(c) || c == '-' || c == '.' || c == '_' || c == '~';
    }

protected boolean isAlpha(int c) {
      return (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z');
    }

protected boolean isDigit(int c) {
      return (c >= '0' && c <= '9');
    }

protected boolean isSubDelimiter(int c) {
      return (c == '!') || (c == '$') || (c == '&') || (c == '\'') || (c == '(') || (c == ')')
          || (c == '*') || (c == '+') || (c == ',') || (c == ';') || (c == '=');
    }

至于为什么服务b对URLEncode编码的参数解析不了,还待探索,因为我没看服务b的decode代码,不知道服务b是怎么解析的。

由于服务b已经对多方提供,不能让他们适应低版本去增加解决方案(事实上他们也不想动代码),所以只能从发起方来解决问题。

可能的解决办法(没来得及尝试):

1、版本升级,将项目a的feign-core版本升级到10.2.3,问题能解决(已尝试),但是项目a中已经使用低版本的feign与多个服务交互,虽然理论上feign会向下兼容,但是我不敢轻易升级版本,而且版本号跨度还挺大,风险太大 = =。

2、将高版本的encode方法提取出来,手动配置到feign.encode中  

3、加一个interceptor,将低版本encode的template再特殊decode一次,保持和高版本的一致 (失败,template属性是unModifiable)

4、看能否让项目a调用b服务时使用高版本feign-core ,其他feign仍然使用低版本

5、放弃feign 用 httpclient调用 。。。。

 

附:feign的调用栈

1、 ReflectiveFeign 被反射实例化

2、SynchronousMethodHandler.invoke 

      2-1、先实例化RequestTemplate 此处encode参数

      2-2、executeAndDecode方法,将RequestTemplate build为request,此处会先执行拦截器

      2-3、execute 执行 访问原程服务

      2-4、将response decode 

附上源码:

// 2、SynchronousMethodHandler.invoke 
public Object invoke(Object[] argv) throws Throwable {
    // 2-1、先实例化RequestTemplate 此处encode参数
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

  Object executeAndDecode(RequestTemplate template) throws Throwable {
    // 2-2、executeAndDecode方法,将RequestTemplate build为request
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      // 2-3、execute 执行 访问原程服务
      response = client.execute(request, options);
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    boolean shouldClose = true;
    try {
      if (logLevel != Logger.Level.NONE) {
        response =
            logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
      }
      if (Response.class == metadata.returnType()) {
        if (response.body() == null) {
          return response;
        }
        if (response.body().length() == null ||
            response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          return response;
        }
        // Ensure the response body is disconnected
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        return response.toBuilder().body(bodyData).build();
      }
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          Object result = decode(response);
          shouldClose = closeAfterDecode;
          return result;
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
        Object result = decode(response);
        shouldClose = closeAfterDecode;
        return result;
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }

  long elapsedTime(long start) {
    return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
  }

  Request targetRequest(RequestTemplate template) {
    // 此处会先执行拦截器
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
    return target.apply(template);
  }

  Object decode(Response response) throws Throwable {
    try {
      // 2-4、将response decode 
      return decoder.decode(response, metadata.returnType());
    } catch (FeignException e) {
      throw e;
    } catch (RuntimeException e) {
      throw new DecodeException(response.status(), e.getMessage(), e);
    }
  }

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`feign-spring-mvc-starter` 是一个 Feign 的扩展,它支持使用 Spring MVC 注解来定义和调用 REST 服务。使用 `feign-spring-mvc-starter`,你可以像使用 Spring MVC 控制器一样定义 Feign 客户端,从而更方便地进行 REST 服务的开发。 在使用 `feign-spring-mvc-starter` 之前,你需要先了解 Feign 和 Spring MVC 的基本概念和用法。 Feign 是一个声明式的 Web 服务客户端,它可以帮助你更方便地定义和调用 REST 服务。Feign 的基本使用方法是定义一个接口,用于描述 REST 服务的 API,然后使用 Feign 注解来声明这个接口。 Spring MVC 是一个基于 Java 的 Web 框架,它提供了一组注解和 API,用于处理 Web 请求和响应。 `feign-spring-mvc-starter` 将 Feign 和 Spring MVC 结合起来,使你可以使用 Spring MVC 注解来定义和调用 REST 服务。使用 `feign-spring-mvc-starter`,你可以更方便地使用 Feign 来调用 REST 服务。 以下是一个使用 `feign-spring-mvc-starter` 的示例: 1. 添加 Maven 依赖 在 pom.xml 文件中添加以下依赖项: ```xml <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-spring-mvc</artifactId> <version>5.3.1</version> </dependency> ``` 2. 定义 Feign 接口 定义一个 Feign 接口,用于描述 REST 服务的 API。例如: ```java @FeignClient(name = "example-service") public interface ExampleClient { @GetMapping("/example") String getExample(); } ``` 在这个接口中,我们使用了 `@FeignClient` 注解来声明这个接口是一个 Feign 客户端,并指定了服务的名称。然后,我们定义了一个 `getExample()` 方法,用于调用 example-service 服务的 /example 路径。 3. 定义 Spring MVC 控制器 定义一个 Spring MVC 控制器,用于处理来自客户端的请求。例如: ```java @RestController public class ExampleController { private final ExampleClient exampleClient; public ExampleController(ExampleClient exampleClient) { this.exampleClient = exampleClient; } @GetMapping("/") public String index() { return exampleClient.getExample(); } } ``` 在这个控制器中,我们注入了 `ExampleClient`,并在 `index()` 方法中使用它来调用 example-service 服务的 /example 路径。 4. 运行应用程序 现在,你可以运行应用程序并访问 http://localhost:8080/ ,你应该会看到来自 example-service 服务的响应。 这就是一个使用 `feign-spring-mvc-starter` 的示例。使用 `feign-spring-mvc-starter`,你可以更方便地使用 Feign 来调用 REST 服务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值