前面使用RestTemplate结合Ribbon方式(客户端负载均衡 Ribbon-CSDN博客)实现负载均衡时,利用RestTemplate对HTTP请求进行封装处理,从而实现一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用不止一处,往往一个接口会被多个地方调用,所以通常会对每个微服务自行封装一些客户端类用于包装依赖服务的调用。Spring Cloud对Feign进行了封装,它能够使服务之间的调用变得更加简单。
Feign简介
Feign是Netflix开发的声明式、模板化的HTTP客户端。当Feign与Eureka和Ribbon组合使用时,Feign就具有了负载均衡的功能。在Feign的实现下,我们只需要定义一个接口并使用注解方式配置,即可完成服务接口的绑定,从而简化了Ribbon自动封装服务调用客户端的开发工作量。如此看来,我们可以把Feign理解为一个Spring Cloud远程服务的框架或者工具,它能够帮助开发者用更少的代码,更好的兼容方式对远程服务进行调用。
Feign快速入门
第一个Feign程序
Feign程序的架构如下图所示:
1.搭建Eureka Server
2.创建服务提供者
创建HelloController类:
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello(){
return "hello Feign";
}
}
3.创建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.0 https://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>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xiaofen</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-server</name>
<description>Demo project for Spring Boot with Eureka Server</description>
<properties>
<java.version>1.8</java.version>
<!-- 使用具体的Spring Cloud版本,避免使用别名或描述性文本 -->
<spring-cloud.version>2021.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 使用与 Spring Cloud 版本兼容的 Eureka Server 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<!-- 注意:这里不直接指定版本,因为会在 dependencyManagement 中管理 -->
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>-->
<!--Spring Cloud 2020.0 及更高版本中,Ribbon 已经被 Spring Cloud LoadBalancer 替代-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>${spring-cloud.version}</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>
在application.yml进行相关配置
server:
port: 8764
spring:
application:
name: eureka-feign-client
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka
添加@EnableFeignClients注解
在启动类EurekaFeignClientApplication中添加@EnableEurekaClient注解开启Eureka Client功能,再添加@EnableFeignClients注解开启Feign Client功能。
在FeignService类中添加@FeignClient注解:
@Service
@FeignClient(name = "eureka-provider") //eureka-provider是yml中的name 其作用:相当于对方的IP地址+端口号
public interface FeignService {
@GetMapping("/hello") //这个GetMapping写的路径是对方eureka-provider中的controller的GetMapping的路径
public String sayHello();
}
创建FeignController类,调用sayHello()方法:
在eureka-feign-client中创建controller包,并在该包下创建FeignController类,该类定义的hello()方法用于调用FeignService的sayHello()方法。
@RestController
public class FeignController {
@Autowired
private FeignService feignService;
@GetMapping("/hello")
public String sayHello(){
return feignService.sayHello();
}
}
跳转原理过程:
4.测试运行
Feign的参数绑定
第一个Feign程序实现的是一个不带参数的REST服务绑定。实际业务中,我们经常需要在调用服务时传递各种参数。下面我们在第一个Feign程序的基础上改进,使用eureka-feign-client远程调用eureka-provider的接口。不同的是,此时调用eureka-provider接口时,需要传递一个不同类型的参数。
首先在eureka-feign-client 和 eureka-provider 写一个实体类
public class User {
private String name;
private Integer age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
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;
}
}
其次,在controller层 写三个重写方法
@GetMapping("/hello1")
public String hello(@RequestParam String name) {
return "hello," + name;
}
@GetMapping("/hello2")
public User hello(@RequestParam String name, @RequestParam Integer age) {
return new User(name, age);
}
@PostMapping("/hello3")
public String hello(@RequestBody User user) {
return "hello," + user.getName() + "," + user.getAge();
}
然后,在eureka-feign-client 的Service层写入这三个方法的接口
@GetMapping("/hello1")
public String hello(@RequestParam String name);
@GetMapping("/hello2")
public User hello(@RequestParam String name, @RequestParam Integer age);
@PostMapping("/hello3")
public String hello(@RequestBody User user);
最后在 eureka-feign-client 写访问的暴露路径
@RestController
public class FeignController {
@Autowired
private FeignService feignService;
@GetMapping("/helloUser")
public String helloUser(){
StringBuilder sb = new StringBuilder();
sb.append(feignService.hello("xiaofeng")).append("<BR/>")
.append(feignService.hello(new User("xiaofeng",11))).append("<BR/>")
.append(feignService.hello("xiaofeng",11)).append("<BR/>");
return sb.toString();
}
}
访问的顺序:
浏览器输入http://localhost:8764/helloUser,首先会到这个方法来
然后会到feignService层执行
最后到eureka-provider中的controller方法中来:
测试运行:
Feign继承特性
在第一个Feign程序与参数绑定的开发中,可能同学们已经观察到很多代码是重复的。在Spring Cloud Feign开发中,能够使用继承特性来帮助我们解决这些代码的重复问题,进一步减少编码量。接下来,我们在参数绑定案例程序的基础上继续改造,对Feign的继承特性进行介绍
首先,我们需要一个公共工程存放其他服务需要的实体类和接口,这样可以避免在其他服务中编写同样的实体类和接口。后续通过引入公共工程的依赖,从而调用公共工程中的资源。
首先创建一个common-client
pom.xml
<?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 https://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>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xiaofeng</groupId>
<artifactId>common-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>common-client</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.3</spring-cloud.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-openfeign</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>${spring-cloud.version}</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>
实体类:
服务层:
@Service
@FeignClient(name = "eureka-provider") //eureka-provider是yml中的name 其作用:相当于对方的IP地址+端口号
public interface FeignClientService {
@GetMapping("/hello") //这个GetMapping写的路径是对方eureka-provider中的controller的GetMapping的路径
public String sayHello();
@GetMapping("/hello1")
public String hello(@RequestParam String name);
@GetMapping("/hello2")
public User hello(@RequestParam String name, @RequestParam Integer age);
@PostMapping("/hello3")
public String hello(@RequestBody User user);
}
写完就可以打包成jar包啦!
然后这两个模块就不需要重复写实体类啦。
项目目录:
然后只需要在这两个模块的Pom.xml文件中导入写好的公共模块即可使用。
<dependency>
<groupId>com.xiaofeng</groupId>
<artifactId>common-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
FeignController:
@RestController
public class FeignController {
@Autowired
private RefactorService feignClientService;
@GetMapping("/feign-comsumer")
public String feignComsumer(){
StringBuilder sb = new StringBuilder();
sb.append(feignClientService.hello("xiaofeng")).append("<BR/>")
.append(feignClientService.hello(new User("xiaofeng",11))).append("<BR/>")
.append(feignClientService.hello("xiaofeng",11)).append("<BR/>");
return sb.toString();
}
}
RefactorService:
@FeignClient(name = "eureka-provider")
public interface RefactorService extends FeignClientService {
}
RefactorController:
@RestController
public class RefactorController implements FeignClientService {
@Override
public String sayHello() {
return null;
}
@Override
public String hello(String name) {
return "hello,"+name;
}
@Override
public User hello(String name, Integer age) {
return new User(name,age);
}
@Override
public String hello(User user) {
return "hello,"+user.getName()+","+user.getAge();
}
}
最后启动各个模块(不需要启动公共模块),测试运行:
Feign配置
1.Ribbon的相关配置
Feign默认整合了Ribbon能够实现负载均衡,我们可以通过配置Ribbon,自定义各个服务的调用方式。接下来,我们从全局配置和指定服务配置两个方面介绍如何在Spring Cloud Feign中进行Ribbon的相关配置。
全局配置
Ribbon的全局配置其实非常简单,我们可以直接在application.yml配置文件中,使用ribbon.<key>=<value>的方式设定ribbon的各项默认参数,示例代码如下:
ribbon:
ConnectTimeout: 5000 #设置连接超时时间,默认为1s
ReadTimeout: 5000 #设置读取超时时间
OkToRetryOnAllOperations:true # 对所有操作请求都进行重试
MaxAutoRetries:1 # 对当前实例的重试次数
MaxAutoRetriesNextServer:2 # 切换实例的重试次数
接下来,为了测试设置的ConnectTimeout参数是否生效,我们在服务提供者eureka-provider的hello()方法中添加下列代码:
try{
Thread.sleep(6000);
}catch(Exception e){
}
由于ConnectTimeout参数设置的超时时间是5秒,上述代码设置的程序等待时间是6秒,这样必然会导致出现超时。此时使用浏览器访问http://localhost:8764/hello,发现消费者eureka-feign-client的控制台会报java.net.SocketTimeoutException: Read timed out。
日志配置
在application.yml文件中,可以使用logging.level.<FeignClient>参数配置格式开启指定Feign消费者的DEBUG日志,其中<FeignClient>指的是Feign消费者中需要开启DEBUG日志的接口的完整路径,比如针对本章中我们的Feign客户端接口FeignService(eureka-feign-client项目中的类)可以按如下配置开启:
logging:
level:
com.itheima.eurekafeignclient.service.FeignService: DEBUG
需要注意的是,如果只添加上述配置,还是无法输出DEBUG日志的,因为Feign客户端的Logger.Level对象默认定义为NONE级别,该级别不会记录任何Feign调用过程中的信息,所以我们需要调整它的级别。
Feign中的日志级别主要分为四类,具体介绍如下:
1. NONE:不记录任何信息。
2. BASIC:仅记录请求方法,URL以及响应状态码和执行时间。
3. HEADERS:除了记录BASIC级别的信息之外,还会记录请求和响应的头 信息。
4. FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等。
设置日志级别为FULL的实例:
![](https://i-blog.csdnimg.cn/direct/a42be0cd33aa4d7b9d5eb3cf6e11a23a.png)
![](https://i-blog.csdnimg.cn/direct/5ca0378a57da44379dcff64ce09a3196.png)
![](https://i-blog.csdnimg.cn/direct/d2755435a9c24738bf1599545fe33880.png)
![](https://i-blog.csdnimg.cn/direct/e1b7c2eee1e6421d8fd95feca4f7f6ed.png)
其他配置
我们从请求压缩方面讲解,Spring Cloud Feign支持对请求与响应进行GZIP压缩,以减少通信过程中的性能损耗。我们只需要通过以下参数设置,就可以开启请求与响应的压缩功能,代码如下:
feign:
compression:
request:
enabled: true
Response:
Enabled: true
我们还能对请求压缩做一些细致的设置,例如,下面的配置内容指定了压缩请求支持的MIME TYPE类型,并设置了请求压缩的大小下限,只有超过这个大小的请求才会对其进行压缩。代码如下:
feign:
compression:
request:
mime-types:
text/xml,application/xml,application/json
min-request-size: 2048
上述配置的参数中,feign.compression.request.mime-types和feign.compressionfeign.compression.request.min-request-size设置的均为默认值。
Feign服务调用的工作原理及负载均衡的实现原理
Feign服务调用的工作原理可以分为以下几个步骤:
(1)首先通过@EnableFeignClients注解开启FeignClient功能。程序启动时,会 通过该注解开启对@FeignClient注解的包扫描。
(2)根据Feign规则实现接口,并在接口上面添加@FeignClient注解。
(3)程序启动后,会进行包扫描,扫描所有的@FeignClient注解类,并将这些信息注入IoC容器。
(4)当接口方法被调用时,通过JDK的代理生成具体的RequestTemplate模板对象。根据RequestTemplate再生成HTTP请求的Request对象,Request对象交给Client处理。
开启Spring Cloud Feign功能是通过@EnableFeignClients注解实现的。程序启动时,首先会检测是否有@EnableFeignClients注解,如果有,则会开启扫描功能,并扫描被@FeignClient注解修饰的接口。接下来,我们从@EnableFeignClients注解入手,分析Feign远程调用服务的工作原理。
查看@EnableFeignClients注解的源码,具体代码如下所示。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
继续跟踪FeignClientsRegistrar类源码,发现其内部定义了一个registerBeanDefinitions方法,具体代码如下所示。
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
this.registerDefaultConfiguration(metadata, registry);
this.registerFeignClients(metadata, registry);
}
registerDefaultConfiguration()用于加载相关配置,registerFeignClients()用于扫描所有@FeignClient注解的接口,并将这些信息注入IoC容器。
当被@FeignClient注解修饰的方法被调用时,该方法会被SynchronousMethodHandler拦截处理,并生成一个RequestTemplate对象。如下:
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template= buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
return executeAndDecode(template);
} catch(RetryableException e) {
...//省略代码
}
continue;
}
}
}
Request对象交给Client处理。源码如下:
Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request= targetRequest(template) ;
...//省略代码
response = client.execute(request,options);
...//省略代码
registerFeignClients()方法内部定义了一个基于classpath的组件扫描器,然后组件扫描器会根据指定的扫描位置和@EnableFeignClients注解属性找到开发人员定义的所有Feign客户端,也就是所有添加了@FeignClient注解的所有接口,最后将注册Feign客户端的动作交给registerFeignClient()方法完成。
Feign是如何注册LoadBalancerFeignClient作为其客户端调用实现的呢? 下面我们查看LoadBalancerFeignClient的自动配置类FeignRibbonClientAutoConfiguration的源码,具体代码如下所示。
继续查看 LoadBalancerFeignClient源码,部分代码如下所示。
FeignLoadBalancer.RibbonRequest ribbonRequest =
new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName).executeWithLoadBalancer
(ribbonRequest,requestConfig).toResponse();
从上述代码可以看出,Spring Cloud Feign的负载均衡客户端功能是通过Spring Cloud Ribbon实现的,其中executeWithLoadBalancer()方法用于以负载均衡的方式发送请求。
继续查看executeWithLoadBalancer()方法源码,在executeWithLoadBalancer()方法中有一个submit()方法,该方法用于实现负载均衡。查看submit()方法源码,部分源码如下所示。
Observable<T> o =
(server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
@Override
// Called for each server being selected
public Observable<T> call(Server server) {
context.setServer(server);
}}
上述代码中,selectServer()方法最终会调用ILoadBalancer选择服务从而实现负载均衡。