原创

透彻理解Spring Cloud系列(二六)——Feign基本使用

从本章开始,我将讲解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的核心思想其实就是动态代理,为我们后面研究它的源码打下基础:

正文到此结束

感谢赞赏~

本文目录