SpringCloud 声明式调用Feign

概述

Feign 是由 Netflix 开源的声明式的 HTTP 客户端,目前已经捐献给 OpenFeign 社区。

Feign makes writing java http clients easier

通过使用定义简单的接口,并声明 Feign 提供的注解,来实现 HTTP 的调用

Spring Cloud OpenFeign 组件,将 Feign 集成到 Spring Cloud 体系中,实现服务的声明式 HTTP 调用。相比使用 RestTemplate 实现服务的调用,Feign 简化了代码的编写,提高了代码的可读性,大大提升了开发的效率。

Spring Cloud OpenFeign 除了支持 Feign 自带的注解之外,额外提供了对 JAX-RS 注解、SpringMVC 注解的支持。特别是对 SpringMVC 注解的支持,简直是神来之笔,让我们不用学习 Feign 自带的注解,而直接使用超级熟悉的 SpringMVC 注解。

同时,Spring Cloud OpenFeign 进一步将 Feign 和 Ribbon 整合,提供了负载均衡的功能。另外,Feign 自身已经完成和 Hystrix 整合,提供了服务容错的功能。

如此,我们基于注解,极其简单的实现服务的调用,并且具有负载均衡、服务容错的功能。

快速入门

使用Nacos作为注册中心,搭建一个服务提供者 demo-provider,启动 2 个实例,注册服务到 Nacos 中。然后,搭建一个服务消费者 demo-consumer,使用 Ribbon 进行负载均衡,使用 Feign 声明式调用服务提供者 demo-provider 的 HTTP 接口。

具体案例实现步骤:

步骤1:搭建服务提供者(子项目)

 项目
  • 服务提供者中不会涉及到Feign

步骤2:搭建服务消费者(其实就是一个子项目)

项目依赖:

<properties>
        <spring.boot.version>2.2.4.RELEASE</spring.boot.version>
        <spring.cloud.version>Hoxton.SR1</spring.cloud.version>
        <spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
    </properties>

    <!--
        引入 Spring BootSpring CloudSpring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
        在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
     -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 引入 SpringMVC 相关依赖,并实现对其的自动配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- 引入 Spring Cloud OpenFeign 相关依赖,使用 OpenFeign 提供声明式调用,并实现对其的自动配置 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

这里我们没有主动引入 spring-cloud-netflix-ribbon依赖,因为 spring-cloud-starter-alibaba-nacos-discoveryspring-cloud-starter-openfefign 默认都引入了它。如下图所示:

依赖关系

最终代码:
请添加图片描述

步骤3:简单测试

① 通过 DemoProviderApplication 启动 2 次,启动服务提供者的 2 个实例。因为 IDEA 默认同一个程序只允许启动 1 次,所以我们需要配置 DemoProviderApplication 为 Allow parallel run。如下图所示:

Allow parallel run

② 通过 DemoConsumerApplication 启动服务消费者。

访问服务消费者的 http://127.0.0.1:28080/hello02?name=yudaoyuanma 接口 2 次,返回结果如下:

consumer:10107-provider:123
consumer:13729-provider:123
  • 使用 Feign 声明式调用服务 demo-provider 成功
  • 使用 Ribbon 负载均衡成功
  • 从注册中心 Nacos 加载服务 demo-provider 的服务实例成功

总结

  • 搭建服务提供者需要做的事情:在启动类上添加注解:@EnableDiscoveryClient,标识服务注册到Nacos注册中心;在配置文件中,配置项目名称spring.application.name,将项目注册到注册中心(Nacos, Eureka)–spring.cloud.nacos.discovery.server-addr

  • 搭建服务消费者需要做的事情:在启动类上添加注解@@EnableFeignClients,声明开启 Feign客户端的功能;HTTP接口上添加注解@FeignClient(name = "服务提供者名称"),实现对服务提供者声明式调用。

  • HTTP接口中可以使用Feign定义的注解(@RequestLine@Param)或者SpringMVC注解实现HTTP API的调用

    import feign.Param;
    import feign.RequestLine;
    
    // 商品 API
    interface ProductAPI {
    
        // 获得商品详情
        @RequestLine("POST /products/{id}")
        String get(@Param("id") Integer id);
    
    }
    
  • HTTP接口中的方法名必须和服务提供者中Controller层中的某一个方法名相同
    在这里插入图片描述

自定义Feign配置

本小节,我们来学习如何对 Feign 进行自定义配置。例如说,自定义 Feign 的日志配置,将 Feign 的请求信息打印出来,方便排查问题。

在自定义 Feign 配置的时候,会有全局客户端两种级别。客户端级别是更细粒度的配置,针对每个服务,Spring Cloud OpenFeign 会创建一个 Feign 客户端,并且使用服务名作为 Feign 客户端的名字

实现 Feign 自定义配置,可以通过配置文件Spring JavaConfig 两种方式。

配置文件方式

修改 application.yaml 配置文件,额外添加如下配置:

logging:
  level:
    cn.iocoder.springcloud.labx03.feigndemo.consumer.feign: DEBUG

feign:
  # Feign 客户端配置,对应 FeignClientProperties 配置属性类
  client:
    # config 配置项是 Map 类型。key 为 Feign 客户端的名字,value 为 FeignClientConfiguration 对象
    config:
      # 全局级别配置
      default:
        logger-level: BASIC
      # 客户端级别配置
      demo-provider:
        logger-level: FULL

在 Feign 中,定义了四种日志级别

  • NONE:不打印日志
  • BASIC:只打印基本信息,包括请求方法、请求地址、响应状态码、请求时长
  • HEADERS:在 BASIC 基础信息的基础之上,增加请求头、响应头
  • FULL:打印完整信息,包括请求和响应的所有信息。

Spring javaConfig方式

请添加图片描述

客户端级别的自定义配置

通过 @FeignClient 注解的 configuration 属性,我们可以设置指定 FeignClient 使用的配置类,即 Feign 客户端级别的自定义配置

全局级别的自定义配置

通过 @EnableFeignClients 注解的 defaultConfiguration 属性,我们可以设置默认 FeignClient 使用的配置类,即 Feign 全局级别的自定义配置

为什么自定义配置类不需要被@Configuration注解标识

对于 DefaultFeignClientConfiguration 和 DemoProviderFeignClientConfiguration 两个配置类,我们并没有添加 @Configuration 注解

因为,Spring Boot 项目默认扫描 DemoConsumerApplication 所在包以及子包下的所有 Bean 们。而 @Configuration 注解也是一种 Bean,也会被扫描到。

如果添加 @Configuration 注解到 DefaultFeignClientConfiguration 和 DemoProviderFeignClientConfiguration 上,将会被 Spring Boot 所扫描到,导致整个项目的 Feign 客户端都使用相同的 Feign 配置,就无法到达 Feign 客户端级别的自定义配置的目的

因此,我们没有给 DefaultFeignClientConfiguration 和 DemoProviderFeignClientConfiguration 添加 @Configuration 注解。

简单测试

启动快速入门中的服务提供者,启动本小节中的服务消费者。访问服务消费者的 http://127.0.0.1:28080/hello02?name=yudaoyuanma 接口,可以看到 IDEA 控制台输出如下 Feign 请求日志:

2020-02-10 23:28:50.007 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] ---> GET http://demo-provider/echo?name=yudaoyuanma HTTP/1.1
2020-02-10 23:28:50.007 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] ---> END HTTP (0-byte body)
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] <--- HTTP/1.1 200 (106ms)
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] connection: keep-alive
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] content-length: 26
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] content-type: text/plain;charset=UTF-8
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] date: Mon, 10 Feb 2020 15:28:50 GMT
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] keep-alive: timeout=60
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] 
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] 14747-provider:yudaoyuanma
2020-02-10 23:28:50.114 DEBUG 13774 --- [io-28080-exec-6] c.i.s.l.f.c.f.DemoProviderFeignClient    : [DemoProviderFeignClient#echo] <--- END HTTP (26-byte body)
  • 从日志中也可以看出 Feign 是调用日志组件的 DEBUG 级别打印日志。

如果想要测试 Feign 全局级别的自定义配置的效果,可以去掉 DemoProviderFeignClient 类上的 @FeignClient 注解的 configuration 属性。

实践建议

  • 对于 Feign 自定义配置,推荐使用配置文件的方式,简单方便好管理。在配置文件的方式无法满足的情况下,使用 Spring JavaConfig 的方式作为补充。不过绝大多数场景下,都基本不需要哈~
  • 配置文件方式的优先级高于 Spring JavaConfig 方式,客户端级别的优先级高于全局级别

继承特性

我们在快速入门者一节中的总结部分讲过:HTTP接口中的方法名与服务提供者Controller层的某一个方法同名。那么我们就可以抽取出来这个方法,让服务提供者去实现这个接口中的方法,服务消费者继承这个接口

实践总结:Spring Cloud OpenFeign官方不推荐这中做法,但是在实际工作中,蛮多公司采用这种方式,显而易见的好处,可以方便服务消费者的快速接入,基本无需编写额外的代码。

具体怎么选择,可以自己进行评估,看看使用继承特性的情况下,在享受优点的同时,是否能够接受带来的缺点。从 Dubbo的使用方式来说,也可以认为它是是支持采用继承特性

复杂参数

在快速入门这一小节中,考虑到简单上手 Spring Cloud OpenFeign 的使用,我们只提供了 Feign 请求单个参数的简单参数例子。但是实际项目中,我们必然会面临传递多个参数的复杂参数的场景。例如说:

GET /demo/?param1=value1&param2=value2

POST /demo {
    param1: value1,
    param2: value2
}

针对 GETPOST 类型的请求,Spring Cloud OpenFeign 传递复杂参数有不同的处理方式

DTO对象

在快速入门的案例中进行修改作为案例演示

// DemoDTO.java
public class DemoDTO {
    private String username;
    private String password;
    // ... 省略 setter、getter 方法
}

服务提供者的接口:

// ProviderController.java

@GetMapping("/get_demo")
public DemoDTO getDemo(DemoDTO demoDTO) {
    return demoDTO;
}

@PostMapping("/post_demo")
public DemoDTO postDemo(@RequestBody DemoDTO demoDTO) {
    return demoDTO;
}

服务消费者的接口:

// DemoProviderFeignClient.java
@GetMapping("/get_demo") // GET 方式一,最推荐
DemoDTO getDemo(@SpringQueryMap DemoDTO demoDTO);

@GetMapping("/get_demo") // GET 方式二,相对推荐
DemoDTO getDemo(@RequestParam("username") String username, @RequestParam("password") String password);

@GetMapping("/get_demo") // GET 方式三,不推荐
DemoDTO getDemo(@RequestParam Map<String, Object> params);

@PostMapping("/post_demo") // POST 方式
DemoDTO postDemo(@RequestBody DemoDTO demoDTO);

📚 GET 场景

①【最推荐】方式一,采用 Spring Cloud OpenFeign 提供的 @SpringQueryMap 注解,并使用 DemoDTO 对象。

默认情况下,Feign 针对 POJO 类型的参数,即使我们声明为 GET 类型的请求,也会自动转换成 POST 类型的请求。如果我们去掉 @SpringQueryMap 注解,就会报如下异常:

feign.FeignException$MethodNotAllowed: status 405 reading DemoProviderFeignClient#getDemo(DemoDTO)
  • Feign 自动转换成了 POST /get_demo 请求,而服务提供者提供的 /get_demo 只支持 GET 类型,因此返回响应状态码为 405 的错误。@SpringQueryMap 注解的作用,相当于 Feign 的 @QueryMap注解,将 POJO 对象转换成 QueryString

②【较推荐】方式二,采用 SpringMVC 提供的 @RequestParam 注解,并将所有参数平铺开。

参数较少的时候,可以采用这种方式。如果参数过多的话,还是采用方式一更优。

③【不推荐】方式三,采用 SpringMVC 提供的 @RequestParam 注解,并使用 Map 对象。非常不推荐,因为可读性差,都不知道传递什么参数。

📚 POST 场景

① 唯一方式,采用 SpringMVC 提供的 @RequestBody 注解,并使用 DemoDTO 对象。

文件上传

表单上传

Feign单独使用

在使用 Spring Cloud 的项目中,我们大多数是通过 Feign 调用从 Ribbon 负载均衡选择的服务实例,而 Ribbon 是通过注册中心获取到的服务实例列表。但是有些场景下,可能想要单独使用 Feign 调用,例如说:

  • 调用第三方服务,例如说短信云服务、推送云服务
  • 调用的虽然是内部服务,但是并没有注册到注册中心,而是使用 Nginx 代理并负载均衡实现高可用

Feign单独使用,代码只是在以下地方有不同之处:

//@FeignClient(name = "demo-provider")
@FeignClient(name = "iocoder", url = "http://www.iocoder.cn") // 注意,保持 name 属性和 url 属性的 host 是一致的。
public interface DemoProviderFeignClient {

//    @GetMapping("/echo")
//    String echo(@RequestParam("name") String name);

    @GetMapping("/") // 请求首页
    String echo(@RequestParam("name") String name);

}

@FeignClient 注解的 url 属性设置要调用的服务的地址。不过要注意,保持 name 属性和 url 属性的 host 是一致的,不然还是会使用 Ribbon 进行负载均衡。

HTTP客户端

默认情况下,Feign 通过 JDK 自带的 HttpURLConnection 封装了 Client.Default,实现 HTTP 调用的客户端。因为 HttpURLConnection 缺少对 HTTP 连接池的支持,所以性能较低,在并发到达一定量级后基本会出现。

因此 Feign 提供了另外两个 HTTP 客户端:

ApacheHttpClient

在第一小节(快速入门)项目的基础上:

引入依赖:

<!-- 引入 Feign Apache HttpClient 依赖 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

配置文件添加额外配置

feign:
  # Feign Apache HttpClient 配置项,对应 FeignHttpClientProperties 配置属性类
  httpclient:
    enabled: true # 是否开启。默认为 true
    max-connections: 200 # 最大连接数。默认为 200
    max-connections-per-route: 50 # 每个路由的最大连接数。默认为 50。router = host + port

通过 feign.httpclient 配置项,我们可以开启 Feign Apache HttpClient,并进行自定义配置。在 FeignHttpClientProperties 配置属性类中,还有其它配置项,胖友可以简单看看。

不过有一点要注意,虽然说 feign.httpclient.enable 默认为 true 开启,但是还是需要引入 feign-httpclient 依赖,才能创建 ApacheHttpClient 对象。

简单测试:

OkHttpClient

在第一小节(快速入门)项目的基础上:

引入依赖:

<!-- 引入 Feign Apache HttpClient 依赖 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

配置文件添加额外配置

feign:
  httpclient:
    enabled: false # 是否开启。默认为 true
  okhttp:
    enabled: true # 是否开启。默认为 false

通过设置 feign.okhttp.enabled 配置项为 true,我们可以开启 Feign OkHttp。目前暂无其它 feign.okhttp 配置项。

另外,因为 feign.httpclient.enabled 配置项默认为 true,所以需要手动设置成 false,避免使用了 Feign Apache HttpClient。

请求重试

Feign 和 Ribbon 都有请求重试的功能,两者都启用该功能的话,会产生冲突的问题。因此,有且只能启动一个的重试。目前比较推荐的是使用 Ribbon 来提供重试

在 Spring Cloud OpenFeign 中,默认创建的是 NEVER_RETRY 不进行重试。如此,我们只需要配置 Ribbon 的重试功能即可。

Feign 与 RestTemplate 的对比

从开发效率、可维护性的角度来说,Feign 更加有优势。
从执行性能、灵活性的角度来说,RestTemplate 更加有优势。

个人推荐使用 Feign 为主,RestTemplate 为辅

  • 相比来说,开发效率、可维护性非常重要,要保证开发的体验。
  • 执行性能的问题,因为 Feign 多一层 JDK 动态代理,所以会差一些。不过 HTTP 调用的整体性能的大头在网络传输和服务端的执行时间,所以 Feign 和 RestTemplate 的性能差距可以相对忽略。
  • 灵活性的问题,99.99% 的情况下,Feign 都能够实现或者相对绕的实现;无法实现的情况下,在考虑采用 RestTemplate 进行实现。

Feign 主要组件

Feign.Builder

Feign.Builder 类,Feign 构造器,可以设置各种配置,最终构建出指定 API 接口的 HTTP “客户端”

RemoteService service = Feign.builder()
            .options(new Options(1000, 5000)) // 请求的连接和读取超时时间
            .retryer(new Retryer.Default(5000, 5000, 3)) // 重试策略
            .target(RemoteService.class, "http://www.iocoder.cn"); // 目标 API 接口和目标地址

Client

Client 接口,定义提交 HTTP 请求的方法。Feign 提供了 4 个 Client 实现类,如下图所示:

Contract

Encoder

Decoder

RequestInterceptor

Logger

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值