Openfeign 蓝图
前言
众所周知,我们在使用 Openfeign 的时候,只需要定义接口即可完成微服务之间的调用,从使用的角度看很简单,但是它背后的原理并不简单,借助 Openfeign 接口调用,我们能够自动享受缓存、负载均衡、限流、熔断降级等功能(需要集成 sentinel 或 hystrix 等),然而这些功能是如何集成到 Openfeign 的呢?我们一起来看下完整拼图。
注①:本文针对 spring cloud alibaba 2021.0.4.0,并且假设已经集成了 sentinel。另外本文需要对 spring 框架有一定的了解,因此不适合新手同学;
注②://...
表示省略无关代码,这是为了关注重点并节约篇幅。
本文将会用到的示例代码
本文以订单服务为例,说明 Openfeign 的接口调用是如何集成各种功能的,接口定义如下:
@FeignClient(contextId = "remoteOrderService", value = "order-service", fallbackFactory = OrderFallbackFactory.class)
public interface RemoteOrderService {
@PostMapping(value = "/placeOrder", consumes = MediaType.APPLICATION_JSON)
public Order placeOrder(@RequestBody OrderReq req);
}
接口使用方法如下:
// 注入 remote order service
@Autowired
RemoteOrderService remoteOrderService;
// 调用
remoteOrderService.placeOrder(req);
为什么定义了接口就可以完成注入
敏锐的同学,可能一下子就想到了动态代理。没错,实际上就是动态代理,只不过相关代码被隐藏的很深而已,相关代码在 ReflectiveFeign 中:
@SuppressWarnings("unchecked")
@Override
public <T> T newInstance(Target<T> target) {
// ...
InvocationHandler handler = factory.create(target, methodToHandler);
// 以我们的订单接口为例,target.type() 就是 RemoteOrderService
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
// ...
return proxy;
}
当我们注入 RemoteOrderService bean 时,实际上注入是这个动态代理对象。这个动态代理对象功能强大,支持负载均衡、缓存(spring cache)、限流、降级等,之后我们会依次介绍这些功能是如何集成到这个动态代理对象的。
在开始介绍功能集成之前,有必要说下为什么可以注入 RemoteOrderService bean?可以注入的前提一定要有 bean definition,spring 框架根据 bean definition 构造 bean 对象,让我回到魔法开始的地方。
我们使用 Openfeign 的第一步是在我们启动类上加上 @EnableFeignClients
注解,该注解定义如下:
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients { // ...}
上面代码的重点在于 @Import(FeignClientsRegistrar.class)
,我们知道 @Import
是用来导入资源的,正是通过 FeignClientsRegistrar 类的 registerBeanDefinitions 方法向 spring 容器注册 bean definition。该方法会扫描 @FeignClient
注解,并为注解标记的类注册对应的 bean definition。具体来说,被 @FeignClient
注解标记的接口会依次调用 registerFeignClient 方法以完成 bean definition 的注册。
以我们的示例代码为例,在调用 registerFeignClient 方法之后,RemoteOrderService bean 的 bean definition 会被注册到容器中。后续当我们通过 @Autowired
注入 RemoteOrderService bean 时,下面代码中的 genericBeanDefinition 方法的第二个参数对应的回调方法会被触发,完成 bean 对象的构建。
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
Map<String, Object> attributes) {
// ...
// clazz 为 RemoteOrderService.class
Class clazz = ClassUtils.resolveClassName(className, null);
// ...
// RemoteOrderService bean 的构建过程坑爹的复杂,因此需要通过工厂模式完成构建
// FeignClientFactoryBean 正是构建 RemoteOrderService bean 的核心
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
// ...
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
// 这里的回调会在我们注入 RemoteOrderService bean 时被调用
// ...
// factoryBean.getObject 会生成 RemoteOrderService 对象
return factoryBean.getObject();
});
// ...
// 注册 RemoteOrderService bean definition
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
到这里我们知道了正是通过 FeignClientFactoryBean#getObject 完成了 RemoteOrderService bean 对象的构建。然而这个构建过程是复杂的,用到的核心技术是动态代理和 NamedContextFactory。动态代理的目的是为了根据接口生成对象,NamedContextFactory 是为了配置隔离。配置隔离的好处是每个 feign client 可以有自己独立的一套配置。
注:NamedContextFactory 可以实现配置隔离,但不代表所有的配置都需要隔离,NamedContextFactory 如果设置了父容器,那么 NamedContextFactory 中的所有子容器可以通过父容器共享一些默认配置。
前奏有些长,现在让我们回归正轨,我们将依次来看负载均衡、缓存、限流、降级等功能是如何被植入到 RemoteOrderService bean 中的。
负载均衡
前面我们提到了 RemoteOrderService bean 实际上是通过 FeignClientFactoryBean#getObject 完成构建的,getObject 会调用 getTarget 方法,进入 getTarget 就会发现负载均衡的植入:
<T> T getTarget() {
// FeignContext 是 NamedContextFactory 的子类,通过 FeignContext 对象完成 feign client 配置隔离
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(url)) {
// ...
// 当 未配置 url的时候,就植入负载均衡逻辑
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
}
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
}
回顾下之前我们的 RemoteOrderService 接口上添加的注解,可以看出并没有设置 url 属性,因此一定会走到负载均衡分支。
@FeignClient(contextId = "remoteOrderService", value = "order-service", fallbackFactory = OrderFallbackFactory.class)
如果上面的注解中明确指定了 url,那么 feign 会将请求发送到 url 指定的地址,否则根据 value 指定的服务名称(order-service),配合注册中心可以知道 order-service 所有实例的端口和地址,并使用某种负载均衡算法(可配置)将请求发送到其中一个实例上。
究竟是如何植入负载均衡逻辑呢,我们进入 loadBalance 方法一探究竟:
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
// ...
}
// ...
}
代码非常简单,就是从容器中获取 Client 对象,并放到 Feign.Builder 对象中。Client 对象是用来发送请求并获取响应的,然而这和负载均衡有什么关系呢?玄机就在于这个从容器中获取的 Client 对象本身,这个 Client 是支持负载均衡功能的 Client。那么这个对象是在哪注册注册到容器中的呢?
我们知道 spring boot 支持自动化配置,这个 bean 正是通过自动配置注入的:
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
HttpClient5FeignLoadBalancerConfiguration.class, DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
class DefaultFeignLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
@Conditional(OnRetryNotEnabledCondition.class)
public Client feignClient(LoadBalancerClient loadBalancerClient,
LoadBalancerClientFactory loadBalancerClientFactory) {
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
loadBalancerClientFactory);
}
}
缓存
在 spring 中,我们可以通过 spring cache 提供的一系列注解完成缓存功能,feign 也对 spring cache 提供了支持,默认情况下容器会被注入一个 CachingCapability bean,如果我们使用 @EnableCaching
开启了 spring cache,那么 feign 就能识别 @Cache*
相关的注解,举个例子:
@FeignClient(contextId = "remoteOrderService", value = "order-service", fallbackFactory = OrderFallbackFactory.class)
public interface RemoteOrderService {
@GetMapping(value = "/getOrder")
@Cacheable(cacheNames = "order-cache", key = "#id")
public Order getOrder(@RequestParam String id);
}
使用不复杂,但是缓存功能是如何被集成到 RemoteOrderService bean 中的呢?前面我们提到了 RemoteOrderService bean 构建使用了动态代理技术,相关代码在 ReflectiveFeign 中:
public <T> T newInstance(Target<T> target) {
// ...
// factory 的类型为 InvocationHandlerFactory,用来构建动态代理对象所需的 InvocationHandler 对象
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
// ...
return proxy;
}
关键点在就在于 InvocationHandlerFactory,从这个类的名字我们就知道他是为了构造 InvocationHandler 对象的工厂。 ReflectiveFeign 对象中的 InvocationHandlerFactory 对象是来自 Feign.Builder 对象,因此我们需要从 Feign.Builder 对象中寻找线索。Feign.Builder 对象的 build 方法会将 InvocationHandlerFactory 对象传递给 ReflectiveFeign 构造方法:
public Feign build() {
// ...
InvocationHandlerFactory invocationHandlerFactory =
Capability.enrich(this.invocationHandlerFactory, capabilities);
// ...
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
InvocationHandlerFactory 对象传递给 ReflectiveFeign 构造方法之前会经过一步 Capability.enrich,enrich 之后的对象类型保持不变,这有点像 decorator 设计模式。enrich 方法接收一个 Capability 列表,其中一个 Capability 对象为 CachingCapability,它是在 FeignAutoConfiguration 被自动注入容器的:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class,
FeignEncoderProperties.class })
public class FeignAutoConfiguration {
@Bean
@ConditionalOnProperty(value = "feign.cache.enabled", matchIfMissing = true)
@ConditionalOnBean(CacheInterceptor.class)
public Capability cachingCapability(CacheInterceptor cacheInterceptor) {
return new CachingCapability(cacheInterceptor);
}
}
CachingCapability 的 enrich 方法如下,可见经过 enrich 之后,InvocationHandlerFactory 的具体类型为 FeignCachingInvocationHandlerFactory。
@Override
public InvocationHandlerFactory enrich(InvocationHandlerFactory invocationHandlerFactory) {
return new FeignCachingInvocationHandlerFactory(invocationHandlerFactory, cacheInterceptor);
}
到这里我们知道缓存的处理就隐藏在 FeignCachingInvocationHandlerFactory 类的 create 方法中:
@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
final InvocationHandler delegateHandler = delegateFactory.create(target, dispatch);
return (proxy, method, argsNullable) -> {
// ① 这里返回的 InvocationHandler 对象的实现委托给 cacheInterceptor 处理
Object[] args = Optional.ofNullable(argsNullable).orElseGet(() -> new Object[0]);
return cacheInterceptor.invoke(new MethodInvocation() {
// ...
@Override
public Object proceed() throws Throwable {
return delegateHandler.invoke(proxy, method, args);
}
// ...
});
};
}
到这里大家应该明白了,RemoteOrderService bean 对象的方法调用之前会被 cacheInterceptor 拦截,cacheInterceptor 正是 spring cache 通过切面编程的方式处理各种 @Cache*
注解的核心类。
限流、熔断降级
上一节我们讲解了 feign 的方法缓存功能是通过 CachingCapability.enrich 方法增强 InvocationHandlerFactory 对象完成的, 但是这里还有一个问题 Feign.Builder 对象的中增强之前的 InvocationHandlerFactory 对象从哪来?查看 Feign.Builder 类可以找到:
public abstract class Feign {
public static class Builder {
private InvocationHandlerFactory invocationHandlerFactory =
new InvocationHandlerFactory.Default();
// ...
}
// ...
}
可见默认的 InvocationHandlerFactory 对象是 new InvocationHandlerFactory.Default()。但是当我们集成了 sentinel 的时候,容器中 Feign.Builder 对象实际类型为 SentinelFeign.Builder,该类中重写了 build 方法,通过 super.invocationHandlerFactory(xx) 重新设置了 InvocationHandlerFactory 对象,对应的代码如下:
public final class SentinelFeign {
// ...
public static Builder builder() {
return new Builder();
}
public static final class Builder extends Feign.Builder
implements ApplicationContextAware {
// ....
@Override
public Feign build() {
super.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override
public InvocationHandler create(Target target,
// ...
return new SentinelInvocationHandler(target, dispatch);
}
});
super.contract(new SentinelContractHolder(contract));
return super.build();
}
// ...
}
}
上面提到容器中 Feign.Builder 对象实际类型为 SentinelFeign.Builder,这也是通过自动配置完成的,代码如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class, Feign.class })
public class SentinelFeignAutoConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.sentinel.enabled")
public Feign.Builder feignSentinelBuilder() {
return SentinelFeign.builder();
}
}
到这里大家应该知道熔断降级和限流等功能都是通过 SentinelFeign.Builder 中设置的 InvocationHandlerFactory 对象的 create 方法返回的 SentinelInvocationHandler 实现的。现在我们有必要到该类中看看熔断和限流功能的实现:
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
// ...
// only handle by HardCodedTarget
if (target instanceof Target.HardCodedTarget) {
Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target;
MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
.get(hardCodedTarget.type().getName()
+ Feign.configKey(hardCodedTarget.type(), method));
// resource default is HttpMethod:protocol://url
if (methodMetadata == null) {
result = methodHandler.invoke(args);
}
else {
String resourceName = methodMetadata.template().method().toUpperCase()
+ ":" + hardCodedTarget.url() + methodMetadata.template().path();
Entry entry = null;
try {
ContextUtil.enter(resourceName);
entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
result = methodHandler.invoke(args);
}
catch (Throwable ex) {
// 发生异常时会调用 FeignClient 注解上配置的 fallback 或者 fallbackFactory 中对应的方法。
}
finally {
if (entry != null) {
entry.exit(1, args);
}
ContextUtil.exit();
}
}
}
// ...
}
我们知道 sentinel 根据资源名称制定限流和熔断规则,通过上面的代码可以看出熔断限流对应的资源名称为:
String resourceName = methodMetadata.template().method().toUpperCase()
+ ":" + hardCodedTarget.url() + methodMetadata.template().path();
以 getOrder 方法为例,资源名称为 GET:http://order-service/getOrder
@FeignClient(contextId = "remoteOrderService", value = "order-service", fallbackFactory = OrderFallbackFactory.class)
public interface RemoteOrderService {
@GetMapping(value = "/getOrder")
public Order getOrder(@RequestParam String id);
}
限流实战
由于需要通过 sentinel dashboard 下发限流规则,因此需要下载 sentinel dashbaord并启动。
nohup java -jar -Dserver.port=8718 -Dcsp.sentinel.dashboard.server=127.0.0.1:8718 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar &
这里假设我们应用的名称为 ruoyi-sytem,现在我们需要配置我们的应用,将其接入到 dashboard:
spring:
application:
# 应用名称
name: ruoyi-system
cloud:
sentinel:
eager: true
transport:
# 控制台地址
dashboard: 127.0.0.1:8718
然后是制定限流规则,在 dashboard 中找到 ruoyi-system,新增流控规则:
这里我们这是 qps 为 1,如果一秒内请求数量超过 1,就会触发流控,具体来说就是抛出 com.alibaba.csp.sentinel.slots.block.flow.FlowException。
熔断也是类似的操作,只不过是在 dashboard 中配置一个熔断规则,当触发熔断时,会抛出 com.alibaba.csp.sentinel.slots.block.degrade.DegradeException。
总结
一图胜于千言,放张图吧:
温馨提示:反馈需要登录