Spring Cloud Feign 使用对象参数的操作

目录
  • 概述
  • @RequestBody
  • @SpringQueryMap
  • QueryMapEncoder
  • 解决方案

概述

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>

2.编写上面那样的类,你可以直接复制过去改个包名就行,如果还需要除了 Date 以外的格式化,请看注释和文章分析。其中我对日期的格式化,直接使用了 @DateTimeFormat 提供的模式,和 Spring 保持了一致。

3.编写一个 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);
    }
}

4.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 如何使用对象参数的文章就介绍到这了,更多相关Spring Cloud Feign对象参数内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 解决SpringCloud Feign传对象参数调用失败的问题

    SpringCloud Feign传对象参数调用失败 不支持GET请求方式 使用Apache HttpClient替换Feign原生httpclient @RequestBody接收json参数 bootstrap-local.yml feign: httpclient: enabled: true pom.xml <!-- 使用Apache HttpClient替换Feign原生httpclient --> <dependency> <groupId>com.netf

  • Spring Cloud Feign 使用对象参数的操作

    目录 概述 @RequestBody @SpringQueryMap QueryMapEncoder 解决方案 概述 Spring Cloud Feign 用于微服务的封装,通过接口代理的实现方式让微服务调用变得简单,让微服务的使用上如同本地服务.但是它在传参方面不是很完美.在使用 Feign 代理 GET 请求时,对于简单参数(基本类型.包装器.字符串)的使用上没有困难,但是在使用对象传参时却无法自动的将对象包含的字段解析出来. 如果你没耐心看完,直接跳到最后一个标题跟着操作就行了. @Req

  • 使用Spring Cloud Feign上传文件的示例

    最近经常有人问Spring Cloud Feign如何上传文件.有团队的新成员,也有其他公司的兄弟.本文简单做个总结-- 早期的Spring Cloud中,Feign本身是没有上传文件的能力的(1年之前),要想实现这一点,需要自己去编写Encoder 去实现上传.现在我们幸福了很多.因为Feign官方提供了子项目feign-form ,其中实现了上传所需的 Encoder . 注:笔者测试的版本是Edgware.RELEASE.Camden.Dalston同样适应本文所述. 加依赖 <depen

  • Spring cloud Feign 深度学习与应用详解

    简介 Spring Cloud Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单.Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数.格式.地址等信息.Feign会完全代理HTTP请求,开发时只需要像调用方法一样调用它就可以完成服务请求及相关处理.开源地址:https://github.com/OpenFeign/feign.Feign整合了Ribbon负载和Hystrix熔断,可以不再需要显式地

  • 解决Spring Cloud Feign 请求时附带请求头的问题

    问题描述 Feign 在请求时是不会将 request 的请求头带着请求的,导致假如 Feign 调用的接口需要请求头的信息,比如当前用户的 token 之类的就获取不到 解决方案 FeignConfiguration 通过实现 Feign 的 RequestInterceptor 将从上下文中获取到的请求头信息循环设置到 Feign 请求头中. /** * feign 配置文件 * 将请求头中的参数,全部作为 feign 请求头参数传递 * @author: linjinp * @create

  • 使用Spring Cloud Feign作为HTTP客户端调用远程HTTP服务的方法(推荐)

    在Spring Cloud Netflix栈中,各个微服务都是以HTTP接口的形式暴露自身服务的,因此在调用远程服务时就必须使用HTTP客户端.我们可以使用JDK原生的URLConnection.Apache的Http Client.Netty的异步HTTP Client, Spring的RestTemplate.但是,用起来最方便.最优雅的还是要属Feign了. Feign简介 Feign是一种声明式.模板化的HTTP客户端.在Spring Cloud中使用Feign, 我们可以做到使用HTT

  • Spring Cloud Feign实例讲解学习

    前面博文搭建了一个Eureka+Ribbon+Hystrix的框架,虽然可以基本满足服务之间的调用,但是代码看起来实在丑陋,每次客户端都要写一个restTemplate,为了让调用更美观,可读性更强,现在我们开始学习使用Feign. Feign包含了Ribbon和Hystrix,这个在实战中才慢慢体会到它的意义,所谓的包含并不是Feign的jar包包含有Ribbon和Hystrix的jar包这种物理上的包含,而是Feign的功能包含了其他两者的功能这种逻辑上的包含.简言之:Feign能干Ribb

  • 详解Spring Cloud Feign 熔断配置的一些小坑

    1.在使用feign做服务调用时,使用继承的方式调用服务,加入Hystrix的熔断处理fallback配置时,会报错,已解决. 2.使用feign默认配置,熔断不生效,已解决. 最近在做微服务的学习,发现在使用feign做服务调用时,使用继承的方式调用服务,加入Hystrix的熔断处理fallback配置时,会报错,代码如下: @RequestMapping("/demo/api") public interface HelloApi { @GetMapping("user/

  • 详解spring cloud Feign使用中遇到的问题总结

    本文介绍了spring cloud Feign使用中遇到的问题总结,分享给大家,具体如下: 问题一: 在前面的示例中,我们讲过 @RequestMapping(value = "/user/{id}", method = RequestMethod.GET) @GetMapping("/user/{id}") 这两个注解的效果是等价的,但是在Feign使用中,只能用上面的那种方式,不能直接用@GetMapping,下面我们将前面的那个示例中,改成@GetMappin

  • 使用Spring Cloud Feign远程调用的方法示例

    在Spring Cloud Netflix栈中,各个微服务都是以HTTP接口的形式暴露自身服务的,因此在调用远程服务时就必须使用HTTP客户端.我们可以使用JDK原生的URLConnection.Apache的Http Client.Netty的异步HTTP Client, Spring的RestTemplate.但是,用起来最方便.最优雅的还是要属Feign了. Feign简介 Feign是一个声明式的Web服务客户端,使用Feign可使得Web服务客户端的写入更加方便. 它具有可插拔注释支持

  • Spring Cloud Feign组件实例解析

    这篇文章主要介绍了Spring Cloud Feign组件实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 采用Spring Cloud微服务框架后,经常会涉及到服务间调用,服务间调用采用了Feign组件. 由于之前有使用dubbo经验.dubbo的负载均衡策略(轮训.最小连接数.随机轮训.加权轮训),dubbo失败策略(快速失败.失败重试等等), 所以Feign负载均衡策略的是什么? 失败后是否会重试,重试策略又是什么?带这个疑问,查了

随机推荐