Springcloud基础知识(5)- Spring Cloud OpenFeign | 声明式服务调用



Feign 是 Netflix 公司发布的一种实现负载均衡和服务调用的开源组件。Spring Cloud 将其与 Netflix 中的其他开源服务组件(例如 Eureka、Ribbon 以及 Hystrix 等)一起整合进 Spring Cloud Netflix 模块中,整合后全称为 Spring Cloud Netflix Feign。
 
Feign 对 Ribbon 进行了集成,利用 Ribbon 维护了一份可用服务清单,并通过 Ribbon 实现了客户端的负载均衡。

Feign 是一种声明式服务调用组件,它在 RestTemplate 的基础上做了进一步的封装。通过 Feign,我们只需要声明一个接口并通过注解进行简单的配置(类似于 Dao 接口上面的 Mapper 注解一样)即可实现对 HTTP 接口的绑定。

Feign 支持多种注解,例如 Feign 自带的注解以及 JAX-RS 注解等,但遗憾的是 Feign 本身并不支持 Spring MVC 注解,这无疑会给广大 Spring 用户带来不便。

2019 年 Netflix 公司宣布 Feign 组件正式进入停更维护状态,于是 Spring 官方便推出了一个名为 OpenFeign 的组件作为 Feign 的替代方案。


1. OpenFeign 简介


    OpenFeign 全称 Spring Cloud OpenFeign,它是 Spring 官方推出的一种声明式服务调用与负载均衡组件,它的出现就是为了替代进入停更维护状态的 Feign。

    OpenFeign 是 Spring Cloud 对 Feign 的二次封装,它具有 Feign 的所有功能,并在 Feign 的基础上增加了对 Spring MVC 注解的支持,例如 @RequestMapping、@GetMapping 和 @PostMapping 等。

    OpenFeign 常用注解如下表。

注解 描述
@FeignClient 该注解用于通知 OpenFeign 组件对 @RequestMapping 注解下的接口进行解析,并通过动态代理的方式产生实现类,实现负载均衡和服务调用。
@EnableFeignClients 该注解用于开启 OpenFeign 功能,当 Spring Cloud 应用启动时,OpenFeign 会扫描标有 @FeignClient 注解的接口,生成代理并注册到 Spring 容器中。
@RequestMapping Spring MVC 注解,在 Spring MVC 中使用该注解映射请求,通过它来指定控制器(Controller)可以处理哪些 URL 请求,相当于 Servlet 中 web.xml 的配置。
@GetMapping Spring MVC 注解,用来映射 GET 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.GET) 。
@PostMapping Spring MVC 注解,用来映射 POST 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.POST) 。

 
        Spring Cloud Finchley 及以上版本一般使用 OpenFeign 作为其服务调用组件。由于 OpenFeign 是在 2019 年 Feign 停更进入维护后推出的,因此大多数 2019 年及以后的新项目使用的都是 OpenFeign,而 2018 年以前的项目一般使用 Feign。

    Feign 和 OpenFegin 具有以下相同点:

        (1) Feign 和 OpenFeign 都是 Spring Cloud 下的远程调用和负载均衡组件;
        (2) Feign 和 OpenFeign 作用一样,都可以实现服务的远程调用和负载均衡;
        (3) Feign 和 OpenFeign 都对 Ribbon 进行了集成,都利用 Ribbon 维护了可用服务清单,并通过 Ribbon 实现了客户端的负载均衡;
        (4) Feign 和 OpenFeign 都是在服务消费者(客户端)定义服务绑定接口并通过注解的方式进行配置,以实现远程服务的调用;

    Feign 和 OpenFeign 具有以下不同:
    
        (1) Feign 和 OpenFeign 的依赖项不同,Feign 的依赖为 spring-cloud-starter-feign,而 OpenFeign 的依赖为 spring-cloud-starter-openfeign;
        (2) Feign 和 OpenFeign 支持的注解不同,Feign 支持 Feign 注解和 JAX-RS 注解,但不支持 Spring MVC 注解;(3) OpenFeign 除了支持 Feign 注解和 JAX-RS 注解外,还支持 Spring MVC 注解;


2. OpenFeign 实现服务调用

    本文将在 “ ” 里 SpringcloudDemo03 项目基础上,添加一个 ConsumerFeign 子模块,来演示 OpenFeign 服务调用。SpringcloudDemo03 的 Spring Boot 版本是 2.3.12.RELEASE。

    1) 创建 ConsumerFeign 模块

        选择左上的项目列表中的 SpringcloudDemo03,点击鼠标右键,选择 New -> Module 进入 New Module 页面:

            Maven -> Project SDK: 1.8 -> Check "Create from archtype" -> select "org.apache.maven.archtypes:maven-archtype-quickstart" -> Next

                Name: ConsumerFeign
                GroupId: com.example
                ArtifactId: ConsumerFeign

            -> Finish

        注:模块 ConsumerFeign 创建后,Maven 命令会自动修改主项目 SpringcloudDemo03 的 pom.xml,添加如下内容:

           
                ...

                ConsumerFeign
           


    2) 修改 ConsumerFeign 的 pom.xml 内容如下

 1         <?xml version="1.0" encoding="UTF-8"?>
 2         <project xmlns="http://maven.apache.org/POM/4.0.0"
 3                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4                 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
 5                                     http://maven.apache.org/xsd/maven-4.0.0.xsd">
 6             <parent>
 7                 <artifactId>SpringcloudDemo03artifactId>
 8                 <groupId>com.examplegroupId>
 9                 <version>1.0-SNAPSHOTversion>
10             parent>
11             <modelVersion>4.0.0modelVersion>
12 
13             <artifactId>ConsumerFeignartifactId>
14 
15             <name>ConsumerFeignname>
16             
17             <url>http://www.example.comurl>
18 
19             <properties>
20                 <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
21                 <maven.compiler.source>1.8maven.compiler.source>
22                 <maven.compiler.target>1.8maven.compiler.target>
23             properties>
24 
25             <dependencies>
26                 <dependency>
27                     <groupId>junitgroupId>
28                     <artifactId>junitartifactId>
29                     <version>4.12version>
30                     <scope>testscope>
31                 dependency>
32                 <dependency>
33                     <groupId>org.springframework.bootgroupId>
34                     <artifactId>spring-boot-starter-webartifactId>
35                 dependency>
36                 <dependency>
37                     <groupId>org.springframework.bootgroupId>
38                     <artifactId>spring-boot-starter-testartifactId>
39                     <scope>testscope>
40                 dependency>
41                 
42                 <dependency>
43                     <groupId>com.examplegroupId>
44                     <artifactId>CommonAPIartifactId>
45                     <version>${project.version}version>
46                 dependency>
47                 <dependency>
48                     <groupId>org.projectlombokgroupId>
49                     <artifactId>lombokartifactId>
50                     <version>1.18.8version>
51                 dependency>
52                 <dependency>
53                     <groupId>org.springframework.cloudgroupId>
54                     <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
55                 dependency>
56                 <dependency>
57                     <groupId>org.springframework.cloudgroupId>
58                     <artifactId>spring-cloud-starter-openfeignartifactId>
59                 dependency>
60             dependencies>
61 
62             <build>
63                 <plugins>
64                     <plugin>
65                         <groupId>org.springframework.bootgroupId>
66                         <artifactId>spring-boot-maven-pluginartifactId>
67                         <configuration>
68                             <mainClass>com.example.AppmainClass>
69                             <layout>JARlayout>
70                         configuration>
71                         <executions>
72                             <execution>
73                                 <goals>
74                                     <goal>repackagegoal>
75                                 goals>
76                             execution>
77                         executions>
78                     plugin>
79                 plugins>
80             build>
81         project>


        注:spring-cloud-starter-openfeign 内置了 Ribbon,不需要另外导入 Ribbon 的依赖。
        
            Spring Boot 2.3.12.RELEASE 自动导入的 OpenFeign 版本是 2.2.9.RELEASE,没有遇到客户端默认 1 秒超时问题。 在 Spring Boot 2.6.6 下自动导入的 OpenFeign 版本是 3.1.3,也没有遇到客户端默认 1 秒超时问题。 
           
    3) 创建 src/main/resources/application.yml 文件

1         server:
2             port: 80
3         eureka:
4             client:
5                 register-with-eureka: false # 消费者不向服务注册中心注册服务
6                 fetch-registry: true  # 消费者客户端需要去检索服务
7                 service-url:
8                     defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/


    4) 创建 src/main/java/com/example/service/EmployeeFeignService.java 文件

 1         package com.example.service;
 2 
 3         import java.util.List;
 4 
 5         import org.springframework.stereotype.Service;
 6         import org.springframework.cloud.openfeign.FeignClient;
 7         import org.springframework.web.bind.annotation.PathVariable;
 8         import org.springframework.web.bind.annotation.RequestMapping;
 9         import org.springframework.web.bind.annotation.RequestMethod;
10 
11         import com.example.entity.Employee;
12 
13         @Service
14         @FeignClient(value = "EMPLOYEE-SERVICE-PROVIDER")
15         public interface EmployeeFeignService {
16 
17             @RequestMapping(value = "/employee/get/{id}", method = RequestMethod.GET)
18             public Employee get(@PathVariable("id") int id);
19 
20             @RequestMapping(value = "/employee/list", method = RequestMethod.GET)
21             public List list();
22         }


        @FeignClient 注解中,value 属性的取值为:服务提供者的服务名,即 ServiceProvider 的配置文件(application.yml)中 spring.application.name 的取值。

        接口中定义的方法与服务提供者(ServiceProvider 的各实例)中 EmployeeController 定义的服务方法对应。

    5) 创建 src/main/java/com/example/controller/ConsumerController.java 文件

 1         package com.example.controller;
 2 
 3         import java.util.List;
 4 
 5         import org.springframework.beans.factory.annotation.Autowired;
 6         import org.springframework.web.bind.annotation.PathVariable;
 7         import org.springframework.web.bind.annotation.RequestMapping;
 8         import org.springframework.web.bind.annotation.RestController;
 9 
10         import com.example.entity.Employee;
11         import com.example.service.EmployeeFeignService;
12 
13         @RestController
14         @RequestMapping(value = "/consumer")
15         public class ConsumerController {
16             @Autowired
17             private EmployeeFeignService employeeFeignService;
18 
19             @RequestMapping(value = "/employee/get/{id}")
20             public Employee get(@PathVariable("id") Integer id) {
21                 return employeeFeignService.get(id);
22             }
23 
24             @RequestMapping(value = "/employee/list")
25             public List list() {
26                 return employeeFeignService.list();
27             }
28 
29         }


    6) 修改 src/main/java/com/example/App.java 文件

 1         package com.example;
 2 
 3         import org.springframework.boot.SpringApplication;
 4         import org.springframework.boot.autoconfigure.SpringBootApplication;
 5         import org.springframework.cloud.openfeign.EnableFeignClients;
 6 
 7         @SpringBootApplication
 8         @EnableFeignClients     // 开启 OpenFeign 功能
 9         public class App {
10             public static void main(String[] args) {
11                 SpringApplication.run(App.class, args);
12             }
13         }


    7) 运行

        下面我们在 “ ” 里的集群基础上,测试 OpenFeign 实现服务调用。


        依次启动 server-7001、server-7002、server-7003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。

        再依次启动 service-8001、service-8002、service-8003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。

        浏览器访问 http://eureka7001.com:7001/,页面上 “Instances currently registered with Eureka” 区域显示:

            Application                          AMIs      Availability Zones      Status
            EMPLOYEE-SERVICE-PROVIDER    n/a (3)        (3)                    UP (3) - sevice-provider-8003 , sevice-provider-8002 , sevice-provider-8001

        启动 ConsumerFeign 模块:

            浏览器访问 http://localhost/consumer/employee/list,显示结果如下:

                [{"id":1,"name":"Test Name1","port":8001},{"id":2,"name":"Test Name2","port":8001}]

            刷新页面:

                [{"id":1,"name":"Test Name1","port":8002},{"id":2,"name":"Test Name2","port":8002}]

            再次刷新页面:

                [{"id":1,"name":"Test Name1","port":8003},{"id":2,"name":"Test Name2","port":8003}]

            注:通过 port 的变化可以看出,OpenFeign 内置了 Ribbon,因此它也实现了客户端的负载均衡,其默认负载均衡策略为轮询策略。


3. OpenFeign 日志增强

    OpenFeign 提供了日志打印功能,我们可以通过配置调整日志级别,来了解请求的细节。

    Feign 为每一个 FeignClient 都提供了一个 feign.Logger 实例,通过它可以对 OpenFeign 服务绑定接口的调用情况进行监控。

    OpenFeign 日志打印功能的开启方式比较简单,下面我们在 ConsumerFeign 模块里进行演示。

    1) 修改 ConsumerFeign 的 application.yml,添加配置如下

          logging:
              level:
                  # OpenFeign 日志以什么样的级别监控该接口
                  com.example.service.EmployeeFeignService: debug

        以上配置的含义就是,OpenFeign 以 debug 级别监控 com.example.service.EmployeeFeignService 接口。
        
        com.example.service.EmployeeFeignService 是开启 @FeignClient 注解的接口(即服务绑定接口)的完整类名。也可以只配置部分路径,表示监控该路径下的所有服务绑定接口。debug表示监听该接口的日志级别。

    2) 创建 src/main/java/com/example/config/OpenFeignConfig.java 文件

 1             package com.example.config;
 2 
 3             import feign.Logger;
 4             import org.springframework.context.annotation.Bean;
 5             import org.springframework.context.annotation.Configuration;
 6 
 7             @Configuration
 8             public class OpenFeignConfig {
 9                 @Bean
10                 Logger.Level feginLoggerLevel() {
11                     return Logger.Level.FULL;
12                 }
13             }


        该配置的作用是通过配置的 Logger.Level 对象告诉 OpenFeign 记录哪些日志内容。

        Logger.Level 的具体级别如下:

            (1) NONE:不记录任何信息;
            (2) BASIC:仅记录请求方法、URL 以及响应状态码和执行时间;
            (3) HEADERS:除了记录 BASIC 级别的信息外,还会记录请求和响应的头信息;
            (4) FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等等;

    3) 重启 ConsumerFeign 模块

        浏览器访问 http://localhost/consumer/employee/list,控制台输出如下:

            2022-06-25 17:56:17.392 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] ---> END HTTP (0-byte body)
            2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] <--- HTTP/1.1 200 (4ms)
            2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] connection: keep-alive
            2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] content-type: application/json
            2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] date: Sat, 25 Jun 2022 09:56:17 GMT
            2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] keep-alive: timeout=60
            2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] transfer-encoding: chunked
            2022-06-25 17:56:17.398 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list]
            2022-06-25 17:56:17.398 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] [{"id":1,"name":"Test Name1","port":8002},{"id":2,"name":"Test Name2","port":8002}]
            2022-06-25 17:56:17.398 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService   : [ConsumerFeignService#list] <--- END HTTP (83-byte body)