一、Feign是什么?二、Feign的快速搭建三、Feign的几种姿态 参数绑定 继承特性四、其他配置 Ribbon 配置 Hystrix 配置
一、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请求到, 触发 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服务网关