声明式服务调用SpringCloud Feign
声明式服务调用SpringCloud Feign
前面使用了Ribbon做客户端负载均衡,使用Hystrix做容错保护,这两者被作为基础工具类框架被广泛地应用在各个微服务的实现中。SpringCloudFeign是将两者做了更高层次的封装以简化开发。它基于Netfix Feign实现,整合了SpringCloudRibbon和SpringCloudHystrix,除了提供这两者的强大功能外,还提供了一种声明是的Web服务客户端定义的方式。SpringCloudFeign在NetFixFeign的基础上扩展了对SpringMVC注解的支持,在其实现下,我们只需创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。简化了SpringCloudRibbon自行封装服务调用客户端的开发量。
快速入门
创建项目 feign-consumer,其余代码可参考上一章的代码。
pom 依赖
<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>
</dependencies>
创建应用启动类
创建应用启动类,并通过 @EnableFeignClients 注解开启 Spring Cloud Feign 的支持功能
package com;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
定义 HelloService 接口
定义 HelloService 接口,通过 @FeignClient 注解指定服务名来绑定服务,然后再使用 SpringMVC 的注解来绑定具体该服务提供的 REST 接口
这里服务名不区分大小写,所以使用 SERIVCE-USER 和 service-user 都是可以的
package com.controller;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient("hello-service")
public interface HelloService {
@RequestMapping("/hello")
String hello();
}
方法调用
package com.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping(value = "ribbon-consumer", method = RequestMethod.GET)
public String helloConsumer() {
return helloService.hello();
}
}
配置文件
spring.application.name=feign-consumer
server.port=9000
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
#修改缓存清单的更新时间,该值默认为30s
eureka.client.registry-fetch-interval-seconds=30
测试
依次启动服务注册中心、服务提供方、服务消费方。然后访问http://localhost:9000/ribbon-consumer,有时候可以正常返回数据,不断刷新几次地址,可以发现feign通过轮询实现了客户端负载均衡。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rtWmRAcX-1600188406570)(media/1546395519194.png)]
参数绑定
hello-service改造
增加方法
hello-service中多增加一些接口 ,其中包含带有request参数的请求、带有header信息的请求、带有requestbody的请求以及请求响应体是一个对象的请求。
package com.web;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import java.util.Random;
@RestController
public class HelloController {
private final Logger logger = Logger.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello() throws Exception {
ServiceInstance instance = client.getLocalServiceInstance();
// 测试超时触发断路器
int sleepTime = new Random().nextInt(3000);
logger.info("sleepTime:" + sleepTime);
Thread.sleep(sleepTime);
logger.info("/hello, host:" + instance.getHost() + ", service_id:" + instance.getServiceId());
return "Hello World";
}
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
public String hello(@RequestParam String name) {
return "Hello1 " + name;
}
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
public User hello(@RequestHeader String name, @RequestHeader Integer age) {
return new User(name, age);
}
@RequestMapping(value = "/hello3", method = RequestMethod.POST)
public String hello(@RequestBody User user) {
return "Hello3 " + user.getName() + ", " + user.getAge();
}
}
构造 User对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lpMNRgfq-1600188406571)(media/1546398184529.png)]
增加User 对象如下, 这里必须要有 User 的默认构造函数,不然 Spring Cloud Feign 根据 JSON 字符串转换 User 对象会抛出异常。
package com.web;
public class User {
private String name;
private Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "name=" + name + ", age=" +age;
}
}
feign-consumer改造
增加user对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lvKw5yNp-1600188406572)(media/1546396616427.png)]
接口绑定声明
直接将上面添加的接口复制到消费方的Service接口中,删除方法体。需要注意的是:在SpringMVC中@RequestParam和@RequestHeader注解,如果我们不指定value,则默认采用参数的名字作为其value,但是在Feign中,这个value必须明确指定,否则会报错。
package com.controller;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient("hello-service")
public interface HelloService {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello();
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
public String hello(@RequestParam("name") String name);
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
public User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age);
@RequestMapping(value = "/hello3", method = RequestMethod.POST)
public String hello(@RequestBody User user);
}
测试接口
@RequestMapping(value = "feign-consumer2", method = RequestMethod.GET)
public String helloConsumer1() {
StringBuilder sb = new StringBuilder();
sb.append(helloService.hello()).append("\n");
sb.append(helloService.hello("DIDI")).append("\n");
sb.append(helloService.hello("DIDI", 30)).append("\n");
sb.append(helloService.hello(new User("DIDI", 30))).append("\n");
return sb.toString();
}
访问http://localhost:9000/feign-consumer2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fQnowY5q-1600188406574)(media/1546397504495.png)]
继承特性
根据上面参数绑定的做法,我们需要进行很多接口的copy操作,这样比较麻烦,可以通过继承的方式进行简化。
创建API模块
导入依赖
<?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</groupId>
<artifactId>service-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>service-api</name>
<description>Demo project for Spring Boot</description>
<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>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
复制user类
复制上节中的 User 对象到 service-api 工程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L9r9ws1U-1600188406575)(media/1546398291662.png)]
创建 HelloService 接口
package com.serviceapi.service;
import com.serviceapi.domain.User;
import org.springframework.web.bind.annotation.*;
@RequestMapping("/refactor")
public interface HelloService {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello();
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
public String hello(@RequestParam("name") String name);
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
public User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age);
@RequestMapping(value = "/hello3", method = RequestMethod.POST)
public String hello(@RequestBody User user);
}
重构hello-service
导入依赖
<dependency>
<groupId>com</groupId>
<artifactId>service-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
删除user类
重写服务提供类
package com.web;
import com.serviceapi.domain.User;
import com.serviceapi.service.HelloService;
import org.springframework.web.bind.annotation.*;
import java.util.Random;
@RestController
public class HelloController implements HelloService {
@Override
public String hello() throws Exception {
// 测试超时触发断路器
int sleepTime = new Random().nextInt(3000);
Thread.sleep(sleepTime);
return "Hello World";
}
@Override
public String hello(@RequestParam("name") String name) {
return "Hello " + name;
}
@Override
public User hello(@RequestHeader("name")String name, @RequestHeader("age")Integer age) {
return new User(name, age);
}
@Override
public String hello(@RequestBody User user) {
return "Hello "+ user.getName() + ", " + user.getAge();
}
}
重构feign-consumer
创建 RefactorHelloService 接口
package com.controller;
import com.serviceapi.service.HelloService;
import org.springframework.cloud.netflix.feign.FeignClient;
@FeignClient(value = "hello-service")
public interface RefactorHelloService extends HelloService {
}
删除User类和HelloService 接口
测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PIAgoT3M-1600188406576)(media/1546480393877.png)]
优点和缺点
- 优点
可以很方便的实现接口定义和依赖的共享,不再使用复制粘贴接口进行绑定。
- 缺点
接口的变动就会对项目构建造成影响,可能服务提供方修改了一个接口定义,那么会导致客户端工程的构建失败。
好像无法使用服务降级功能。
Ribbon 配置
全局配置
全局配置的方法非常简单,我们可以直接使用ribbon.=的方式来设置ribbon的各项默认参数。如下:
#以下配置全局有效
ribbon.eureka.enabled=true
#建立连接超时时间,原1000
ribbon.ConnectTimeout=60000
#请求处理的超时时间,5分钟
ribbon.ReadTimeout=60000
#所有操作都重试
ribbon.OkToRetryOnAllOperations=true
#重试发生,更换节点数最大值
ribbon.MaxAutoRetriesNextServer=10
#单个节点重试最大值
ribbon.MaxAutoRetries=1
指定服务配置
大多数情况下,我们对于服务调用的超时时间可能会根据实际服务的特性做一些调整,所以仅仅进行个性化配置的方式与使用Spring Cloud Ribbon时的配置方式是意义的,都采用.ribbon.key=value的格式进行设置。但是,这里就有一个疑问了,所指代的Ribbon客户端在那里呢?
回想一下,在定义Feign客户端的时候,我们使用了@FeignClient注解。在初始化过程中,Spring Cloud Feign会根据该注解的name属性或value属性指定的服务名,自动创建一个同名的Ribbon客户端。如下:
#以下配置对服务hello-service-provider有效
hello-service.ribbon.eureka.enabled=true
#建立连接超时时间
hello-service.ribbon.ConnectTimeout=500
#请求处理的超时时间
hello-service.ribbon.ReadTimeout=2000
#所有操作都重试
hello-service.ribbon.OkToRetryOnAllOperations=true
#重试发生,更换节点数最大值
hello-service.ribbon.MaxAutoRetriesNextServer=2
#单个节点重试最大值
hello-service.ribbon.MaxAutoRetries=1
重试机制
feign-consumer 添加之前上述指定服务配置
访问http://localhost:9000/feign-consumer
在 user-service 可以看到控制台的两个服务提供者有时会打印出如下信息:
Ribbon超时与Hystrix超时问题,为了确保Ribbon重试的时候不被熔断,我们就需要让Hystrix的超时时间大于Ribbon的超时时间,否则Hystrix命令超时后,该命令直接熔断,重试机制就没有任何意义了。
从上面的配置来说,ribbon超时配置为1800,请求超时后,该实例会重试1次,更新实例会重试1次。
所以hystrix的超时时间要大于 (1 + MaxAutoRetries + MaxAutoRetriesNextServer) * ReadTimeout 比较好,具体看需求进行配置。
Hystrix 配置
全局配置
#全局设置超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
#关闭 Hystrix 功能
#feign.hystrix.enabled=false
#关闭熔断功能
#hystrix.command.default.execution.timeout.enabled=false
禁用 Hystrix
全局关闭 Hystrix
#关闭 Hystrix 功能
feign.hystrix.enabled=false
针对某个客户端关闭 Hystrix ,通过使用 @Scope(“prototype”) 注解为指定的客户端配置 Feign.Builder 实例
构建一个关闭 Hystrix 的配置类
@Configuration
public class DisableHystrixConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder(){
return Feign.builder();
}
}
在 Hello-Service 的 @FeignClient 注解中,通过 configuration 参数引入上面实例的配置
@FeignClient(value = "SERIVCE-USER",configuration = DisableHystrixConfiguration.class)
@Service
public interface HelloService {
···
}
指定命令配置
针对尝试机制中对 /hello 接口的熔断时间的配置可通过如下配置
hystrix.command.hello.execution.isolation.thread.timeoutInMilliseconds=5000
服务降级配置
还原之前的类
对 feign-consumer 工程进行改造,添加回之前的HelloService、User类,不使用继承特性实现服务的降级。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgvlH2ZX-1600188406576)(media/1546481152661.png)]
编写服务降级类
package com.controller;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
@Component
public class HelloServiceFallBack implements HelloService {
@Override
public String hello() {
return "error";
}
@Override
public String hello(@RequestParam("name") String name) {
return "error";
}
@Override
public User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age) {
return new User("未知", 0);
}
@Override
public String hello(@RequestBody User user) {
return "error";
}
}
绑定服务降级类
在服务绑定接口 HelloService 中,通过 @FeignClient 注解的 fallback 属性来指定对应的服务降级实现类
package com.controller;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name="HELLO-SERVICE", fallback = HelloServiceFallBack.class)
public interface HelloService {
......
}
测试
访问http://localhost:9000/feign-consumer2,有时候会得到如下结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uWA7AhqM-1600188406577)(media/1546481338539.png)]
注意
在fallback的实现函数中不再支持com.netfix.hystrix.HystrixComand和rx.Observable类型的异步执行方式和响应式执行方式。
其他配置
请求压缩
Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高通信效率,配置方式如下:
# 配置请求GZIP压缩
feign.compression.request.enabled=true
# 配置响应GZIP压缩
feign.compression.response.enabled=true
# 配置压缩支持的MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置压缩数据大小的下限
feign.compression.request.min-request-size=2048
日志配置
每一个被创建的Feign客户端都会有一个logger。该logger默认的名称为Feign客户端对应的接口的全限定名。Feign日志记录只能响应DEBUG日志级别。
配置属性文件
具体配置在application.properties中配置:
logging.level.=DEBUG开启指定Feign客户端的DEBUG模式日志;为Feign客户端定义接口的完整路径,如下:
# feign日志配置
logging.level.com.controller.HelloService=DEBUG
配置feign-consumer 启动类或实现配置类
package com;
import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class Application {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
也可以通过实现配置类,然后在具体的Feign 客户端来指定配置类以实现是否要调整不同的日志界别
@Configuration
public class FullLogConfiguration {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
测试
调用 http://localhost:9010/feign-consumer
请求详细日志
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oblsnma7-1600188406577)(media/1546482631072.png)]
还没有评论,来说两句吧...