写在前面

在前面我们学习了SpringCloud Ribbon和SpringCloud Hystrix这两个非常重要的组件,学会了如何在微服务架构中实现客户端负载均衡的服务调用以及如何通过断路器来保护我们的微服务应用。这两者通常是作为基础工具类框架被广泛运用在各个微服务的实现中,不仅包括我们自身的业务类微服务,也包括一些基础设施类微服务,如后面要学习的网关。同时经过大量的实践,我们发现这两个框架几乎都是同时出现的,那么问题来了,我们是否可以将这两个工具进行更深层次的封装用以简化开发呢?答案是肯定的,俗话说的好,前人种树后人乘凉,接下来就学习基于这两个工具的更深层次封装的组件Spring Cloud Feign。Spring Cloud Feign基于Netflix Feign实现,它整合了SpringCloud Ribbon和SpringCloud Hystrix,并在此基础上提供了一种声明式的Web服务客户端定义方式。

Spring Cloud Feign简介

在前面我们学习Spring Cloud Ribbon的时候,通常都会利用它对RestTemplate的请求拦截来实现对依赖服务的接口调用,而RestTemplate已经实现了对HTTP请求的封装处理,就像我们之前在MovieService类中定义的各个方法中的逻辑一样,使用了一套类似于模板化的调用方法。而且前面对于RestTemplate的调用实现也只是简单的进行了介绍,而在实际开发过程中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以我们通常都会针对各个微服务自行封装一些客户端类来包装这些依赖服务的调用。

这个时候我们就会发现一个问题,由于RestTemplate的封装,使得几乎每一个调用都是简单的模板化内容,因此为了解决这个问题,Spring Cloud Feign在此基础上做了进一步的封装,它用来帮助我们定义和实现依赖服务接口的定义。

在Spring Cloud Feign的实现下,我们只需创建一个接口并用注解方式来配置它,即可完成对服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时需要自行封装服务调用客户端的工作。

Spring Cloud Feign具备可插拔的注解支持,包括Feign注解和JAX-RS注解。同时为了适应Spring的广大用户,它在Netflix Feign的基础上扩展了对SpringMVC的注解支持,大大减少了学习它的成本,此外对于Feign自身的一些主要组件,如编码器和解码器等,它也提供了可插拔的方式,便于开发者扩展和替换它们。

快速入门

个人觉得对于新知识来说,首先是通过demo来直观感受它的使用是后期学习它的动力,因此接下来将通过一个简单的demo来演示Spring Cloud Feign在服务客户端定义上带来的便捷之处。

还是老规矩,既然是微服务,那么就需要和之前的内容进行解耦,因此这里依旧是新建一个新的Spring Boot工程,然后基于前面介绍的hello-service项目(即服务提供者),通过Spring Cloud Feign提供的声明式服务绑定功能来实现对该服务接口的调用。

第一步,在Gitte上新建一个分支,名称为feign-consumer,并将其pull到本地;

第二步,新建一个普通的SpringBoot工程,名称为feign-consumer,注意使用默认的依赖。

第三步,添加项目依赖。Spring Boot工程创建完成后,修改pom.xml文件,添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR7</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
</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>

请注意Spring Boot2.x系列中一些组件的依赖名称发生了变化,此处的Feign所导入的依赖为spring-cloud-starter-openfeign这个和Spring Boot1.x系列中完全不同。

当然了也可以采用可视化的方式来创建该项目,创建过程如下所示:

第四步,在feign-consumer项目入口类上添加启动注解。接下来在项目的入口类上添加@EnableDiscoveryClient注解,用于开启客户端发现功能;同时还需要添加@EnableFeignClients注解,用于开启Spring Cloud Feign的支持功能,如下所示:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignConsumerApplication.class, args);
}
}

第五步,声明服务(正常这是第六步)。在feign-consumer项目目录下新建一个service包,并在里面新建一个HelloService的接口文件,里面的代码为:

1
2
3
4
5
@FeignClient("hello-service")
public interface HelloService {
@RequestMapping(value = "/world")
String world();
}

可以看到在上面的接口上,我们使用了@FeignClient("hello-service")注解并在其中通过指定服务名来绑定服务,然后再使用Spring MVC的@RequestMapping注解来绑定具体该服务提供的REST接口。

请注意这里的服务名不区分大小写,因此使用hello-service和HELLO-SERVICE都是一样的。

第六步,提供服务(正常这是第五步)。正如前面第五步所说的,接下来我们需要回到hello-service项目(即服务提供者)中,它的服务名必须为hello-service,且必须提供一个/world接口,因此最简单的就是直接在其现有的HelloController类中定义一个/world接口,里面的代码为:

1
2
3
4
@GetMapping(value = "/world")
public String world(){
return "world";
}

请注意其中的方法名和接口名必须和你在HelloService接口文件中定义的保持一致。(准确来说是第五步接口中的方法名和接口必须和第六步中的方法名和接口保持一致)

第七步,调用服务。在feign-consumer项目目录下创建controller包,并在里面新建ConsumerController类,用于实现对Feign客户端的调用。同时需要注入之前定义的HelloService实例,并在helloConsumer方法中调用这个绑定了hello-service服务接口的客户端来向该服务发起/world接口的调用。相应的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class ConsumerController {

@Autowired
private HelloService helloService;

@GetMapping(value = "/feign-consumer")
public String helloConsumer(){
return helloService.world();
}
}

第八步,属性配置。在feign-consumer项目的application.yml配置文件中设置自身服务名、启动端口号和注册中心地址等信息,相应的信息为:

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: feign-consumer
server:
port: 3001

eureka:
client:
service-url:
defaultZone: http://peer1:1111/eureka/

第九步,启动测试。分别按照以下顺序来启动相应的服务,启动顺序如下(注意不能颠倒):
(1)服务名为eureka-server的工程,这是服务注册中心,注意端口为1111,即启动实例名为peer1的服务实例。
(2)服务名为hello-service的工程,这是服务提供者,注意需要启动两个实例,启动的端口分别为8081和8082,即启动实例名为p1和p2的服务实例(注意两个服务提供者均需往服务注册中心注册)。
(3)服务名为feign-consumer的工程,这是服务消费者,注意只启动一个实例,启动的端口为3001,即启动实例名为feign-consumer的服务实例(注意这个服务消费者需往服务注册中心注册)。

然后在浏览器地址栏中输入http://localhost:1111/,可以看到信息面板显示如下信息:

接着多次访问http://localhost:3001/feign-consumer链接,可以得到之前和使用Ribbon一样的效果,都返回了world。然后观察feign-consumer的控制台输出信息,可以发现Feign实现的服务消费者依然是使用Ribbon维护了针对hello-service的服务列表信息,并且通过轮询方式实现了客户端负载均衡。注意与Ribbon不同的是通过Feign我们只需要定义服务绑定接口,然后以声明式的方法优雅地实现了服务的调用,避免了之前多次出现模板式的RestTemplate代码。

也就是说使用Feign我们对于服务提供者的调用采取的是如下方式:

1
2
3
4
5
@FeignClient("hello-service")
public interface HelloService {
@RequestMapping(value = "/world")
String world();
}

而使用Ribbon,那么我们采取的调用方式则如下所示:

1
2
3
public String world(){
return restTemplate.getForObject("http://HELLO-SERVICE/world}",String.class);
}

之后两者都是在Controller类中定义相关API来调用此方法。