2023-08-07
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/264

从本章开始,我将讲解Spring Cloud中的另一个组件—— Feign 。Feign是什么?能解决什么样的问题?

回顾一下我们之前使用RestTemplate + Ribbon + Eureka的方式来进行服务间的调用,每次调用服务接口,都必须像下面这样写代码:

    @RestController
    public class ServiceBController {
        @Bean
        @LoadBalanced
        public RestTemplate getRestTemplate() {
            return new RestTemplate();
        }
    
        @GetMapping(value = "/greeting/{name}")
        public String greeting(@PathVariable("name") String name) {
            RestTemplate restTemplate = getRestTemplate();
            // ServiceA是服务提供方向Eureka注册的应用名
            return restTemplate.getForObject("http://ServiceA/sayHello/" + name, String.class);
        }
    }

显然,这是一种低效的方式,针对服务提供方ServiceA的每一个对外接口,我们都要硬编码。理想的微服务架构中,ServiceA应该提供一个接口存根jar包,然后服务调用方直接引用一个jar包,并使用一些注解即可完成调用。

Feign要解决的就是这个问题,它就是一个 声明式服务调用框架

一、基本使用

我们来看下Feign的基本使用。我将创建以下应用:

  • eureka-server:服务注册中心
  • serviceA:服务提供者
  • service-a-api:服务提供者API存根
  • serviceB:服务调用者

更多Spring Cloud Feign的使用介绍,请参考Spring官方文档:https://docs.spring.io/spring-cloud-openfeign/docs/2.2.5.RELEASE/reference/html/。

1.1 eureka-server

eureka-server就是一个普通的Eureka注册中心。

启动类

    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaServer {
        public static void main(String[] args) {
            SpringApplication.run(EurekaServer.class, args);
        }
    }

配置文件

    server:
      port: 8761
    eureka:
      client:
        registerWithEureka: false
        fetchRegistry: false

pom依赖

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
    
          <groupId>com.tpvlog</groupId>
          <artifactId>eureka-server</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <packaging>jar</packaging>
    
          <name>eureka-server</name>
         <url>http://maven.apache.org</url>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.3.1.RELEASE</version>
        </parent>
    
        <properties>
            <spring.cloud-version>Hoxton.SR8</spring.cloud-version>
        </properties>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring.cloud-version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
          <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            </dependency>
        </dependencies>
    </project>

1.2 service-a-api

service-a-api定义了ServiceA对外的接口。

pom依赖

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
    
          <groupId>com.tpvlog</groupId>
          <artifactId>service-a-api</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <packaging>jar</packaging>
    
          <name>service-a-api</name>
          <url>http://maven.apache.org</url>
    
          <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          </properties>
    
          <distributionManagement>
            <repository>
                <id>nexus-releases</id>
                <name>Nexus Release Repository</name>
                <url>http://localhost:8081/repository/maven-releases/</url>
            </repository>
            <snapshotRepository>
                <id>nexus-snapshots</id>
                <name>Nexus Snapshot Repository</name>
                <url>http://localhost:8081/repository/maven-snapshots/</url>
            </snapshotRepository>
        </distributionManagement>
    
          <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.3.1.RELEASE</version>
            </dependency>
          </dependencies>
    </project>

服务接口

    @RequestMapping("/user")  
    public interface ServiceAInterface {
    
        @RequestMapping(value = "/sayHello/{id}", method = RequestMethod.GET)
        String sayHello(@PathVariable("id") Long id, 
                @RequestParam("name") String name, 
                @RequestParam("age") Integer age);
    
        @RequestMapping(value = "/", method = RequestMethod.POST)
        String createUser(@RequestBody User user);
    
        @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
        String updateUser(@PathVariable("id") Long id, @RequestBody User user);
    
        @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
        String deleteUser(@PathVariable("id") Long id);
    
        @RequestMapping(value = "/{id}", method = RequestMethod.GET)
        User getById(@PathVariable("id") Long id);
    
    }
    public class User {
        private Long id;
        private String name;
        private Integer age;
    
        //...
    }

1.3 serviceA

ServiceA就是一个普通的Spring Boot应用。

启动类

    @SpringBootApplication
    @EnableEurekaClient
    public class ServiceAApplication {
        public static void main(String[] args) {
            SpringApplication.run(ServiceAApplication.class, args);
        }
    }

配置文件

    server:
      port: 8088
    spring:
      application:
        name: ServiceA
    eureka:
      instance:
        hostname: localhost
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka

pom依赖

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
    
          <groupId>com.tpvlog</groupId>
          <artifactId>serviceA</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <packaging>jar</packaging>
    
          <name>serviceA</name>
          <url>http://maven.apache.org</url>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.3.1.RELEASE</version>
        </parent>
    
        <properties>
            <spring.cloud-version>Hoxton.SR8</spring.cloud-version>
        </properties>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring.cloud-version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>com.tpvlog</groupId>
                <artifactId>service-a-api</artifactId>
                <version>0.0.1-SNAPSHOT</version>
            </dependency>
        </dependencies>
    </project>

服务接口实现

    @RestController
    public class ServiceAController implements ServiceAInterface {
    
        public String sayHello(@PathVariable("id") Long id, @RequestParam("name") String name, 
                @RequestParam("age") Integer age) {     
            System.out.println("打招呼,id=" + id + ", name=" + name + ", age=" + age);   
            return "{'msg': 'hello, " + name + "'}";  
        }
    
        public String createUser(@RequestBody User user) {
            System.out.println("创建用户," + user);  
            return "{'msg': 'success'}";
        }
    
        public String updateUser(@PathVariable("id") Long id, @RequestBody User user) {
            System.out.println("更新用户," + user);  
            return "{'msg': 'success'}";
        }
    
        public String deleteUser(@PathVariable("id") Long id) {
            System.out.println("删除用户,id=" + id);
            return "{'msg': 'success'}"; 
        }
    
        public User getById(@PathVariable("id") Long id) {
            System.out.println("查询用户,id=" + id);
            return new User(1L, "张三", 20);
        }
    }

1.4 serviceB

serviceB就是服务调用方,依赖了Feign实现声明式服务调用。

启动类

    @SpringBootApplication
    @EnableEurekaClient
    @EnableFeignClients
    public class ServiceBApplication {
        public static void main(String[] args) {
            SpringApplication.run(ServiceBApplication.class, args);
        }
    }

配置文件

    server:
      port: 9090
    spring:
      application:
        name: ServiceB
    eureka:
      instance:
        hostname: localhost
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka

pom依赖

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
    
          <groupId>com.tpvlog</groupId>
          <artifactId>serviceB</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <packaging>jar</packaging>
    
          <name>serviceB</name>
          <url>http://maven.apache.org</url>
    
          <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          </properties>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.6.RELEASE</version>
        </parent>
    
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Hoxton.SR8</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <dependency>
                <groupId>com.tpvlog</groupId>
                <artifactId>service-a-api</artifactId>
                <version>0.0.1-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-netflix-eureka-client</artifactId>
                <version>2.2.5.RELEASE</version>
                <scope>compile</scope>
            </dependency>
        </dependencies>
    </project>

服务调用

首先,定义一个Feign客户端,继承ServiceA接口存根:

    @FeignClient("ServiceA")    // ServiceA就是服务A的名称
    public interface ServiceAClient extends ServiceAInterface {
    }
    @RestController
    @RequestMapping("/ServiceB/user")  
    public class ServiceBController {    
        // 注入Feign客户端
        @Autowired
        private ServiceAClient serviceA;
    
        @RequestMapping(value = "/sayHello/{id}", method = RequestMethod.GET)
        public String greeting(@PathVariable("id") Long id, 
                @RequestParam("name") String name, 
                @RequestParam("age") Integer age) {
            return serviceA.sayHello(id, name, age);
        }
    
        @RequestMapping(value = "/", method = RequestMethod.POST)
        public String createUser(@RequestBody User user) {
            return serviceA.createUser(user);
        }
    
        @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
        public String updateUser(@PathVariable("id") Long id, @RequestBody User user) {
            return serviceA.updateUser(id, user); 
        }
    
        @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
        public String deleteUser(@PathVariable("id") Long id) {
            return serviceA.deleteUser(id);
        }
    
        @RequestMapping(value = "/{id}", method = RequestMethod.GET)
        public User getById(@PathVariable("id") Long id) {
            return serviceA.getById(id);
        }
    }

上面的@FeignClient注解的value表示要消费的服务名称,是在服务提供方的yml文件中配置的。Feign会自动生成访问后台服务的代理接口服务,后续我讲Feign原理时会专门讲解。

二、核心组件

Feign就跟Ribbon一样,内部都包含了很多的核心组件:

  • 编码器(Encoder) :如果调用接口时,传递的参数是个对象,Feign会将这个对象进行编码,转换成JSON格式;
  • 解码器(Decoder) :接受到响应后,将JSON转换为一个对象;
  • Logger :负责日志打印,即打印这个接口调用的详细请求,包含请求、响应等等;
  • Contract :契约组件,Feign使用时一般会用到SpringMVC相关的注解,这个组件就负责Feign的原生注解与SpringMVC注解之间的转化;
  • Feign.Builder :FeignClient的一个实例构造器,这是Builder设计模式的典型实现;
  • FeignClient :Feign客户端,里面包含了上述的一系列组件,可类比Ribbon客户端理解。

Spring Cloud对上述的这些Feign的组件都有默认的实现:

  • Encoder:SpringEncoder;
  • Decoder:ResponseEntityDecoder;
  • Logger:Slf4jLogger;
  • Contract:SpringMvcContract;
  • Feign.Builder:HystrixFeign.Builder;
  • FeignClient:LoadBalancerFeignClient;

2.1 自定义组件

除了这些默认组件外,我们也可以自定义实现:

    @FeignClient(name = "ServiceA", configuration = FooConfiguration.class)
    public interface ServiceAClient {
        //..
    }
    public class FooConfiguration {
        // 配置拦截器
        @Bean
        public RequestInterceptor requestInterceptor() {
            return new MyRequestInterceptor();
        }
    
        // 配置日志级别:none,basic,headers,full
        @Bean
        Logger.Level feignLoggerLevel() {
            return Logger.Level.FULL;
        }
    }

Spring会为每一个Feign客户端创建一个独立上下文ApplicationContext,默认这些客户端的组件都依赖FeignClientsConfiguration配置,我们可以通过自定义配置的方式替换掉部分默认配置给我们生成的Feign组件。

2.2 客户端配置

Feign使用时必然会涉及到很多参数的配置,这些参数大部分都有默认值,我们也可以通过下面的方式进行指定:

    feign:
      client:
        config:
          ServiceA:    # 针对指定Feign客户端进行配置
            connectTimeout: 5000
            readTimeout: 5000
            loggerLevel: full
            decode404: false
    
    feign:
      client:
        config:
          default:    # 全局默认配置
            connectTimeout: 5000
            readTimeout: 5000
            loggerLevel: full

三、总结

最后,我用一张图简单说下Feign的核心工作流程,以便大家有一个初步印象。这张图并不完全准确,但可以帮助大家理解Feign,也能说明Feign的核心思想其实就是 动态代理 ,为我们后面研究它的源码打下基础:

202308072153116991.png

阅读全文