SpringCloudAlibaba OpenFeign
在前面,我们使用Nacos服务注册发现后,服务远程调用可以使用RestTemplate+Ribbon或者OpenFeign调用。实际开发中很少使用RestTemplate这种方式进行调用服务,每次调用需要填写地址,还要配置各种的参数,很麻烦。使用OpenFeign的方式就可以解决这种问题。
那么说起了OpenFeign,就需要提及一下Feign了,因为OpenFeign是Feign的增强版。Feign是一个轻量级Restful HTTP服务客户端,内置ribbon用作客户端负载均衡,使用Feign时只需要定义一个接口加上注解,符合面向接口的编程习惯,使远程调用服务更加容易。
OpenFeign是对Feign的进一步封装,使其支持Spring MVC的标准注解和HttpMessageConverters,如@RequestMapping等。
集成OpenFeign
在消费者客户端中集成OpenFeign
导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在bootstrap.yml
中增加OpenFeign对Sentinel的支持,增加feign.sentinel.enabled
配置项
server:
port: 9001
spring:
application:
name: consumer # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
sentinel:
transport:
port: 8719 # 启动http server,并且该服务与Sentinel仪表板进行交互,使sentinel可以控制应用,若端口占用则8719+1依次扫描
dashboard: 127.0.0.1:8080 # 仪表版访问地址
feign: # 增加对sentinel的支持
sentinel:
enabled: true
这里说一下如果不加feign.sentinel.enabled=true
的配置,那么在@FeignClient中定的fallback属性定义的异常、限流等自定义的处理逻辑不会生效
在主启动类上加入@EnableFeignClients
注解,标记为启用OpenFeign,具体如下:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Consumer {
public static void main(String[] args) {
SpringApplication.run(Consumer.class, args);
}
}
笔者这里再提一句,假如是分布式项目,将openfeign抽取为一个单独的服务模块时,可能出现我openfeign中的包名和其他模块不一致,可以自己声明FeignClient组件的basePackages
设置一下FeignClient的扫描路径。示例如下:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.alibaba.provider.feigns")
public class Consumer {
public static void main(String[] args) {
SpringApplication.run(Consumer.class, args);
}
}
增加一个FeignClient的客户端
/**
* "provider" : 表示调用的生产者服务名
* fallback:异常时进入的处理类
*/
@FeignClient(value = "provider", fallback = OpenFeignTestServiceFallback.class)
public interface OpenFeignTestService {
@RequestMapping(value = "/openFeignProviderTest", method = RequestMethod.GET)
public String openFeignProviderTest();
}
创建OpenFeignTestServiceFallback做fallback处理
@Component
public class OpenFeignTestServiceFallback implements OpenFeignTestService{
@Override
public String openFeignProviderTest() {
return "我是兜底方法";
}
}
创建controller做个接口,方便调用
@RestController
public class OpenFeignTestController {
@Resource
private OpenFeignTestService openFeignTestService;
@RequestMapping("/openFeignTest")
public String openFeignTest() {
return openFeignTestService.openFeignProviderTest();
}
}
在服务生产者方,提供接口
@RestController
public class OpenFeignProviderTest {
@RequestMapping("/openFeignProviderTest")
public String openFeignProviderTest() {
return "OpenFeignTestController#openFeignProviderTest" + RandomUtils.nextInt(0, 1000);
}
}
测试接口
使用curl或浏览器都行,调用服务消费者方的/openFeignTest
接口测试
curl http://localhost:9001/openFeignTest
==>OpenFeignTestController#openFeignProviderTest748
代码优化一下:
在@FeignClient注解中,value属性填写的是服务提供者的服务名称,这么把值直接写死是不合适的,假如服务提供者名称变了,那么这里写的就需要修改,而且如果用的地方比较多的情况下,需要到处修改。如何解决呢?
- 把服务名用一个单独的类,定义静态常量
- 使用配置文件方式,使用表达式获取即可
定义静态常量类就不再演示,使用配置文件方式:
provider:
name: provider
在FeignClient客户端使用表达式获取
@FeignClient(value = "${provider.name}", fallback = OpenFeignTestServiceFallback.class)
结合sentinel规则使用
不管是RestTemplate+Ribbon还是OpenFeign的远程调用,都是支持Sentinel的,到sentinel面板中配置资源流控规则
注意:sentinel是懒加载方式,需要先去调用一次接口,才能在控制台添加规则
在sentinel面板,新增openFeignProviderTest
流控规则,阈值类型QPS,单机阈值1
欧克,我们对服务端的,/openFeignProviderTest
做了限流,那么试试多次连续访问客户端接口试试。
可以看到,有请求会进入到fallback降级中。
那么对于异常该怎么处理呢?
修改服务端接口代码,设置一个异常,进行测试接口
欧克,发现,服务端如果是出现了异常,仍然会进入客户端的fallback处理中,是不是非常奈斯。
那么同样的,假如服务器进入宕机状态,会如何?关掉服务端试试。
行的,一样可以进入到fallback处理中。
实现负载均衡
OpenFeign也具有负载均衡的功能,多个服务端时,采用对应的算法寻找一个服务端进行请求。
下面,服务端将输出当前项目的端口号,并且再新建一个服务端的项目
@RestController
public class OpenFeignProviderTest {
@Value("${server.port}")
private Integer port;
@RequestMapping("/openFeignProviderTest")
public String openFeignProviderTest() {
return "OpenFeignTestController#openFeignProviderTest" + port;
}
}
再新建一个项目,端口为8003即可。
server:
port: 8002
...多余部分省略
server:
port: 8003
...多余部分省略
项目启动,nacos中可以发现,服务端两个实例
多次调用客户端接口,看结果
依次轮询方式请求服务。
OpenFeign的超时配置
2020版本以前
的OpenFeign的默认等待接口返回数据的时间是1s
,超过1秒就报错,如果有fallback,那么执行fallback的处理。
2020版本后,源码如下:
public Options() {
//10L: connectTimeout
//60L: readTimeout
this(10L, TimeUnit.SECONDS, 60L, TimeUnit.SECONDS, true);
}
connectTimeout是10s,readTimeout是60s
普通接口是没有问题的,但是如果是一些耗时业务,执行过程中一定是大于1s的,不太合理。
那么OpenFeign提供了超时的配置。消费者bootstrap.yml
文件中增加超时配置:
feign.client.config.default.connectTimeout
:建立连接的超时时间feign.client.config.default.readTimeout
:建立连接后从服务器读取资源所用时间的超时时间配置
server:
port: 9001
spring:
application:
name: consumer # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
sentinel:
transport:
port: 8719 # 启动http server,并且该服务与Sentinel仪表板进行交互,使sentinel可以控制应用,若端口占用则8719+1依次扫描
dashboard: 127.0.0.1:8080 # 仪表版访问地址
feign:
sentinel:
enabled: true
client:
config:
default:
connectTimeout: 2000 # 建立连接超时时间
readTimeout: 2000 # 读取资源超时时间
@RequestMapping("/openFeignProviderTest")
public String openFeignProviderTest() throws InterruptedException {
Thread.sleep(5000);
return "OpenFeignTestController#openFeignProviderTest" + port;
}
这里故意睡眠5s,超时时间是2s那么,一定是超时了,会进入fallback.
时间调长点试试
..
feign:
sentinel:
enabled: true
client:
config:
default:
connectTimeout: 7000 # 建立连接超时时间
readTimeout: 7000 # 读取资源超时时间
没问题
OpenFeign的日志
为方便查找异常,一般在本地开发环境中,把OpenFeign远程调用接口的日志详情打印出来。
在服务消费者bootstrap.yml
文件新增日志级别的配置,新增配置项logging.level.
声明接口的包名,完整配置如下:
server:
port: 9001
spring:
application:
name: consumer # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
sentinel:
transport:
port: 8719 # 启动http server,并且该服务与Sentinel仪表板进行交互,使sentinel可以控制应用,若端口占用则8719+1依次扫描
dashboard: 127.0.0.1:8080 # 仪表版访问地址
feign:
sentinel:
enabled: true
client:
config:
default:
connectTimeout: 5000 # 建立连接超时时间
readTimeout: 5000 # 读取资源超时时间
logging:
level:
com.alibaba.provider.feigns: debug # 打印自己项目中com.alibaba.provider.feigns包的日志,级别是debug级别
然后在java中创建一个配置类
@Configuration
public class OpenFeignLoggerConfiguration {
@Bean
public Logger.Level openFeignLoggerLevel() {
return Logger.Level.FULL; // FULL日志级别
}
}
OpenFeign日志有以下几个级别
- NONE:无记录,默认的
- BASIC:只记录请求方法和url及响应状态代码和执行时间
- HEADERS:只记录基本信息及请求和响应头
- FULL:记录请求和响应的头文件,正文和元数据,信息最全
配置后日之后,如下:
信息非常完整,方便排错。
请求和响应压缩
OpenFeign支持通过简单配置实现请求和响应进行gzip
压缩,提高数据传输的效率。
开启压缩可以有效节约网络资源,但是在压缩和解压过程中会增加CPU的压力,最好把最小请求长度参数调大一些。
向bootstrap.yml
文件中,增加参数配置(服务消费者端):
server:
port: 9001
spring:
application:
name: consumer # 应用名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
sentinel:
transport:
port: 8719 # 启动http server,并且该服务与Sentinel仪表板进行交互,使sentinel可以控制应用,若端口占用则8719+1依次扫描
dashboard: 127.0.0.1:8080 # 仪表版访问地址
feign:
sentinel:
enabled: true
client:
config:
default:
connectTimeout: 5000 # 建立连接超时时间
readTimeout: 5000 # 读取资源超时时间
compression:
request:
enabled: true # 请求压缩启用
mime-types: text/xml, application/xml, application/json # 要压缩的类型
min-request-size: 2048 # 最小请求长度 单位:字节
response:
enabled: true # 响应压缩启用
logging:
level:
com.alibaba.provider.feigns: debug # 打印com.alibaba.provider.feigns包的日志,级别是debug级别
压缩后,可以看下日志情况:
OpenFeign的参数传递
简单参数传递
服务端接口
@PostMapping("/sampleParamsProviderTest")
public String sampleParamsProviderTest(@RequestParam("name") String name, @RequestParam("id") Integer id) {
return "OpenFeignProviderTest# sampleParamsProviderTest#" + port + "id=" + id + ",name=" + name;
}
消费者OpenFeign
@FeignClient(value = "${provider.name}", fallback = OpenFeignTestServiceFallback.class)
public interface OpenFeignTestService {
@PostMapping("/sampleParamsProviderTest")
public String sampleParamsProviderTest(@RequestParam("name") String name, @RequestParam("id") Integer id);
}
消费者fallback
@Component
public class OpenFeignTestServiceFallback implements OpenFeignTestService {
@Override
public String sampleParamsProviderTest(String name, Integer id) {
return "我是兜底方法" + name + id;
}
}
消费者提供一个接口做测试
@GetMapping("/sampleParamsProviderTest")
public String sampleParamsProviderTest() {
return openFeignTestService.sampleParamsProviderTest("gangge", 1);
}
@SpringQueryMap对象传递
那么先创建一个要传递的对象,服务提供者和消费者共用
public class Params {
private Integer id;
private String name;
.../getter、setter方法
}
服务提供者,创建一个给消费者调用的接口
@GetMapping("/springQueryMapProviderTest")
public String springQueryMapProviderTest(Params params) {
return "OpenFeignProviderTest# sampleParamsProviderTest#" + port + "id=" + params.getId() + ",name=" + params.getName();
}
消费者OpenFeign
@GetMapping("/springQueryMapProviderTest")
public String springQueryMapProviderTest(@SpringQueryMap Params params);
消费者fallback
@Override
public String springQueryMapProviderTest(Params params) {
return "我是兜底方法" + params.getId() + params.getName();
}
消费者接口
@GetMapping("/springQueryMapTest")
public String springQueryMapTest() {
Params params = new Params();
params.setId(1);
params.setName("gangge");
return openFeignTestService.springQueryMapProviderTest(params);
}
结果
复杂对象传递
对象套对象的情况,准备一个ComplexObject
和Result
对象(提供者和消费者都需要创建)
ComplexObject
public class ComplexObject {
private Params params;
public Params getParams() {
return params;
}
public void setParams(Params params) {
this.params = params;
}
}
Result
public class Result {
private Integer code;
private String describe;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getDescribe() {
return describe;
}
public void setDescribe(String describe) {
this.describe = describe;
}
}
在服务提供方编写接口,接受消费者调用,后返回Result对象
@PostMapping("/complexObjectProviderTest")
public Result complexObjectProviderTest(@RequestBody ComplexObject complexObject) {
Result result = new Result();
result.setCode(200);
result.setDescribe("#complexObjectProviderTest" + complexObject.getParams().getName() + complexObject.getParams().getId());
return result;
}
消费者声明客户端接口和fallback操作
@PostMapping("/complexObjectProviderTest")
public Result complexObjectProviderTest(@RequestBody ComplexObject complexObject);
@Override
public Result complexObjectProviderTest(ComplexObject complexObject) {
return null;
}
返回Result对象**
@PostMapping("/complexObjectProviderTest")
public Result complexObjectProviderTest(@RequestBody ComplexObject complexObject) {
Result result = new Result();
result.setCode(200);
result.setDescribe("#complexObjectProviderTest" + complexObject.getParams().getName() + complexObject.getParams().getId());
return result;
}
消费者声明客户端接口和fallback操作
@PostMapping("/complexObjectProviderTest")
public Result complexObjectProviderTest(@RequestBody ComplexObject complexObject);
@Override
public Result complexObjectProviderTest(ComplexObject complexObject) {
return null;
}
复杂对象时@RequestBody
注解完全可以,但是只能有一个@RequestBody的参数