目录
[一、OpenFeign 简介](#一、OpenFeign 简介)
[1.1 项目集成](#1.1 项目集成)
[二、OpenFeign 原理](#二、OpenFeign 原理)
[2.1 OpenFeign Client 定位](#2.1 OpenFeign Client 定位)
[2.2 OpenFeign动态代理 编辑](#2.2 OpenFeign动态代理 编辑)
[3.1 服务层负载均衡](#3.1 服务层负载均衡)
[3.2 客户端负载均衡](#3.2 客户端负载均衡)
[3.3 OpenFeign 与 Loadbalancer 实现负载均衡](#3.3 OpenFeign 与 Loadbalancer 实现负载均衡)
微服务是将一个单体应用拆分成一个个较小的服务,那服务拆分后出现的第一个问题就是服务间的相互调用,下面再来看下 Spring Cloud 的组件构成。
今天主要介绍远程调用采用 OpenFeign 的实现方式。
一、OpenFeign 简介
OpenFeign 是一个 Java 语言编写的声明式服务客户端工具,它的前身是 Netflix Feign,Feign 内置了 Ribbon 进行客户端负载均衡。后来随着 Feign 项目进入了维护模式,不在积极更新,Spring Cloud 团队采纳了 Feign 的思想和基本架构,将其发展为 Spring Cloud OpenFeign。
OpenFeign 旨在简化服务间的 HTTP 调用,让用户能够通过定义接口的方式来调用远程服务,而无需关注底层的 HTTP 请求细节和连接管理。
OpenFeign 的主要特点如下:
- 声明式编程:开发者只需要编写接口并在接口上添加注解,如 @FeignClient 和 HTTP 方法相关的注解,无需关心底层 HTTP 的请求细节
- 易于维护:五福消费者只需关注服务提供者的 API 接口定义,降低了服务间的耦合度,方便代码维护和管理
- 支持拦截器:可以自定义 Feign 的请求拦截器,用于实现认证、日志记录、重试等逻辑
- 编程友好:OpenFeign 的 API 设计简洁,符合直觉,能够显著提高开发效率
1.1 项目集成
添加依赖
java
<!-- openfeign组件,注意需要添加先 SpringCloud 相应依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启用 OpenFeign,在启动类上添加 @EnableFeignClients 注解来开启 OpenFeign。
java
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
// 启用 OpenFeign 注解,如何生效的,后边介绍
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
配置服务发现,如果使用了注册中心组件,如Nacos、Consul,Spring Cloud OpenFeign 会自动整合。
定义 Feign 客户端,创建一个接口,通过 @FeignClient 注解指定服务名,并在接口方法上使用 HTTP 方法注解定义请求映射。
java
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "userservice")
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
}
最后就是注入 Feign 客户端,将定义好的 Feign 客户端注入到需要使用的地方,就像注入普通 Spring Bean 一样。
二、OpenFeign 原理
OpenFeign 通过解析注解标签生成一个动态代理类,这个代理类会将接口调用转化为一个远程服务调用的 Request,并发送给目标服务。
首先看下 Spring 是如何找到你定义的 OpenFeign 客户端的。
2.1 OpenFeign Client 定位
上面的集成过程中我们在启动类上添加了 @EnableFeignClients 注解,下面看下注解的源码内容。
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
// 先忽略
}
注意到 EnableFeignClients 注解中还有个注解 @Import(FeignClientsRegistrar.class),再来看下 FeignClientsRegistrar 的具体实现
java
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
// 忽略部分代码
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
// 在此处扫描了 FeignClient 注解
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
// 构造 FeignClientFactoryBean
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
// 将属性添加到 Definition 中
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
// 忽略其他代码
}
FeignClientsRegistrar 类实现了 ImportBeanDefinitionRegistrar接口。ImportBeanDefinitionRegistrar 在 Spring 中主要用于扩展 Spring 的组件扫描功能。它可以支持我们自己写的代码封装成 BeanDefinition 对象,实现此接口的类会回调postProcessBeanDefinitionRegistry 方法,注册到 spring 容器中。把 bean 注入到 spring 容器不止有 @Service @Component等注解方式;还可以实现此接口。
registerFeignClients 方法中扫描了 FeignClient 注解,然后将其加载到 BeanDifinition 中,此时并没有实力化成普通的 bean,而是生成代理类。
2.2 OpenFeign动态代理
在项目初始化节点,OpenFeign 会生成一个代理类,对所有通过该接口发起的远程调用进行动态代理。
OpenFeign 会针对每一个 FeignClient 会生成一个动态代理对象,即图中的 FeignProxyService,这个代理对象在继承关系上属于FeignClient注解所修饰的接口实例。
接下来,这个动态代理对象会被添加到Spring上下文,并注入到对应的服务里,也就是图中的 LocalService 服务。
最后,LoacalService 会发起底层方法调用。实际上这个方法调用会被 OpenFeign 生成的代理对象接管,由代理对象发起一个远程服务调用,并将调用的结果返回给 LocalService。
动态代理流程图如下
在 FeignClientsRegistrar 扫包的过程中还会构造出FeignClientFactoryBean,它有两个重要功能,一个是解析 FeignClient 接口中请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中动态代理构造是由下一层 ReflectiveFeign 完成的。
ReflectiveFeign 包含了 OpenFeign 动态代理的核心逻辑,它主要负责创建出 FeignCLient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个重要任务,一个是解析 FeignCLient接口上各个方法级别的注解,将其中的远程调用 URL、接口类型(GET、POST等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要的任务是将这些 MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实现了 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到FeignClient 接口上。这样一来,发生在 FeignCLient 接口上的调用,最终都会由它背后的动态代理对象来承接。
如果有调用过来时就会通过代理类实现动态代理的过程。
三、负载均衡
在微服务领域,面对一个庞大的微服务集群,如果每次发起服务调用到请求到一两台机器上,在大用户访问的情况下,这几台服务器一定不堪重负。
因此我们需要将访问流量分散到集群中的各个服务器上,实现雨露均沾,这就是所谓的负载均衡技术。
实现起来有两条不同的途径,负载均衡有两大门派
- 服务端负载均衡
- 客户端负载均衡
3.1 服务层负载均衡
网关层负载均衡也被称为服务端负载均衡,就是在服务集群内设置一个中心化负载均衡器,比如 API Gateway服务。发起服务间调用的时候,服务请求并不直接发向目标服务器,而是发给这个全局负载均衡器,它在根据配置的负载均衡策略将请求转发到目标服务。
网关层负载均衡的应用范围非常广,它不依赖于服务发现技术,客户端并不需要拉取完整的服务列表;同时发起服务调用的客户端也不用操心使用什么负载均衡策略。
不过,网关层负载均衡的劣势也很明显:
- 网络消耗:多了一次客户端请求网关层的网络开销,在线上高并发场景下这层调用会增加10ms~20ms左右的响应时间,在超高 QPS 的场景下,性能损耗也会被同步放大,降低系统的吞吐量;
- 复杂度和故障率提升:需要额外搭建内部网关组件作为负载均衡器,增加了系统复杂度,而多出来的那一网络调用无疑也增加了请求失败率。
Spring CLoud Loadbalancer 可以很好的弥补这些劣势。
3.2 客户端负载均衡
Spring CLoud Loadbalancer 采用了客户端负载均衡技术,每个发起服务调用的客户端都存有完整的目标服务地址列表,根据配置的负载均衡策略,由客户端自己决定向哪台服务器发起的调用。
客户端负载均衡的优势很明显:
- 网络开销小:客户端直接发起点对点的服务调用;
- 配置灵活:各个客户端可以根据自己的需要灵活定制负载均衡策略。
所以需要使用注册中心来获取全部的调用方地址列表,这样才能实现负载均衡策略。loadbalancer 提供了两种内置的负载均衡策略:
- RandomLoadBalancer:在服务列表中随机挑选一台服务器发起调用
- RoundRobinLoadBalancer:通过内部保存一个 position 计数器,按照次序从上到下依次调用服务,每次调用后计数器加1,属于排好一个队列一个个来。
3.3 OpenFeign 与 Loadbalancer 实现负载均衡
当引入负载均衡的依赖后,Spring Cloud 会为 OpenFeign 配置一个 LoadBalancerClient,这时对原生 Feign 客户端的包装,他会在执行 HTTP 请求前,通过 LoadBalancerClient 决定请求的目标服务实际地址。
这样就实现了客户端负载均衡的效果。
关于 OpenFeign 的介绍就到这里,欢迎留言讨论。
往期经典推荐
一文看懂Nacos如何实现高效、动态的配置中心管理-CSDN博客
Spring Cloud + Nacos 引领服务治理新航向-CSDN博客