通过前面的学习,我们基本掌握了微服务架构中如何使用 SpringCloud Ribbon 和 SpringCloud Hystrix 来实现客户端负载均衡的服务调用、通过断路器来保护我们的微服务应用。接下来,介绍一款重磅武器 SpringCloud Feign,它是更高层次的封装并简化了以上两个基本工具。它不仅整合了 SpringCloud Ribbon 和 SpringCloud Hystrix,还提供了一种声明式的 Web 服务客户端定义方式。
问:为什么使用 Feign?
答:我们在使用 Ribbon 的时候,通常利用它对 RestTemplate 的请求拦截来实现对依赖服务的接口调用,RestTemplate 是对 Http 请求进行封装过的。在实际项目中,我们对服务的调用往往有多处,如果还使用 RestTemplate,我们就需要在每一处地方都注入,俗称“重复造轮子”。使用 SpringCloud Feign,我们只需创建一个接口并用注解的方式配置它,就可以完成对服务提供方的接口绑定,简化了使用 Ribbon 时自行封装服务调用客户端的开发量。Feign 为了适应 Spring 的广大用户,还在 Netflix Feign 的基础上增加对 SpringMVC 的注解支持,对一些编码器和解码器,它还可以拔插式方式提供,这些都是福音啊。
接下来,开始学习 SpringCloud Feign。
首先创建一个 feign-consumer 模块
pom.xml 配置文件中,加入 spring-cloud-starter-openfeign 依赖,完整代码如下:
<?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">
<parent>
<artifactId>MyProject</artifactId>
<groupId>com.study</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>feign-consumer</artifactId>
<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>
<version>${netflix.eureka.client.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
创建 com.study 包,新建 FeignConsumerApplication 启动类,加上 @EnableFeignClients 注解开启 Feign 的功能。
注意:如果未能识别 @EnableFeignClients 注解,一般是 SpringCloud 版本选择不对的问题!按照本系列教程的来,版本选择 Finchley.SR2。之前笔者试过 Finchley.SR4 版本,一直未能识别 @EnableFeignClients 注解,且网上寻找了一番,无果。为了降低学习成本,请保持与本系列教程版本一致!
完整代码如下:
package com.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-21 下午 11:06
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignConsumerApplication.class,args);
}
}
接下来,创建一个接口:SayHelloService,添加注解 @FeignClient(“服务名”) 指定服务名来绑定服务,然后再使用 SpringMVC 的注解来绑定具体该服务提供的 Rest 接口。我们先在 study 包上创建一个 service 包,再创建 SayHelloService 接口。
注意,我们添加的 @FeignClient(“服务名”) 服务名是 eureka-client 的服务名:
完整 SayHelloService 接口的代码如下:
package com.study.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-24 下午 11:29
*/
@FeignClient("eureka-client-biandan")
public interface SayHelloService {
//调用上述服务的 say() 方法
@RequestMapping("/say")
String sayHelloFrom();
}
接下来,我们创建一个类:FeignController 来调用 Feign 客户端的调用,使用 @Autowired 注解直接注入上面定义的 SayHelloService 实例,然后定义一个方法去调用绑定了 eureka-client-biandan 服务接口的客户端,来向该服务发起 say 接口的调用。
FeignController 完整代码如下:
说明:我们看到 sayHelloService 实例报错,可以忽略这个编译报错。因为这个实例是在服务启动的时候才注入。如果有强迫症要去掉这个编译报错,可以把 @Autowired 改成:@Autowired(required = false)
package com.study.controller;
import com.study.service.SayHelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-24 下午 11:47
*/
@RestController
public class FeignController {
@Autowired(required = false)
private SayHelloService sayHelloService;
@RequestMapping(value = "/feign")
public String feignConsumer(){
return sayHelloService.sayHelloFrom();
}
}
接下来,在 eureka-feign 模块创建一个 application.yml 配置文件,端口:10001
# 这是客户端服务的配置节点
server:
port: 10001
eureka:
instance:
hostname: main.study.com
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
client:
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:8000/eureka/
# 服务的名字
spring:
application:
name: eureka-feign-client
注意,我们之前在测试 Ribbon 的时候,在 eureka-client 模块的 EurekaClientApplication 增加了一个线程休眠的代码,需要注释掉:
接下来,像测试 Ribbon 一样,我们依次启动服务:注册中心(EurekaServerApplication)、注册中心集群(EurekaServerClusterApplication 可不启动)、服务提供者(EurekaClientApplication 9000端口、然后修改端口号=9001,再启动,一共2个服务。具体方法见之前的博客)、Feign服务消费者(FeignConsumerApplication)。
浏览器访问地址:http://main.study.com:10001/feign 浏览器显示结果(多次刷新访问,交替显示结果):
eureka-client-biandan 说:让天下没有难写的代码!from port =9000
eureka-client-biandan 说:让天下没有难写的代码!from port =9001
上述结果验证了 Feign 具有 Ribbon 的负载均衡功能。
Ribbon 配置
问:如何在使用 SpringCloud Feign 的工程中使用 Ribbon 的配置?
答:常规的方法有以下 2 种
1、Ribbon 的全局配置
全局配置的方法比较简单,直接使用 ribbon.<key> = <value> 的方式设置 Ribbon 的各项参数即可。比如我们设置 Ribbon 的超时时间,可以在 eureka-feign 模块的 application.yml 配置文件中这样配置:
ribbon:
ConnectTimeout: 1500
ReadTimeout: 1500
2、指定服务的配置
有时候我们需要对某个服务的超时时间做实际的调整,这时候就需要指定服务来配置。基本配置语法如下:
<server>.ribbon.key = value
比如,我们需要对 eureka-client-biandan 这个服务配置超时时间
有人喜欢抬杠了,问:如果有全局配置和指定服务配置,会使用哪个配置呢?
答:经过测试,证实了使用指定服务配置。一般的系统配置都这样:指定配置 > 全局配置。
我们改造一下 eureka-client 模块的 EurekaClientApplication 类,加入线程休眠,模拟服务超时的情况:
接下来,我们依次重新启动服务:服务提供者(EurekaClientApplication 9000端口、然后修改端口号=9001,再启动,一共2个服务。可以对着启动类鼠标右键 Debug 启动)、Feign服务消费者(FeignConsumerApplication)。
浏览器地址输入:http://main.study.com:10001/feign 会出现如下情况:
①访问访问返回 500,这时候,我们去 eureka-feign 服务的控制台看下错误信息
发现 eureka-feign 的控制台出现读取超时的情况,然后我们去到 eureka-client 端口为 9000 和 9001 两个服务的控制台看下信息:
我们发现这 2 个服务线程都超过了我们设置的 1000 毫秒(指定服务的配置),所以出现读取超时的情况。
多次刷新,我们会看到正常的情况:
eureka-client-biandan 说:让天下没有难写的代码!from port =9001
这时候,我们去 eureka-client 端口是 9001 的服务控制台看下信息:
②其中一个服务超时,另一个服务正常的情况。比如端口 9000 的服务线程休眠了 1200 毫秒,这时候 Feign 通过重试机制,去调用端口 9001 的服务,假如 9001 的服务线程只休眠了 50 毫秒,则可以返回正常结果。
所以,重试机制对于高可用的服务集群来说非常重要。
注意:Ribbon 的超时和 Hystrix 的超时熔断是两个不同的概念。Hystrix 的超时直接熔断,也就是返回 fallbackMethod 里我们自定义的返回错误信息的方法。比如我们系列教程代码如下:@HystrixCommand(fallbackMethod = "errorFallBack")
返回的是我们自定义的 errorFallBack() 方法。我们需要让 Hystrix 的超时熔断时间大于 Ribbon 的超时重试时间,否则 Hystrix 超时熔断之后直接返回错误信息,重试机制就没有意义了。
Hystrix 配置
默认的情况下,SpringCloud Feign 会为将所有 Feign 客户端的方法都封装到 Hystrix 命令中进行服务保护。那么使用 Feign 时如何配置 Hystrix 的属性以及如何实现服务降级呢?
1、全局配置
Feign 对于 Hystrix 的全局配置同 SpringCloud Ribbon 的全局配置一样,直接使用它的默认配置前缀 hystrix.command.default 就可以进行设置。比如设置全局的超时时间:hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 5000
关闭 Hystrix 熔断功能有两种配置方式,见如下代码:
# 全局 hystrix 配置
hystrix:
command:
default:
execution:
timeout:
enabled: false #关闭熔断功能
isolation:
thread:
timeoutInMilliseconds: 500 #设置超时熔断时间
# 关闭 Hystrix 熔断功能
feign:
hystrix:
enabled: false
2、指定命令配置
一般采用 hystrix.command.<commandKey> 作为前缀,而 <commandKey> 默认情况下采用 Feign 客户端中的方法名作为标识。如我们上面的例子对 SayHelloService 接口的 sayHelloFrom() 方法的熔断超时时间配置设置如下:
hystrix:
command:
sayHelloFrom:
isolation:
thread:
timeoutInMilliseconds: 8000 #设置超时熔断时间
注意:在使用指定命令配置的时候,方法名可能重复,这时候相同的方法的 Hystrix 配置会共用。如果想单独配置,可以重写 Feign.Builder 的实现,并在应用的主类中创建它的实例来覆盖自动化配置的 HystrixFeign.Builder 实现。
服务降级配置
Hystrix 提供的服务降级是服务容错的重要功能,Feign 在定义服务客户端时,HystrixCommand 的定义被封装了起来,我们就不能像之前介绍 Hystrix 时那样通过 @HystrixCommand 注解的 fallback 参数来指定具体的服务降级处理方法。但是,Feign 提供了另外一种更简单的方式。我们来改造一下 eureka-feign 模块。
首先我们在 service 包下创建一个包:impl,然后在 impl 包里创建一个 SayHelloService 的实现类:SayHelloServiceImpl
SayHelloServiceImpl 完整代码如下:
package com.study.service.impl;
import com.study.service.SayHelloService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-26 下午 9:23
*/
@Component
public class SayHelloServiceImpl implements SayHelloService {
@Value("{spring.application.name}")
private String serverName;
@Override
public String sayHelloFrom() {
return serverName + " 说:请求接口超时。";
}
}
说明:服务降级逻辑实现只需要为 Feign 客户端的定义接口编写一个实现类。比如 SayHelloService 接口的一个服务降级类:SayHelloServiceImpl ,每个重写的方法都可以用来定义相应的服务降级逻辑。
接下来,改造 SayHelloService 接口。通过 @FeignClient 的注解的 fallback 属性来指定对应的服务降级实现类。
package com.study.service;
import com.study.service.impl.SayHelloServiceImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-24 下午 11:29
*/
@FeignClient(value = "eureka-client-biandan",fallback = SayHelloServiceImpl.class)
public interface SayHelloService {
//调用上述服务的 say() 方法
@RequestMapping("/say")
String sayHelloFrom();
}
注意这时候的 application.yml 的完整配置如下,默认熔断的时间是 500 毫秒:
# 这是客户端服务的配置节点
server:
port: 10001
eureka:
instance:
hostname: main.study.com
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
client:
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:8000/eureka/
# 服务的名字
spring:
application:
name: eureka-feign-client
# 全局 Ribbon 配置
ribbon:
ConnectTimeout: 1500
ReadTimeout: 1500
# 全局 hystrix 配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2500 #设置超时熔断时间
# 开启 Hystrix 熔断功能
feign:
hystrix:
enabled: true
接下来,我们测试。重启 Feign 服务(FeignConsumerApplication)。然后停掉 eureka-client 两个服务(端口分别是 9000 和 9001 两个服务),来模拟请求超时的情况。浏览器访问地址:http://main.study.com:10001/feign 可以看到如下情况:
eureka-feign-client 说:请求接口超时。
这样一来,我们就实现类服务降级的效果。也就是 Feign 实现了 Hystrix 的熔断功能。