Openfeign 蓝图

Last Modified: 2023/02/03

前言

众所周知,我们在使用 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。

总结

一图胜于千言,放张图吧:

有问题吗?点此反馈!

温馨提示:反馈需要登录