概述

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. JMM模型基础知识笔记

    概述 内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象,不同架构下的物理机拥有不一样的内存模型,Java虚拟机也有自己的内存模型,即Java内存模型(JavaMem ...

  2. 360浏览器兼容模式下jsp页面访问不到js文件

    360浏览器兼容模式下jsp页面访问不到js文件 查看自己js中的语法问题,不要用ES6的语法,编译不了故找不到js文件 const var of 码出高效 java 比较 所有整型包装类对象之间值的 ...

  3. 互联网医疗行业PEST分析实践

    前言 今年开始逐步切入产品与数据工作,完全脱离了原来的舒适区,确实有一些挑战.开始以为只做数仓建设的事情,就仓促的学习了一些数仓相关的知识,但没搞多久,还要负责公司BI的工作,又开始补习数分相关的知识 ...

  4. nRF24L01无线模块笔记

    nRF24L01模块 官网链接: https://www.nordicsemi.com/Products/nRF24-series 常见的无线收发模块, 工作在2.4GHz频段, 适合近距离遥控和数据 ...

  5. 【刷题-LeetCode】238. Product of Array Except Self

    Product of Array Except Self Given an array nums of n integers where n > 1, return an array outpu ...

  6. 小记录:flask的DEBUG开关

    请求站点的如下位置: http://www.ahfu.com/ahfuzhang/?debugger=yes&cmd=resource&f=style.css 居然正常范围了CSS文件 ...

  7. 春节将至,喜庆的烟花安排上(js实现烟花)

    一年一度的春节即将来临,然后苦逼的我还在使劲的摸鱼,未能回家体验小时候路边放爆竹的快乐时光,所以只能在网上来实现这个小小的心愿了.烟花静态效果图如下: 为了大伙复制方便就不分开写,直接复制即可,具体实 ...

  8. 44.Prim算法

    public static void main(String[] args) { //测试看看图是否创建ok char[] data = new char[]{'A','B','C','D','E', ...

  9. CSS基本语法(三)

    目录 CSS基础语法(三) 十五.CSS定位 1.为什么要使用定位 2.定位组成 定位模式 静态定位 相对定位 绝对定位** 固定定位 粘性定位 边偏移 子绝父相 3.定位的叠放次序 4.拓展 绝对定 ...

  10. 【Vue源码学习】响应式原理探秘

    最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...