概述

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. kafka时间轮简易实现(二)

    概述 上一篇主要介绍了kafka时间轮源码和原理,这篇主要介绍一下kafka时间轮简单实现和使用kafka时间轮.如果要实现一个时间轮,就要了解他的数据结构和运行原理,上一篇随笔介绍了不同种类的数据结 ...

  2. 使用nexus搭建一个maven私有仓库

    使用nexus搭建一个maven私有仓库 大家好,我是程序员田同学.今天带大家搭建一个maven私有仓库. 很多公司都是搭建自己的Maven私有仓库,今天就带大家使用nexus搭建一个自己的私有仓库, ...

  3. 【C primer plus】初始化链表函数的错误

    C primer plus第六版 的一处错误 第五百页17.3.4 实现接口的程序清单17.5中的初始化链表函数有误 #源代码 void InitializeList(List * plist) { ...

  4. 雷柏鼠标vt350Q配对

    vt350q 闲鱼捡了个垃圾vt350q,23元,无接收器,不知道好坏 鼠标线 拿到手插上线没法用,后来用了罗技anywhere2s的线可以,原来usb鼠标线是五根. 鼠标毛病 使用后发现滚轮有时候乱 ...

  5. 论文解读GALA《Symmetric Graph Convolutional Autoencoder for Unsupervised Graph Representation Learning》

    论文信息 Title:<Symmetric Graph Convolutional Autoencoder for Unsupervised Graph Representation Learn ...

  6. IDEA出现Cannot resolve symbol “xxx“(无法解析符号)的解决办法

    1,File->Invalidate Caches/Restart 清除缓存并重启 idea 2,检查pom文件中的依赖关系是否正确 3,maven -> Reimport 4,打开pro ...

  7. mongodb中oplogsize大小设置

    1 oplogsize大小配置: 2 mongodb副本集构建的高可用方案,最少需要三个节点,一个主节点master,一个从节点slave,一个选举仲裁节点arbiter.当主节点奔溃的时候,仲裁节点 ...

  8. TCP长连接实践与挑战

    点这里立即申请 本文介绍了tcp长连接在实际工程中的实践过程,并总结了tcp连接保活遇到的挑战以及对应的解决方案. 作者:字节跳动终端技术 --- 陈圣坤 概述 众所周知,作为传输层通信协议,TCP是 ...

  9. CentOS-7部署Docker容器

    为了适应当前容器化的部署方式,故最近也在对docker进行学习.目前的理解是docker服务就像一艘货船,而你的各项服务可以封装在一个个集装箱里,而且在docker里服务的启动非常快,故有必要从基础的 ...

  10. java.lang.IllegalArgumentException: Failed to register servlet with name 'dispatcher'.Check if there is another servlet registered under the same name

    前言 一年前接手了一个项目,项目始终无法运行,不管咋样,都无法处理,最近,在一次尝试中,终于成功处理了. 含义 意思很明显了,注册了一个相同的dispatcher,可是找了很久,没有相同的Contro ...