Spring Cloud Feign 如何使用对象参数

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud Feign 如何使用对象参数相关的知识,希望对你有一定的参考价值。

参考技术A Spring Cloud Feign 用于微服务的封装,通过接口代理的实现方式让微服务调用变得简单,让微服务的使用上如同本地服务。但是它在传参方面不是很完美。在使用 Feign 代理 GET 请求时,对于简单参数(基本类型、包装器、字符串)的使用上没有困难,但是在使用对象传参时却无法自动的将对象包含的字段解析出来。

对象传参是很常见的操作,虽然可以通过一个个参数传递来替代,但是那样就太麻烦了,所以必须解决这个问题。

我在网上看到有人用 @RequestBody 来注解对象参数,我在尝试后发现确实可用。这个方案实际使用 body 体装了参数(使用的是 GET 请求),但是这个方案有些问题:

于是我继续寻找答案,发现可以使用 @SpringQueryMap 仅添加在 consumer 的参数上就能自动对 Map 类型参数编码再拼接到 URL 上。而我用的高版本的 Feign,可以直接把对象编码。

可是正当我以为得到正解时,却发现还是有问题:

我明明在 Date 类型的字段上加上了 @DateTimeFormat(pattern = "yyyy-MM-dd") ,却没有生效,他用自己的方式进行了编码(或者说序列化),而且官方确实没有提供这种格式化方式。

又一番找寻后发现了一位大佬自己实现了一个注解转换替代 @SpringQueryMap,并实现了丰富的格式化功能 ORZ(原文链接: Spring Cloud Feign实现自定义复杂对象传参 ),只能说佩服佩服。但是我没有那样的技术,又不太想复制粘贴他那一大堆的代码,因为出了问题也不好改,所以我还是想坚持最大限度地使用框架,最小限度的给框架填坑。

终于功夫不费有心人,我发现了 Feign 预留的自定义编码器接口 QueryMapEncoder,框架提供了两个实现:

虽然这两个实现不能满足我的要求,但是只要稍加修改写一个自己的实现类就行了,于是我在 FieldQueryMapEncoder 的基础上修改,仅仅添加了一个方法,小改了一个方法就实现了功能。

原理:Feign 其实还是用 Map 进行的编码,编码方式也很简单,String 是 key,Object 是 value。最开始的方式就是用 Object 的 toString() 方法把参数编码,这也是为什么 Date 字段会变成一个默认的时间格式,因为 toString() 根本和 @DateTimeFormat 没有关系。而高版本使用编码器实现了对象传参,实际实际上是通过简单的反射获取对象的元数据,再放到 Map 中。

上面的原理都能从 @DateTimeFormat 的注释和编码器的源码中得到答案。

我们要做的就是自定义一个编码器,实现在元数据放入 Map 之前根据需要把字段变成我们想要的字符串。下面是我实现的代码,供参考:

加注释的方法,就是我后添加进去的。encode 方法的最后一行稍微修改了一下,引用了我加的方法,其他都是直接借鉴过来的(本来我想更偷懒,直接继承一下子,但是它用了私有的内部类导致我只能全部复制粘贴了)。

spring cloud——feign为GET请求时的对象参数传递

一、问题重现


楼主在使用feign进行声明式服务调用的时候发现,当GET请求为多参数时,为方便改用DTO对象进行参数传递。但是,在接口调用时feign会抛出一个405的请求方式错误:

{"timestamp":1540713334390,"status":405,"error":"Method Not Allowed", "exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method \'POST\' not supported","path":"/role/get"}

 API接口层代码如下:

@RequestMapping(value = "/role")
public interface RoleService {
    @GetMapping(value = "/get")
    Result<RuleInfoVO> getRoleInfo(RoleInfoRequest request);

}

服务端实现:

@Slf4j
@RestController
public class RoleInfoResource implements RoleService {

    @Override
    public Result<RuleInfoVO> getRoleInfo(RoleInfoRequest request) {
        log.info("params of getRoleInfo:{}",request);
        return new Result<>();
    }
}

feign客户端调用:

@Slf4j
@RestController
@RequestMapping(value = "/role")
public class RoleController {

    @Autowired
    private RoleInfoApi roleInfoApi;

    @GetMapping(value = "/get")
    public Result<RuleInfoVO> getRuleInfo(RoleInfoRequest request){
        log.info("params of getRuleInfo:{}",request);
        Result<RuleInfoVO> result = this.roleInfoApi.getRoleInfo(request);
        log.info("result of getRuleInfo:{}",result);
        return result;
    }
}

检查feign调用方式与服务端所声明的方式一致,但是为什么为抛出405异常?带着该疑问稍微跟了一下源码,发现feign默认的远程调用使用的是jdk底层的HttpURLConnection,这在feign-core包下的Client接口中的convertAndSend方法可看到:

if (request.body() != null) {
        if (contentLength != null) {
          connection.setFixedLengthStreamingMode(contentLength);
        } else {
          connection.setChunkedStreamingMode(8196);
        }
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
        if (gzipEncodedRequest) {
          out = new GZIPOutputStream(out);
        } else if (deflateEncodedRequest) {
          out = new DeflaterOutputStream(out);
        }
        try {
          out.write(request.body());
        } finally {
          try {
            out.close();
          } catch (IOException suppressed) { // NOPMD
          }
        }
      }

该段代码片段会判断requestBody是否为空,我们知道GET请求默认是不会有requestBody的,因此该段代码会执行到HttpURLConnection中的 private synchronized OutputStream getOutputStream0() throws IOException; 方法:

1 if (this.method.equals("GET")) {
2                     this.method = "POST";
3 }

最关键的代码片段已显示当请求方式为GET请求,会将该GET请求修改为POST请求,这也就是4返回05状态的根本原因。

二、解决方案


幸运的是,feign为我们提供了相应的配置解决方案。我们只需将feign底层的远程调用由HttpURLConnection修改为其他远程调用方式即可。而且基本不需要修改太多的代码,只需再依赖中加入feign-httpclient包的依赖,并在@RequestMapping注解中加入consumes的属性即可:

1 compile \'io.github.openfeign:feign-httpclient:9.5.1\'

楼主用的gradle,使用maven的大佬请自行更改为maven的配置方式。

增加@GetMapping注解的consumes属性,使用@RequestMapping的大佬也一样:

1 @RequestMapping(value = "/role")
2 public interface RoleService {
3 
4 
5     @GetMapping(value = "/get",consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
6     Result<RuleInfoVO> getRoleInfo(@RequestBody RoleInfoRequest request);
7 
8 }

大功告成:

 

源代码请各位大佬移步:https://github.com/LJunChina/cloud-solution-staging

 

以上是关于Spring Cloud Feign 如何使用对象参数的主要内容,如果未能解决你的问题,请参考以下文章

spring cloud——feign为GET请求时的对象参数传递

使用 Spring Cloud Open Feign 获取 JSON 中的对象列表

Spring Cloud Feign实现自定义复杂对象传参

如何使用 spring-cloud-netflix 和 feign 编写集成测试

如何使用 Spring Cloud 将 Hystrix 属性设置为 Feign 请求?

如何使用 Spring Cloud Feign 发布表单 URL 编码的数据