转载

Spring Cloud实战系列(三) - 声明式客户端调用Feign

上一篇文章,讲述了如何通过 RestTemplate 配合 Ribbon 去消费服务。 Feign 是一个 声明式 的 HTTP 伪客户端 ,提供 面向接口 的 HTTP 客户端调用 编程。本文进一步讲如何通过 Feign 去消费服务。

  • Feign 只需要创建一个 接口 并提供 注解 即可调用。

  • Feign 具有 可插拔 的注解特性,可使用 Feign 注解 和 JAX-RS 注解 。

  • Feign 支持 可插拔 的 编码器 和 解码器 。

  • Feign 默认集成了 Ribbon ,可以和 Eureka 结合使用,默认实现了 负载均衡 的效果。

正文

1. 创建服务契约模块

创建一个 service-contract 的项目 Module ,创建完成后的 pom.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.ostenant.github.springcloud</groupId>
    <artifactId>service-contract</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-contract</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

        <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.ostenant.github.springcloud</groupId>
            <artifactId>service-contract</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
复制代码

在 service-contract 中定义 业务接口 和相关的 DTO 对象如下:

User.java

public class User implements Serializable {
    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void getName() {
        return this.name;
    }

    public String setName() {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
复制代码

UserContract.java

UserContract 定义了 User 的所有行为,是一个使用 @FeignClient 注解标记的 声明式服务接口 。其中, @FeignClient 的 value 指定的是 服务提供者 的 服务名称 。

@FeignClient("service-provider")
public interface UserContract {
    @PostMapping("/user")
    void add(@RequestBody User user);

    @GetMapping("/user/{name}")  
    User findByName(@PathVariable String name);

    @GetMapping("/users")
    List<User> findAll();
}
复制代码

对于 服务提供者 而言,需要实现 UserContract 接口的方法;对于 服务消费者 而言,可以直接注入 UserContract 作为 客户端桩 使用。

2. 创建服务提供者

创建一个 service-provider 的项目 Module ,创建完成后引入 服务契约模块 的依赖, pom.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.ostenant.github.springcloud</groupId>
    <artifactId>service-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-provider</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.ostenant.github.springcloud</groupId>
            <artifactId>service-contract</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
复制代码

通过 注解 @EnableEurekaClient 表明自己是一个 Eureka Client 。

@SpringBootApplication
@EnableEurekaClient
@RestController
public class ServiceProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceProviderApplication.class, args);
    }
}
复制代码

创建一个类 UserService ,实现 UserContract 接口的具体业务,对外提供 User 相关的 HTTP 的服务。

@RestController
public class UserService implements UserContract {
    private static final Set<User> userSet = new HashSet<>();

    static {
        userSet.add(new User("Alex", 28));
        userSet.add(new User("Lambert", 32));
        userSet.add(new User("Diouf", 30));
    }

    @Override
    public void add(@RequestBody User user) {
        userSet.add(user);
    }

    @Override
    public User findByName(@PathVariable String name) {
        return userSet.stream().filter(user -> {
            return user.getName().equals(name);
        }).findFirst();
    }

    @Override
    public List<User> findAll() {
        return new ArrayList<>(userSet);
    }
}
复制代码

在 配置文件 中注明的 服务注册中心 的地址, application.yml 配置文件如下:

spring:
  active:
    profiles: sp1 # sp2

---

spring:
  profiles: sp1
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8770
spring:
  application:
    name: service-provider

---

spring:
  profiles: sp2
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8771
spring:
  application:
    name: service-provider
复制代码

分别以 spring.profiles.active=sp1 和 spring.profiles.active=sp2 作为 Spring Boot 的 启动命令参数 ,在 端口号 8770 和 8771 启动 2 个 服务提供者 实例。

3. 创建服务消费者

新建一个项目 Module ,取名为 service-consumer ,在它的 pom 文件中引入 Feign 的 起步依赖 和 服务契约模块 ,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.ostenant.github.springcloud</groupId>
    <artifactId>service-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-consumer</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.ostenant.github.springcloud</groupId>
            <artifactId>service-contract</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
复制代码

在项目的配置文件 application.yml 文件,指定 应用名称 为 service-consumer , 端口号 为 8772 , 服务注册地址 为 http://localhost:8761/eureka/ ,代码如下:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8772
spring:
  application:
    name: service-feign
复制代码

在应用的启动类 ServiceConsumerApplication 上加上 @EnableFeignClients 注解开启 Feign 的功能。

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceConsumerApplication.class, args);
    }
}
复制代码

定义一个 UserController 控制器,用于调用 服务提供者 提供的服务并 响应 前端。

@RestController
public class UserController {
    @Autowired
    private UserContract userContract;

    @PostMapping("/user")
    public void add(@RequestBody User user) {
        userContract.add(user);
    }

    @GetMapping("/user/{name}")
    public User findByName(@PathVariable String name) {
        return userContract.findByName(name);
    }

    @GetMapping("/users")
    public List<User> findAll() {
        return userContract.findAll();
    }
}
复制代码

在 控制层 UserController 引入 Feign 接口,通过 @FeignClient ( 服务名称 ),来指定调用的是哪个 服务 。

启动 服务消费者 应用,访问 http://localhost:8772/users 测试 服务消费者 的访问连通性,响应内容为:

[
    {
      "name": "Alex",
      "age": 28
    },
    {
      "name": "Lambert",
      "age": 32
    },
    {
      "name": "Diouf",
      "age": 30
    }
]
复制代码

4. Feign的源码实现过程

总的来说, Feign 的 源码实现 过程如下:

  1. 首先通过 @EnableFeignClients 注解开启 FeignClient 的功能。只有这个 注解 存在,才会在程序启动时 启动 @FeignClient 注解 的 包扫描 。

  2. 服务提供者实现基于 Feign 的 契约接口 ,并在 契约接口 上面加上 @FeignClient 注解。

  3. 服务消费者启动后,会进行 包扫描 操作,扫描所有的 @FeignClient 的 注解 的类,并将这些信息注入 Spring 上下文中。

  4. 当 接口 的方法被调用时,通过 JDK 的 代理 来生成具体的 RequestTemplate 模板对象 。

  5. 根据 RequestTemplate 再生成 HTTP 请求的 Request 对象。

  6. Request 对象交给 Client 去处理,其中 Client 内嵌的 网络请求框架 可以是 HTTPURLConnection 、 HttpClient 和 OkHttp 。

  7. 最后 Client 被封装到 LoadBalanceClient 类,这个类结合 Ribbon 完成 负载均衡 功能。

原文  https://juejin.im/post/5c4ead926fb9a049dd80ae4f
正文到此结束
Loading...