一、Feign是什么?
通过对前面Spring Cloud Ribbon
和 Spring Cloud Hystrix
,我们已经掌握了开发微服务应用时的两个重磅武器,学会了如何在微服务框架中进行服务间的调用
和如何使用断路器
来保护我们的服务,这两者被作为基础工具类框架广泛的应用在各个微服务框架中。既然这两个组件这么重要,那么有没有更高层次的封装来整合这两个工具以简化开发呢?Spring Cloud Feign
就是这样的一个工具,它整合了Spring Cloud Ribbon 和 Spring Cloud Hystrix 来达到简化开发的目的。
我们在使用Spring Cloud Ribbon
时,通常都会使用RestTemplate
的请求拦截来实现对依赖服务的接口调用,而RestTemplate
已经实现了对Http请求
的封装,形成了一套模板化的调用方法。在之前Ribbon
的例子中,我们都是一个接口对应一个服务调用的url,那么在实际项目开发过程中,一个url可能会被复用,也就是说,一个接口可能会被多次调用,所以有必要把复用的接口封装起来公共调用。Spring Cloud Feign
在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务的接口定义。
二、Feign的快速搭建
我们通过一个示例来看一下Feign的调用过程,下面的示例将继续使用之前的server-provider
服务,这里我们通过Spring Cloud Feign
提供的声明式服务绑定功能来实现对该服务接口的调用
- 首先,搭建一个SpringBoot项目,取名为
feign-consumer
,并在pom.xml
文件中引入spring-cloud-starter-eureka
和spring-cloud-starter-feignn
依赖,具体内容如下:
<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.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.feign.consumer</groupId>
<artifactId>feign-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>feign-consumer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.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-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-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</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>
- 搭建完成pom.xml之后,我们在
feign-consumer
的启动类上添加如下注解
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class FeignConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignConsumerApplication.class, args);
}
}
@EnableDiscoveryClient : 这个注解和@EnableEurekaClient 用法相同,表明这是一个Eureka客户端
@EnableFeignClients : 这个注解表明这个服务是一个Feign服务,能够使用@FeignClient 实现远程调用
- 新建一个
HelloService
接口,在接口上加上__@FeignClient__注解,表明这个接口是可以进行远程访问的,也表明这个接口可以实现复用的接口,它提供了一些远程调用的方法,也相当于制定了一些规则。
// 此处填写的是服务的名称
@FeignClient(value = "server-provider")
public interface HelloService {
@RequestMapping(value = "hello")
String hello();
}
@FeignClient 后面的value值指向的是提供服务的服务名,这样就能够对spring.application.name = server.provider 的服务发起服务调用
- 新建一个Controller,提供外界访问的入口,调用HelloService,完成一系列的服务请求-服务分发-服务调用
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping(value = "/feign-consumer", method = RequestMethod.GET)
public String helloConsumer(){
return helloService.hello();
}
}
- 最后,为
feign-consumer
指定服务的端口号,服务的名称,并向注册中心注册自己
spring.application.name=feign-consumer
server.port=9001
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
测试验证:
像之前一样,启动四个服务: eureka-server
, server-provider(8081,8082)
, feign-consumer
,启动http://localhost:9000/eureka/ 主页,发现主页上注册了四个服务
访问http://localhost:9001/feign-consumer 端口,发现 "Hello World" 能够返回
三、Feign的几种姿态
参数绑定
在上一节的事例中,我们使用Spring Cloud Feign搭建了一个简单的服务调用的示例,但是实际的业务场景中要比它复杂很多,我们会在HTTP的各个位置传入不同类型的参数,并且返回的也是一个复杂的对象结构,下面就来看一下不同的参数绑定方法
- 首先扩展一下
server-provider
中HelloController的内容
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
public String hello1(@RequestParam String name){
return "Hello " + name;
}
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
public User hello2(@RequestHeader Integer id,@RequestHeader String name){
return new User(id,name);
}
@RequestMapping(value = "/hello3",method = RequestMethod.POST)
public String hello3(@RequestBody User user){
return "Hello " + user.getId() + ", " + user.getName();
}
- User 对象的定义入下,省略了get和set方法,需要注意的是,这里必须要有User的默认构造函数,否则反序列化的时候,会报Json解析异常
public class User {
private Integer id;
private String name;
public User(){}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
get and set...
}
- 在
feign-consumer
中的HelloService中声明对服务提供者的调用
@FeignClient(value = "server-provider")
public interface HelloService {
@RequestMapping(value = "hello")
String hello();
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
String hello1(@RequestParam("name") String name);
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
User hello2(@RequestHeader("id") Integer id,@RequestHeader("name") String name);
@RequestMapping(value = "/hello3", method = RequestMethod.POST)
String hello3(@RequestBody User user);
}
hello1 方法传递了一个参数为name的请求参数,它对应远程调用server-provider服务中的hello1方法
hello2 方法传递了一个请求头尾id 和 name的参数,对应远程调用server-provider服务中的hello2方法
hello3 方法传递了一个请求体为user的参数,对应远程调用呢server-provider服务中的hello3方法
- 下面在ConsumerController类中定义一个helloConsumer1的方法,分别对hello1,hello2,hello3方法进行服务调用
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping(value = "/feign-consumer", method = RequestMethod.GET)
public String helloConsumer(){
return helloService.hello();
}
@RequestMapping(value = "/feign-consumer2", method = RequestMethod.GET)
public String helloConsumer1(String name){
StringBuilder builder = new StringBuilder();
builder.append(helloService.hello()).append("\n");
builder.append(helloService.hello1("lx")).append("\n");
builder.append(helloService.hello2(23,"lx")).append("\n");
builder.append(helloService.hello3(new User(24,"lx"))).append("\n");
return builder.toString();
}
}
上面的helloConsumer1方法,分别调用了HelloServcie接口中的hello、hello1、hello2、hello3方法,传递对应的参数,然后对每一个方法进行换行
测试验证
在完成上述的改造之后,启动服务注册中心
、两个 server-provider
服务以及我们改造过的feign-consumer
。通 过发送GET请求到 htttp://localhost:9001/feign-consumer2, 触发 HelloService对新增接口的调用。最终,我们会获得如下输出,代表接口绑定和调用成功。
继承特性
通过上述的示例,我们能够发现能够从服务提供方的Controller中依靠复制操作,构建出相应的服务客户端绑定接口。既然存在很多复制操作,我们自然考虑能否把公用的接口抽象出来?事实上也是可以的,Spring Cloud Feign提供了通过继承来实现Rest接口的复用,下面就来演示一下具体的操作过程
- 首先为了演示Spring Cloud Feign的
继承
特性,我们新建一个maven 项目,名为feign-service-api,我们需要用到Spring MVC的注解,所以在pom.xml 中引入spring-boot-starter-web依赖,具体内容如下:
<?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>
<groupId>com.feign</groupId>
<artifactId>feign-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
- 将User 对象复制到feign-service-api 中,如下
public class User {
private Integer id;
private String name;
// 必须加上
public User(){}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
get and set...
}
- 创建
HelloService
接口,并在接口中定义如下三个方法:
@RequestMapping(value = "/refactor")
public interface HelloService {
@RequestMapping(value = "/hello4", method = RequestMethod.GET)
String hello(@RequestParam("name")String name);
@RequestMapping(value = "/hello5", method = RequestMethod.GET)
User hello(@RequestHeader("id")Integer id,@RequestHeader("name")String name);
@RequestMapping(value = "/hello6", method = RequestMethod.POST)
String hello(@RequestBody User user);
}
- 定义完成后,使用idea 右侧的maven 工具,依次执行mvn clean ,mvn install,把feign-service-api打成
jar
包之后,现在切换项目至 server-provider ,并让server-provider
依赖这个maven项目]
server-provider
server-provider
的pom.xml 添加feign-service-api
打包后的依赖
<dependency>
<groupId>com.feign</groupId>
<artifactId>feign-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 创建
RefactorHelloController
实现feign-service-api
中的HelloService 方法
@RestController
public class RefactorHelloController implements HelloService {
// 注解没有带过来,这是自己加的
@Override
public String hello(@RequestParam("name") String name) {
return "Hello " + name;
}
@Override
public User hello(@RequestHeader("id") Integer id, @RequestHeader("name") String name) {
return new User(id,name);
}
@Override
public String hello(@RequestBody User user) {
return "Hello " + user.getId() + ", " + user.getName();
}
}
这里有一个问题,当继承了HelloService 之后,@RestController,@RequestParam,@RequestHeader,@RequestBody 注解都没有带过来, 但是书上说是只有 @RestController 注解是带不过来的,余下三个都是可以的。这里未查明是何原因 …...
feign-consumer
- 在完成了对
server-provoder
的构建之后,下面来构建feign-consumer
服务,像server-provider 一样,在pom.xml 中添加对feign-service-api
的依赖
<dependency>
<groupId>com.feign</groupId>
<artifactId>feign-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 创建
RefactorHelloService
接口,继承feign-service-api
中的 HelloService接口
@FeignClient(value = "server-provider")
public interface RefactorHelloService extends HelloService {}
- 在
ConsumerController
类注入 RefactorHelloService,并测试 feign-service-api 中的方法,远程调用server-provider 中的/hello4 /hello5 /hello6
方法。
@RequestMapping(value = "/feign-consumer3", method = RequestMethod.GET)
public String helloConsumer3(String name){
StringBuilder builder = new StringBuilder();
builder.append(refactorHelloService.hello("lx")).append("\n");
builder.append(refactorHelloService.hello(new com.feignservice.api.User(24,"lx"))).append("\n");
builder.append(refactorHelloService.hello(23,"lx")).append("\n");
return builder.toString();
}
测试验证
依次启动服务服务注册中心
,server-provider
的两个实例,feign-consumer
服务,在http://localhost:1111/ 主页能够发现如下几个服务
访问 http://localhost:9001/feign-consumer3( 使用Postman 访问),发现能够显示出来如下内容
Hello lx
Hello 24, lx
com.feignservice.api.User@5865261
优点和缺点
使用Spring Cloud Feign
的优点很多,可以将接口的定义从Controller 中剥离,同时配合Maven 构建就能轻易的实现接口的复用,实现在构建期的接口绑定,从而有效的减少服务客户端的绑定配置。但是这种配置使用不当也会带来副作用就是:你不能忽略频繁变更接口带来的影响。所以,如果团队打算采用这种方式来构建项目的话,最好在开发期间就严格遵守面向对象的开闭原则。避免牵一发而动全身,造成不必要的维护量。
四、其他配置
Ribbon 配置
由于Spring Cloud Feign
的客户端负载均衡是通过Spring Cloud Ribbon
实现的,所以我们可以通过配置Spring Cloud Feign 从而配置 Spring Cloud Ribbon 。
全局配置
全局配置的方法很简单,我们可以使用如下配置来设置全局参数
ribbon.ConnectTimeout=5000
ribbon.ReadTimeout=5000
指定服务配置
大多数情况下,我们对于服务的调用时间可能会根据实际服务特性来做一些调整,所以仅仅依靠全局的配置是不行的,因为Feign 这个组件是整合了 Ribbon和 Hystrix的,所以通过设置Feign的属性来达到属性传递的目的。在定义Feign 客户端的时候,我们使用了@FeignClient()
注解,其实在创建@FeignClient(value = server-provider
)的时候,同时也创建了一个名为server-provider
的ribbon 客户端,所以我们就可以使用@FeignClient中的nane 和value 值来设置对应的Ribbon 参数。
# 使用feign-clients 中的注解的value值设置如下参数
# HttpClient 的连接超时时间
server-provider.ribbon.ConnectTimeout=500
# HttpClient 的读取超时时间
server-provider.ribbon.ReadTimeout=2000
# 是否可以为此客户端重试所有操作
server-provider.ribbon.OkToRetryOnAllOperations=true
# 要重试的下一个服务器的最大数量(不包括第一个服务器)
server-provider.ribbon.MaxAutoRetriesNextServer=2
# 同一个服务器上的最大尝试次数(不包括第一个)
server-provider.ribbon.MaxAutoRetries=1
重试机制
Spring Cloud Feign 中实现了默认的请求重试机制,我们可以通过修改server-provider
中的示例做一些验证:
- 在
server-provider
应用中的/hello
接口实现中,增加一些随机延迟,比如
@RequestMapping(value = "hello", method = RequestMethod.GET)
public String hello() throws Exception{
ServiceInstance instance = discoveryClient.getLocalServiceInstance();
log.info("instance.host = " + instance.getHost() + "instance.service = " + instance.getServiceId()
+ "instance.port = " + instance.getPort());
log.info("Thread sleep ... ");
int sleepTime = new Random().nextInt(3000);
log.info("sleepTime = " + sleepTime);
Thread.sleep(sleepTime);
System.out.println("Thread awake");
return "Hello World";
}
- 在
feign-consumer
应用中增加上文提到的重试配置参数,来解释一下上面的配置
MaxAutoRetriesNextServer 设置为2 表示的是下一个服务器的最大数量,也就是说如果调用失败,会更换两次实例进行重试,MaxAutoRetries设置为1 表示的是每一个实例会进行一次调用,失败了再换为其他实例。OKToRetryOnAllOperations的意义是无论是请求超时或者socket read timeout都进行重试,
这里需要注意一点,Ribbon超时和Hystrix超时是两个概念,为了让上述实现有效,我们需要 让Hystrix的超时时间大于Ribbon的超时时间, 否则Hystrix命令超时后, 该命令直接熔断,重试机制就没有任何意义了。
Hystrix 配置
在Spring Cloud Feign
中,除了引入Spring Cloud Ribbon
外,还引入了服务保护工具Spring Cloud Hystrix
,下面就来介绍一下如何使用Spring Cloud Feign配置Hystrix属性实现服务降级。
全局配置
对于Hystrix全局配置同Spring Cloud Ribbon 的全局配置一样,直接使用默认前缀 hystrix.command.default 就可以进行配置,比如设置全局的超时
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
另外,在对Hystrix进行配置之前,我们需要确认feign.hystrix.enable
参数没有设置为false,否则该参数设置会关闭Feign客户端的Hystrix支持。
// 关闭Hystrix 功能(全局关闭)
feign.hystrix.enabled=false
// 关闭熔断功能
hystrix.command.default.execution.timeout.enabled=false
禁用hystrix
如果不想全局地关闭Hystrix支持,而只想针对某个服务客户端关闭Hystrix支持,需要通过使用@Scope("prototype")
注解为指定的客户端配置Feign.Builder 实例
- 构建一个关闭Hystrix的配置类
@Configuration
public class DisableHystrixConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder builder(){
return new Feign.Builder();
}
}
- 在
HelloService
的@FeignClient注解中,通过Configuration参数引入上面实现的配置
@FeignClient(value = "server-provider", fallback = DisableHystrixConfiguration.class)
public interface RefactorHelloService extends HelloService {}
服务降级配置
Hystrix 提供的服务降级是服务容错的重要功能,之前我们开启Ribbon
的服务降级是通过使用 @HystrixCommand(fallbackMethod = "hystrixCallBack")
开启的,Feign
对Ribbon
进行了封装,所以Feign 也提供了一种服务降级策略。下面我们就来看一下Feign 如何使用服务降级策略。我们在feign-consumer
中进行改造
- 服务降级逻辑的实现只需要为Feign客户端的定义接口编写一个具体的接口实现类,比如为
server-provider
接口实现一个服务降级类HelloServiceFallback
,其中每个重写方法的逻辑都可以用来定义相应的服务降级逻辑,具体代码如下
@Component
public class FeignServiceCallback implements FeignService{
@Override
public String hello() {
return "error";
}
@Override
public String hello(@RequestParam("name") String name) {
return "error";
}
@Override
public User hello(@RequestHeader("id") Integer id, @RequestHeader("name") String name) {
return new User(0,"未知");
}
@Override
public String hello(@RequestBody User user) {
return "error";
}
}
- 在服务绑定接口中,通过
@FeignClient
注解的fallback 属性来指定对应的服务降级类
@FeignClient(value = "server-provider",fallback = FeignServiceCallback.class)
public interface FeignService {
@RequestMapping(value = "/hello")
String hello();
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
String hello(@RequestParam("name") String name);
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
User hello(@RequestHeader("id") Integer id,@RequestHeader("name") String name);
@RequestMapping(value = "/hello3", method = RequestMethod.POST)
String hello(@RequestBody User user);
}
测试验证
下面我们来验证一下服务降级逻辑,启动注册中心Eureka-server
,服务消费者feign-consumer
,不启动server-provider
,发送GET 请求到http://localhost:9001/feign-consumer2,该接口会分别调用FeignService中的四个接口,因为feign-consumer
没有启动,会直接触发服务降级,使用Postman调用接口的返回值如下
error
error
error
com.feign.consumer.pojo.User@5ac0702f
后记: Spring Cloud Feign 声明式服务调用就先介绍到这里,下一篇介绍Spring Cloud Zuul服务网关
《Spring Cloud 微服务实战》