概述

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

如果你没耐心看完,直接跳到最后一个标题跟着操作就行了。

@RequestBody

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

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

  1. 注解需要在 consumer 和 provider 两边都有,这造成了麻烦
  2. 使用接口测试工具 Postman 无法跑通微服务,后来发现是因为 body 体的格式选择不正确,这个格式不是通常的表单或者路径拼接,而是 GraphQL。我没有研究过这种格式应该如何填写参数,但是 Postman 上并没有给出像表单那样方便的格式,这对于测试是很不利的。

@SpringQueryMap

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

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

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

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

QueryMapEncoder

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

  • FieldQueryMapEncoder
  • BeanQueryMapEncoder

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

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

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

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

package com.example.billmanagerfront.config.encoder;

import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import org.springframework.format.annotation.DateTimeFormat; import feign.Param;
import feign.QueryMapEncoder;
import feign.codec.EncodeException; public class PowerfulQueryMapEncoder implements QueryMapEncoder {
private final Map<Class<?>, ObjectParamMetadata> classToMetadata = new ConcurrentHashMap<>(); @Override
public Map<String, Object> encode(Object object) throws EncodeException {
ObjectParamMetadata metadata = classToMetadata.computeIfAbsent(object.getClass(),
ObjectParamMetadata::parseObjectType); return metadata.objectFields.stream()
.map(field -> this.FieldValuePair(object, field))
.filter(fieldObjectPair -> fieldObjectPair.right.isPresent())
.collect(Collectors.toMap(this::fieldName, this::fieldObject)); } private String fieldName(Pair<Field, Optional<Object>> pair) {
Param alias = pair.left.getAnnotation(Param.class);
return alias != null ? alias.value() : pair.left.getName();
} // 可扩展为策略模式,支持更多的格式转换
private Object fieldObject(Pair<Field, Optional<Object>> pair) {
Object fieldObject = pair.right.get();
DateTimeFormat dateTimeFormat = pair.left.getAnnotation(DateTimeFormat.class);
if (dateTimeFormat != null) {
DateFormat format = new SimpleDateFormat(dateTimeFormat.pattern());
format.setTimeZone(TimeZone.getTimeZone("GMT+8")); // TODO: 最好不要写死时区
fieldObject = format.format(fieldObject);
} else { }
return fieldObject;
} private Pair<Field, Optional<Object>> FieldValuePair(Object object, Field field) {
try {
return Pair.pair(field, Optional.ofNullable(field.get(object)));
} catch (IllegalAccessException e) {
throw new EncodeException("Failure encoding object into query map", e);
}
} private static class ObjectParamMetadata { private final List<Field> objectFields; private ObjectParamMetadata(List<Field> objectFields) {
this.objectFields = Collections.unmodifiableList(objectFields);
} private static ObjectParamMetadata parseObjectType(Class<?> type) {
List<Field> allFields = new ArrayList<Field>(); for (Class<?> currentClass = type; currentClass != null; currentClass = currentClass.getSuperclass()) {
Collections.addAll(allFields, currentClass.getDeclaredFields());
} return new ObjectParamMetadata(allFields.stream()
.filter(field -> !field.isSynthetic())
.peek(field -> field.setAccessible(true))
.collect(Collectors.toList()));
}
} private static class Pair<T, U> {
private Pair(T left, U right) {
this.right = right;
this.left = left;
} public final T left;
public final U right; public static <T, U> Pair<T, U> pair(T left, U right) {
return new Pair<>(left, right);
} }
}

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

解决方案

  1. 不用引入其他的 Feign 依赖,保证有下面这个就行(看网上其他方法还要引入特定依赖,要对应版本号,挺麻烦的)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 编写上面那样的类,你可以直接复制过去改个包名就行,如果还需要除了 Date 以外的格式化,请看注释和文章分析。其中我对日期的格式化,直接使用了 @DateTimeFormat 提供的模式,和 Spring 保持了一致。
  2. 编写一个 Feign 配置类,将刚自定义的编码器注册进去。细节我就不多说了:
package com.example.billmanagerfront.config;

import com.example.billmanagerfront.config.encoder.PowerfulQueryMapEncoder;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import feign.Feign;
import feign.Retryer; @Configuration
public class FeignConfig {
@Bean
public Feign.Builder feignBuilder() {
return Feign.builder()
.queryMapEncoder(new PowerfulQueryMapEncoder())
.retryer(Retryer.NEVER_RETRY);
}
}
  1. Feign 代理接口中声明使用这个配置类,细节不谈
package com.example.billmanagerfront.client;

import java.util.List;

import com.example.billmanagerfront.config.FeignConfig;
import com.example.billmanagerfront.pojo.Bill;
import com.example.billmanagerfront.pojo.BillType; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "BILL-MANAGER", path = "bill", configuration = FeignConfig.class)
public interface BillClient {
@GetMapping("list")
List<Bill> list(@SpringQueryMap(true) Bill b); @GetMapping("type")
List<BillType> type(); @DeleteMapping("delete/{id}")
public String delete(@PathVariable("id") Long id);
}

尊重原创,转载请标明出处。

应该就这些了。

Spring Cloud Feign 如何使用对象参数的更多相关文章

  1. [spring cloud feign] [bug] 使用对象传输get请求参数

    前言 最近在研究 srping cloud feign ,遇到了一个问题,就是当 get 请求 的参数使用对象接收时,就会进入熔断返回.经过百度,发现网上大部分的解决方案都是将请求参数封装到Reque ...

  2. spring cloud微服务快速教程之(十四)spring cloud feign使用okhttp3--以及feign调用参数丢失的说明

    0-前言 spring cloud feign 默认使用httpclient,需要okhttp3的可以进行切换 当然,其实两者性能目前差别不大,差别较大的是很早之前的版本,所以,喜欢哪个自己选择: 1 ...

  3. 笔记:Spring Cloud Feign Ribbon 配置

    由于 Spring Cloud Feign 的客户端负载均衡是通过 Spring Cloud Ribbon 实现的,所以我们可以直接通过配置 Ribbon 的客户端的方式来自定义各个服务客户端调用的参 ...

  4. 笔记:Spring Cloud Feign Hystrix 配置

    在 Spring Cloud Feign 中,除了引入了用户客户端负载均衡的 Spring Cloud Ribbon 之外,还引入了服务保护与容错的工具 Hystrix,默认情况下,Spring Cl ...

  5. 笔记:Spring Cloud Feign 其他配置

    请求压缩 Spring Cloud Feign 支持对请求与响应进行GZIP压缩,以减少通信过程中的性能损耗,我们只需要通过下面二个参数设置,就能开启请求与响应的压缩功能,yml配置格式如下: fei ...

  6. 笔记:Spring Cloud Feign 声明式服务调用

    在实际开发中,对于服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以我们通常会针对各个微服务自行封装一些客户端类来包装这些依赖服务的调用,Spring Cloud Feign 在此基础上做了进 ...

  7. 第六章:声明式服务调用:Spring Cloud Feign

    Spring Cloud Feign 是基于 Netflix Feign 实现的,整合了 Spring Cloud Ribbon 和 Spring Cloud Hystrix,除了提供这两者的强大功能 ...

  8. Spring Cloud Feign Ribbon 配置

    由于 Spring Cloud Feign 的客户端负载均衡是通过 Spring Cloud Ribbon 实现的,所以我们可以直接通过配置 Ribbon 的客户端的方式来自定义各个服务客户端调用的参 ...

  9. Spring Cloud feign

    Spring Cloud feign使用 前言 环境准备 应用模块 应用程序 应用启动 feign特性 综上 1. 前言 我们在前一篇文章中讲了一些我使用过的一些http的框架 服务间通信之Http框 ...

随机推荐

  1. SSRF服务器端请求伪造漏洞基础

    0x00 思考 1.什么是SSRF漏洞?2.SSRF漏洞的利用方式3.SSRF漏洞绕过4.SSRF漏洞加固 0x01 什么是SSRF漏洞 定义:SSRF漏洞,中文全称为服务端请求伪造漏洞,是一种由攻击 ...

  2. Go 分布式令牌桶限流 + 兜底策略

    上篇文章提到固定时间窗口限流无法处理突然请求洪峰情况,本文讲述的令牌桶线路算法则可以比较好的处理此场景. 工作原理 单位时间按照一定速率匀速的生产 token 放入桶内,直到达到桶容量上限. 处理请求 ...

  3. linux系统Kibana安装 汉化

    Elasticsearch官方系列软件Kibana,在控制台管理维护Elasticsearch. 这里注意Elasticsearch和Kibana的版本一定要一致. 官网下载地址 https://ww ...

  4. mysql5.7安装和卸载过程

    安装mysql 5.7 点击下面链接下载 mysql-5.7.27-winx64.zip 压缩文件 链接:https://pan.baidu.com/s/1CF5mmKkZkD_hxsjFOQJrzw ...

  5. jvm锁的四种状态 无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态

    一:java多线程互斥,和java多线程引入偏向锁和轻量级锁的原因? --->synchronized是在jvm层面实现同步的一种机制.  jvm规范中可以看到synchronized在jvm里 ...

  6. 负载均衡后端状态(proxy_next_upstream 后端错误标识)

    目录 一:负载均衡后端状态 二:down(无论什么情况不会分配流量) 三:backup(备用只有当所有的机器宕机(关闭)才能启动备份服务器) 四:max_fails.fail_timeout(结合使用 ...

  7. 挂载的卸载与运行 关闭selinux

    目录 一:系统目录结构介绍 1 定义挂载设备信息 光驱设备 /cd/cdrom 2 完成设备挂载操作 运行挂载 mount/dev/cdrom /mnt/ 3 检查测试挂载结果 挂载检查 df -h ...

  8. JavaScripts之柯里化与偏函数

    参考资料:https://github.com/mqyqingfeng/Blog/issues/42 柯里化(Currying): 定义:柯里化是表示将某个多参数的函数转换为一系列只有一个参数的函数的 ...

  9. 如何在pyqt中实现亚克力磨砂效果的QLabel

    前言 Windows10 在 UWP 应用中支持亚克力画刷,可以在部件的底部绘制亚克力效果的背景图.下面我们使用 QLabel 来模拟这个磨砂过程. 实现方法 MSDN 文档中介绍了亚克力材料的配方, ...

  10. 计算机电子书 2020 CDNDrive 备份(预览版 II)

    下载方式 pip install CDNDrive # 或 # pip install git+https://github.com/apachecn/CDNDrive cdrive download ...