关于feign对x-www-form-urlencode类型的encode和decode问题

目录
  • 对x-www-form-urlencode类型的encode和decode问题
    • 问题场景
    • 问题原因
    • 可能的解决办法(没来得及尝试)
    • 附:feign的调用栈
  • feignx-www-form-urlencoded类型请求
    • 试了好多方式,最后用以下方式成功

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

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

问题场景

使用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);
    }
  }

feign x-www-form-urlencoded 类型请求

spring发送 content-type=application/x-www-form-urlencoded 和普通请求不太一样。

试了好多方式,最后用以下方式成功

@FeignClient(
    name = "ocr-api",
    url = "${orc.idcard-url}",
    fallbackFactory = OcrClientFallbackFactory.class
)
public interface OcrClient {
 
    @PostMapping(
        value = "/v1/demo/idcard",
        headers = {"content-type=application/x-www-form-urlencoded"}
    )
    OcrBaseResponse<IdCardResponse> getIdCarInfo(@RequestBody MultiValueMap<String, Object> request);
}

Post请求,参数使用@RequestBody 并且使用 MultiValueMap。

    // 测试代码
    @Resource
    private OcrClient ocrClient;
    @GetMapping("getIdCardInfo")
    public Message getIdCardInfo() {
        MultiValueMap<String, Object> req = new LinkedMultiValueMap<>();
        req.add("request_id", 12343531123L);
        req.add("img_url", "xxx.jpg");
        req.add("source", -1);
        req.add("out_business_id", 1321434234L);
        OcrBaseResponse<IdCardResponse> idCarInfo = ocrClient.getIdCarInfo(req);
        return Message.success(idCarInfo);
    }

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Java 如何实现POST(x-www-form-urlencoded)请求

    目录 Java POST(x-www-form-urlencoded)请求 1.引入maven包 2.代码实现 3.POSTMAN参数组装 使用post 请求x-www-form-urlencoded格式数据 Java POST(x-www-form-urlencoded)请求 平时都是喜欢用JSON,这种也是第一次.这两种的区别就是传递参数类型不一样.废话不多说,直接上代码 1.引入maven包 <dependency> <groupId>commons-httpclient&l

  • 使用Spring处理x-www-form-urlencoded方式

    目录 Spring处理x-www-form-urlencoded方式 关于application/x-www-form-urlencoded编码 Spring处理x-www-form-urlencoded方式 最近在重写一个项目时遇到了许多奇葩问题,这个项目是一个简单的web后台项目,基本上全都是增删改查数据库的操作.这里面遇到几个用spring接收前端post请求的接口. 基本情况是post请求有四种data参数格式,这些基础知识在我另一片博文中提到过这里就不废话了.主要是因为前端有两个地方用

  • 关于feign对x-www-form-urlencode类型的encode和decode问题

    目录 对x-www-form-urlencode类型的encode和decode问题 问题场景 问题原因 可能的解决办法(没来得及尝试) 附:feign的调用栈 feignx-www-form-urlencoded类型请求 试了好多方式,最后用以下方式成功 对x-www-form-urlencode类型的encode和decode问题 记录一下开发过程中遇到的一个问题. 问题场景 使用feign调用另一服务b时,在feign-client包里跑单测能调用成功,在另一项目a引入该feign-cli

  • js将网址转为urlencode类型

    调用方法,将下面函数写在一个js文件,支持调用 将要encode 的网址改 function UrlEncode(str) { return transform(str); } function transform(s) { var hex='' var i,j,t j=0 for (i=0; i 65535) { return ("err!") } first = Math.round(num/4096 - .5); temp1 = num - first * 4096; secon

  • 使用feign传递参数类型为MultipartFile的问题

    目录 feign传递参数类型为MultipartFile 引入maven依赖 加入配置类 在feign客户端进行配置 feign传参MultipartFile问题解决 首先引入依赖 新建feign的配置 在feign接口中配置 feign传递参数类型为MultipartFile feign默认是不支持多媒体文件类型的文件传输的,但是可以通过引入第三方jar包解决这个问题,步骤可以分为三步. 引入maven依赖         <dependency>             <group

  • Spring Cloud使用Feign实现Form表单提交的示例

    之前,笔者写了<使用Spring Cloud Feign上传文件>.近日,有同事在对接遗留的Struts古董系统,需要使用Feign实现Form表单提交.其实步骤大同小异,本文附上步骤,算是对之前那篇的补充. 添加依赖: <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form</artifactId> <version>

  • vue form 表单提交后刷新页面的方法

    最近做的项目中,有增删改表格功能,在操作后需要实时更新页面数据.下面可以这样解决. 1.在methods中 定义好一个初始化渲染实例. 例如 lnitializationData(){//初始化页面数据 this.$http.get("/permit/specific", { params: { page: this.localPage, size: this.msg.pagNumber, } }, {emulateJSON: true} ).then((response) =>

  • feign实现传递参数的三种方式小结

    需要注意的一点是,feign好像是无法传递list集合类型的,但是你可以通过传递对象类型,然后在接收方再次将对象装在集合中达到集合传递的效果 传递方式一:传递的都是基本数据类型 restful风格参数,用@PathVariable写着走就行了 传递方式二:传递数组类型的参数 不使用restful风格,直接用@RequestParam声明参数之间的对应关系. 传递方式三:传递带有对象的参数 1.使用restful风格的参数要用@Pathvarible声明参数对应关系,@Pathvariable用于

  • Springcloud feign传日期类型参数报错的解决方案

    目录 feign传日期类型参数报错 Date类型参数报错 LocalDate类型报错 feign传参问题及传输Date类型参数时差的坑 下面说说两种解决方案 feign传参时候使用@DateTimeFormat注解的坑 feign传日期类型参数报错 Date类型参数报错 在Spring cloud feign接口中传递Date类型参数时报错,报错信息. 场景: 客户端传递一个new Date()的参数,服务端接受的参数和客户端有时间差. 客户端打印格式化的new Date(): 2018-05-

  • SpringCloud修改Feign日志记录级别过程浅析

    目录 前言 1. 介绍 2. 方式一 3. 方式二 前言 本次示例代码的文件结构如下图所示. 1. 介绍 Feign 允许我们自定义配置,下面是 Feign 可以修改的配置. 类型 作用 说明 feign.Logger.Level 修改日志级别 包含四种不同级别:NONE.BASIC.HEADERS.FULL feign.codec.Decoder 响应结果的解析器 HTTP 远程调用的结果做解析,例如解析 JSON 字符串反序列化成 Java 对象 feign.codec.Encoder 请求

  • php的urlencode()URL编码函数浅析

    URLEncode的方式一般有两种,一种是传统的基于GB2312的Encode(Baidu.Yisou等使用),另一种是基于UTF-8的Encode(Google.Yahoo等使用). 本工具分别实现两种方式的Encode与Decode: 中文 -> GB2312的Encode -> %D6%D0%CE%C4 中文 -> UTF-8的Encode -> %E4%B8%AD%E6%96%87 Html中的URLEncode: 编码为GB2312的html文件中:http://s.jb

  • Swift如何使用类型擦除及自定义详解

    前言 在 Swift 的世界中,如果我们将协议称之为国王,那么泛型则可以视作皇后,所谓一山不容二虎,当我们把这两者结合起来使用的时候,似乎会遇到极大的困难.那么是否有一种方法,能够将这两个概念结合在一起,以便让它们成为我们前进道路上的垫脚石,而不是碍手碍脚的呢?答案是有的,这里我们将会使用到类型擦除 (Type Erasure) 这个强大的特性. 你也许曾听过类型擦除,甚至也使用过标准库提供的类型擦除类型如 AnySequence.但到底什么是类型擦除? 如何自定义类型擦除? 在这篇文章中,我将

随机推荐