本次我们来看看SpringCloud组件OpenFeign
目录如下:
Feign
是Netflix
开发的声明式、模板化
的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API
。
使用Feign非常简单
—创建一个接口,并在接口上添加一些注解,代码就完成了。Feign具备可插拔的注解、支持多种注解,例如Feign自带的注解或者JAX-RS注解等
。
Spring Cloud Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon
和Spring Cloud Hystrix
,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式
。
动手实践
(1)添加Feign依赖
<?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.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo.cloud</groupId>
<artifactId>microservice-consumer-movie-feign</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 引入spring boot的依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.3.RELEASE</version>
</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!--添加feign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
<!-- 引入spring cloud的依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 添加spring-boot的maven插件 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(2)创建一个Feign接口,并添加@FeignClient注解
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.demo.cloud.study.user.entity.User;
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User findById(@PathVariable("id") Long id);
}
(3)修改Controller代码,让其调用Feign接口
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.demo.cloud.study.user.entity.User;
import com.demo.cloud.study.user.feign.UserFeignClient;
@RestController
public class MovieController {
@Autowired
private UserFeignClient userFeignClient;
@GetMapping("/user/{id}")
public User findById(@PathVariable Long id) {
return this.userFeignClient.findById(id);
}
}
(4)修改启动类,为其添加@EnableFeignClients注解
添加注解:@EnableFeignClients @EnableDiscoveryClient
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;
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class ConsumerMovieApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerMovieApplication.class, args);
}
}
OpenFeign
是 SpringCloud在Feign 的基础上支持了SpringMVC的注解
,如@RequestMapping
等。OpenFeign 的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口
,并通过动态代理的方式产生实现类
,实现类中做负载均衡并调用其他服务。
Feign | openFeign |
---|---|
Feign是 Netflix开发的 声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用 HTTP API。使用 Feign非常简单—创建一个接口,并在接口上添加一些注解,代码就完成了。 Feign具备可插拔的注解、支持多种注解,例如Feign自带的注解或者JAX-RS注解等`。 |
OpenFeign 是 SpringCloud在Feign 的基础上 支持了SpringMVC的注解,如 @RequestMapping等。OpenFeign 的 @FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过 动态代理的方式产生实现类`,实现类中做负载均衡并调用其他服务。 |
接口+注解:微服务调用接口 + @FeignClient
1. 新建 cloud-consumer-feign-order80
2. 添加依赖
<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>LearnCloudartifactId>
<groupId>com.lun.springcloudgroupId>
<version>1.0.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-consumer-feign-order80artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.lun.springcloudgroupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
3、添加 applicatin.yml 配置
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
4. 主启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
5. 业务处理逻辑类
业务逻辑接口+@FeignClient配置调用provider服务
新建 PaymentFeignService 接口
并新增注解@FeignClient
import com.lun.springcloud.entities.CommonResult;
import com.lun.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService
{
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
Controller 控制层
import com.lun.springcloud.entities.CommonResult;
import com.lun.springcloud.entities.Payment;
import com.lun.springcloud.service.PaymentFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderFeignController
{
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id)
{
return paymentFeignService.getPaymentById(id);
}
}
开发中接口传参的方式有很多,在openFeign中的传参是有一定规则的,下面详细介绍。
1、传递JSON数据
Spring Boot 中通过@RequestBody标识入参。
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/order")
public Order createOrder(@RequestBody Order order){
return order;
}
}
consumer中openFeign接口中传参代码如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 参数默认是@RequestBody标注的,这里的@RequestBody可以不填
* 方法名称任意
*/
@PostMapping("/openfeign/provider/order")
Order createOrder(@RequestBody Order order);
}
注意:
openFeign默认的传参方式就是JSON传参(@RequestBody)
,因此定义接口的时候可以不用@RequestBody注解标注
,不过为了规范,一般都填上。
2、POJO表单传参
openFeign提供了一个注解@SpringQueryMap
完美解决POJO表单传参。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/order1")
public Order createOrder1(Order order){
return order;
}
}
consumer消费者openFeign代码如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 参数默认是@RequestBody标注的,如果通过POJO表单传参的,使用@SpringQueryMap标注
*/
@PostMapping("/openfeign/provider/order1")
Order createOrder1(@SpringQueryMap Order order);
}
3、URL中携带参数
此种方式针对restful方式中的GET请求
,也是比较常用请求方式。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@GetMapping("/test/{id}")
public String test(@PathVariable("id")Integer id){
return "accept one msg id="+id;
}
consumer消费者openFeign接口如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
@GetMapping("/openfeign/provider/test/{id}")
String get(@PathVariable("id")Integer id);
}
4、普通表单参数
此种方式传参不建议使用,但是也有很多开发在用。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/test2")
public String test2(String id,String name){
return MessageFormat.format("accept on msg id={0},name={1}",id,name);
}
}
consumer消费者openFeign接口传参如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 必须要@RequestParam注解标注,且value属性必须填上参数名
* 方法参数名可以任意,但是@RequestParam注解中的value属性必须和provider中的参数名相同
*/
@PostMapping("/openfeign/provider/test2")
String test(@RequestParam("id") String arg1,@RequestParam("name") String arg2);
}
超时设置,故意设置超时演示出错情况
1.服务提供方8001/8002故意写暂停程序
@RestController
@Slf4j
public class PaymentController {
...
@Value("${server.port}")
private String serverPort;
...
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout()
{
// 业务逻辑处理正确,但是需要耗费3秒钟
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}
...
}
2.服务消费方80 添加超时方法 PaymentFeignService
CLOUD-PAYMENT-SERVICE
注册Eruka的服务名称
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService{
...
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout();
}
3.服务消费方80 添加超时方法 OrderFeignController
@RestController
@Slf4j
public class OrderFeignController
{
@Resource
private PaymentFeignService paymentFeignService;
...
@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout()
{
// OpenFeign客户端一般默认等待1秒钟
return paymentFeignService.paymentFeignTimeout();
}
}
4、测试情况
多次刷新http://localhost/consumer/payment/feign/timeout
将会跳出错误Spring Boot默认错误页面,主要异常:feign.RetryableException:Read timed out executing GET http://CLOUD-PAYMENT-SERVCE/payment/feign/timeout。
openFeign 其实是有默认的超时时间的,默认分别是连接超时时间10秒、读超时时间60秒,源码在feign.Request.Options#Options()这个方法中,如下图
那么问题来了:为什么我只设置了 睡眠3秒 就报超时呢?
其实openFeign集成了Ribbon,Ribbon的默认超时连接时间、读超时时间都是是1秒,源码在org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute()方法
中,如下图:
源码逻辑: 如果openFeign没有设置对应得超时时间,那么将会采用Ribbon的默认超时时间。 理解了超时设置的原理,由之产生两种方案也是很明了了,如下: 设置openFeign的超时时间 设置Ribbon的超时时间
1、设置Ribbon的超时时间(不推荐)
设置很简单,在配置文件中添加如下设置:
ribbon:
# 值的是建立链接所用的时间,适用于网络状况正常的情况下, 两端链接所用的时间
ReadTimeout: 5000
# 指的是建立链接后从服务器读取可用资源所用的时间
ConectTimeout: 5000
2、设置openFeign的超时时间(推荐)
openFeign设置超时时间非常简单,只需要在配置文件中配置,如下:
default设置的是全局超时时间,对所有的openFeign接口服务都生效
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
注意:单个配置的超时时间将会覆盖全局配置。
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
## 为serviceC这个服务单独配置超时时间
serviceC:
connectTimeout: 30000
readTimeout: 30000
OpenFeign 底层通信组件默认使用 JDK 自带的 URLConnection 对象
进行 HTTP 请求的,因为没有使用连接池,所以性能不是很好。我们可以将 OpenFeign 的通讯组件,手动替换成像 Apache HttpClient 或 OKHttp
这样的专用通信组件,这些的专用通信组件自带连接池可以更好地对 HTTP 连接对象进行重用与管理,同时也能大大的提升 HTTP 请求的效率。
接下来我以 Apache HttpClient 为例,演示一下专用通讯组件的使用。
1、引入Apache HttpClient依赖
在项目的依赖管理文件 pom.xml 中添加以下配置:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
为什么要添加上面的依赖呢?从源码中不难看出,请看org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration这个类
,代码如下:
上述红色框中的生成条件,其中的@ConditionalOnClass(ApacheHttpClient.class),必须要有ApacheHttpClient这个类才会生效
,并且feign.httpclient.enabled这个配置要设置为true
2、开启Apache HttpClient使用
启动 Apache HttpClient 组件,在项目配置文件 application.yml 中添加以下配置:
feign:
client:
httpclient: # 开启 HttpClient
enabled: true
3、如何验证已经替换成功?
在feign.SynchronousMethodHandler#executeAndDecode()
这个方法中可以清楚的看出调用哪个client,如下图:最终调用的是ApacheHttpClient
openFeign虽然提供了日志增强功能,但是默认是不显示任何日志的,不过开发者在调试阶段可以自己配置日志的级别。
openFeign的日志级别如下:
NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
配置起来也很简单,步骤如下:
1、配置日志bean
需要自定义一个配置类,在其中设置日志级别,如下:
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
2、YML文件里需要开启日志的Feign客户端
logging:
level:
# feign日志以什么级别监控哪个接口
com.lun.springcloud.service.PaymentFeignService: debug
虽然 OpenFeign 默认是不输出任何日志,但在开发阶段可能会被修改,因此在生产环境中,我们应仔细检查并设置合理的日志级别,以提高 OpenFeign 的运行效率。
上述步骤将日志设置成了FULL
,此时发出请求,日志效果如下图:
常见的熔断降级框架有Hystrix、Sentinel
,openFeign默认支持的就是Hystrix,这个在官方文档上就有体现。
阿里的Sentinel无论是功能特性、简单易上手等各方面都完全秒杀Hystrix,因此此章节就使用openFeign+Sentinel
进行整合实现服务降级。
1、添加Sentinel依赖
在 openFeign-consumer 消费者的pom文件添加sentinel依赖(由于使用了聚合模块,不指定版本号),如下
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
2、配置文件中开启sentinel熔断降级
要想openFeign使用sentinel的降级功能,还需要在配置文件中开启,添加如下配置
feign:
sentinel:
enabled: true
3、添加降级回调类
这个类一定要和openFeign接口实现同一个类,如下图:
OpenFeignFallbackService
这个是降级回调的类,一旦OpenFeignService
中对应得接口出现了异常则会调用这个类中对应得方法进行降级处理。
4、添加fallback属性
在@FeignClient
中添加fallback属性
,属性值是降级回调的类,如下:
@FeignClient(value = "openFeign-provider",fallback = OpenFeignFallbackService.class)
public interface OpenFeignService {}
5、验证
注意:实际开发中返回结果应该根据架构统一定制,这里只是为了演示方便。
在讲如何优化之前先来看一下GZIP 压缩算法
,概念如下
gzip是一种数据格式,采用用deflate算法压缩数据;gzip是一种流行的数据压缩算法,应用十分广泛,尤其是在Linux平台。
网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。网页加载速度加快的好处不言而喻,除了节省流量,改善用户的浏览体验外,另一个潜在的好处是GZIP与搜索引擎的抓取工具有着更好的关系。例如 Google就可以通过直接读取GZIP文件来比普通手工抓取更快地检索网页。
GZIP压缩传输的原理如下图
步骤拆解:
Accept-Encoding:gzip,deflate
Accept-Encoding
字段,并且支持该类型的压缩,就对响应报文压缩之后返回给客户端,并且携带Content-Encoding:gzip
消息头,表示响应报文是根据该格式压缩过的。Content-Encoding消息头
,如果有,按该格式解压报文。否则按正常报文处理。GZIP传输
的只有两块,分别是Application client --> Application Service、 Application Service -->Application client。
注意:
openFeign支持的GZIP仅仅是在openFeign接口的请求和响应
,即是openFeign消费者调用服务提供者的接口。feign:
## 开启压缩
compression:
request:
enabled: true
## 开启压缩的阈值,单位字节,默认2048,即是2k,这里为了演示效果设置成10字节
min-request-size: 10
mime-types: text/xml,application/xml,application/json
response:
enabled: true
上述发出请求后可以看到请求头中已经携带了GZIP压缩
PS:如果服务消费端的 CPU 资源比较紧张的话,建议不要开启数据的压缩功能,因为数据压缩和解压都需要消耗 CPU 的资源,这样反而会给 CPU 增加了额外的负担,也会导致系统性能降低。
OpenFeign 底层使用的是 Ribbon 做负载均衡
的,查看源码我们可以看到它默认的负载均衡策略是轮询策略,如下图所示:
轮询策略之外,我们还有其他 6 种内置的负载均衡策略可以选择,这些负载均衡策略如下:
权重策略:WeightedResponseTimeRule
,根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。它的实现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大。
最小连接数策略:BestAvailableRule
,也叫最小并发数策略,它是遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,那么会调用轮询策略进行选取。
区域敏感策略:ZoneAvoidanceRule
,根据服务所在区域(zone)的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似。
可用敏感性策略:AvailabilityFilteringRule
,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例。
随机策略:RandomRule
,从服务提供者的列表中随机选择一个服务实例。
重试策略:RetryRule
,按照轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回 null。
出于性能方面的考虑,我们可以选择用权重策略或区域敏感策略来替代轮询策略,因为这样的执行效率最高