当前位置:   article > 正文

使用feign对x-www-form-urlencode类型的encode和decode问题_openfeign将逗号变为%2c

openfeign将逗号变为%2c

关键词: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方法不会保留逗号冒号等字符。

  1. private static String encodeChunk(String value, FragmentType type, Charset charset) {
  2. byte[] data = value.getBytes(charset);
  3. ByteArrayOutputStream encoded = new ByteArrayOutputStream();
  4. // 依次对每个byte编码
  5. for (byte b : data) {
  6. // 对于一些字符不进行编码
  7. if (type.isAllowed(b)) {
  8. encoded.write(b);
  9. } else {
  10. /* percent encode the byte */
  11. pctEncode(b, encoded);
  12. }
  13. }
  14. return new String(encoded.toByteArray());
  15. }
  16. boolean isAllowed(int c) {
  17. return this.isPchar(c) || (c == '/');
  18. }
  19. protected boolean isPchar(int c) {
  20. return this.isUnreserved(c) || this.isSubDelimiter(c) || c == ':' || c == '@';
  21. }
  22. protected boolean isUnreserved(int c) {
  23. return this.isAlpha(c) || this.isDigit(c) || c == '-' || c == '.' || c == '_' || c == '~';
  24. }
  25. protected boolean isAlpha(int c) {
  26. return (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z');
  27. }
  28. protected boolean isDigit(int c) {
  29. return (c >= '0' && c <= '9');
  30. }
  31. protected boolean isSubDelimiter(int c) {
  32. return (c == '!') || (c == '$') || (c == '&') || (c == '\'') || (c == '(') || (c == ')')
  33. || (c == '*') || (c == '+') || (c == ',') || (c == ';') || (c == '=');
  34. }

至于为什么服务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 

附上源码:

  1. // 2、SynchronousMethodHandler.invoke 
  2. public Object invoke(Object[] argv) throws Throwable {
  3. // 2-1、先实例化RequestTemplate 此处encode参数
  4. RequestTemplate template = buildTemplateFromArgs.create(argv);
  5. Retryer retryer = this.retryer.clone();
  6. while (true) {
  7. try {
  8. return executeAndDecode(template);
  9. } catch (RetryableException e) {
  10. try {
  11. retryer.continueOrPropagate(e);
  12. } catch (RetryableException th) {
  13. Throwable cause = th.getCause();
  14. if (propagationPolicy == UNWRAP && cause != null) {
  15. throw cause;
  16. } else {
  17. throw th;
  18. }
  19. }
  20. if (logLevel != Logger.Level.NONE) {
  21. logger.logRetry(metadata.configKey(), logLevel);
  22. }
  23. continue;
  24. }
  25. }
  26. }
  27. Object executeAndDecode(RequestTemplate template) throws Throwable {
  28. // 2-2、executeAndDecode方法,将RequestTemplate build为request
  29. Request request = targetRequest(template);
  30. if (logLevel != Logger.Level.NONE) {
  31. logger.logRequest(metadata.configKey(), logLevel, request);
  32. }
  33. Response response;
  34. long start = System.nanoTime();
  35. try {
  36. // 2-3、execute 执行 访问原程服务
  37. response = client.execute(request, options);
  38. } catch (IOException e) {
  39. if (logLevel != Logger.Level.NONE) {
  40. logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
  41. }
  42. throw errorExecuting(request, e);
  43. }
  44. long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
  45. boolean shouldClose = true;
  46. try {
  47. if (logLevel != Logger.Level.NONE) {
  48. response =
  49. logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
  50. }
  51. if (Response.class == metadata.returnType()) {
  52. if (response.body() == null) {
  53. return response;
  54. }
  55. if (response.body().length() == null ||
  56. response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
  57. shouldClose = false;
  58. return response;
  59. }
  60. // Ensure the response body is disconnected
  61. byte[] bodyData = Util.toByteArray(response.body().asInputStream());
  62. return response.toBuilder().body(bodyData).build();
  63. }
  64. if (response.status() >= 200 && response.status() < 300) {
  65. if (void.class == metadata.returnType()) {
  66. return null;
  67. } else {
  68. Object result = decode(response);
  69. shouldClose = closeAfterDecode;
  70. return result;
  71. }
  72. } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
  73. Object result = decode(response);
  74. shouldClose = closeAfterDecode;
  75. return result;
  76. } else {
  77. throw errorDecoder.decode(metadata.configKey(), response);
  78. }
  79. } catch (IOException e) {
  80. if (logLevel != Logger.Level.NONE) {
  81. logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
  82. }
  83. throw errorReading(request, response, e);
  84. } finally {
  85. if (shouldClose) {
  86. ensureClosed(response.body());
  87. }
  88. }
  89. }
  90. long elapsedTime(long start) {
  91. return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
  92. }
  93. Request targetRequest(RequestTemplate template) {
  94. // 此处会先执行拦截器
  95. for (RequestInterceptor interceptor : requestInterceptors) {
  96. interceptor.apply(template);
  97. }
  98. return target.apply(template);
  99. }
  100. Object decode(Response response) throws Throwable {
  101. try {
  102. // 2-4、将response decode 
  103. return decoder.decode(response, metadata.returnType());
  104. } catch (FeignException e) {
  105. throw e;
  106. } catch (RuntimeException e) {
  107. throw new DecodeException(response.status(), e.getMessage(), e);
  108. }
  109. }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/915798
推荐阅读
相关标签
  

闽ICP备14008679号