Spring Cloud FeignClient 代理方式探究

背景

每次跟人讲起 feignClient 的大致原理,我都是含糊其词:

程序启动时,Spring 会为每个加了 @FeignClient(name=”provider”) 注解的接口生成一个代理 bean, 名称为注解的 name 属性(本例为 provider ),方法就为接口的方法列表。等你执行接口某个方法的时候,代理的方法就会帮你做请求和响应的参数拼接以及 HTTP 请求

直到最近被一个同事打破砂锅问到底,我反倒一脸懵逼。虽然上面那套说辞大体上能自圆其说,但是真相还是需要亲身去探索。

程序入口

我们都知道,如果想使用 FeignClient,需要在启动主类上添加 @EnableFeignClients 注解。那么 Feign 的一套体系生效应该就在于这个注解上了。我们打开 EnableFeignClients 看下源码:

@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    。。。。
}
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,ResourceLoaderAware,EnvironmentAware {
    @Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
		。。。。
		registerFeignClients(metadata, registry);
	}
    public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
        。。。。
        for (BeanDefinition candidateComponent : candidateComponents) {
            // 注意这里,开始寻找添加 @FeignClient 注解的接口,并在后续进行注册
            Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());

            String name = getClientName(attributes);
            registerClientConfiguration(registry, name,attributes.get("configuration"));

            registerFeignClient(registry, annotationMetadata, attributes);
        }
        。。。。
     }
}

上文代码印证了我们之前的猜测:Feign 组件通过 EnableFeignClients 作为入口生效。具体执行过程如下:

  1. 在用户添加了该注解后,该注解引入了 FeignClientsRegistrar 配置
  2. FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,然后在 registerBeanDefinitions 方法中将加上 @FeignClient 注解的接口均在 spring 中做了注册(这句表述不太准确,用户完全可以自定义扫描包之类的,简单起见不做扩展)

生成代理

被 @FeignClient 注解的接口被抛给了 spring 去注册,但是注册的代理对象是在 FeignClientFactoryBean 返回的:

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,ApplicationContextAware {
    @Override
	public Object getObject() throws Exception {
		FeignContext context = applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);
        // 如果没配置 url 属性,则需要走服务发现逻辑
		if (!StringUtils.hasText(this.url)) {
			String url = "http://" + this.name;
			return loadBalance(builder, context, new HardCodedTarget<>(this.type,this.name, url));
		}
        // 注意,这里的 Client 非常重要,它是 feign 网络请求组件接口,默认是 Default
		Client client = getOptional(context, Client.class);
		if (client != null) {
			if (client instanceof LoadBalancerFeignClient) {
				// not load balancing because we have a url,
				// but ribbon is on the classpath, so unwrap
				client = ((LoadBalancerFeignClient)client).getDelegate();
			}
			builder.client(client);
		}

		Targeter targeter = get(context, Targeter.class);
		return targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url));
	}
}

由上文代码我们可以看出,在生成代理的时候分了两种情况:如果 @FeignClient 注解配置了 url 属性,则直接生成代理;如果没有配置,证明需要走服务发现逻辑,则交给 Ribbon 去拼接 url 并生成代理。
我们这里以简单的 url 配置方式为例,看看最终的代理对象是如何生成的:

public class ReflectiveFeign extends Feign {

  @Override
  public <T> T newInstance(Target<T> target) {
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();

    for (Method method : target.type().getMethods()) {
        // 注意,这里的逻辑比源码删减很多
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
    }
    InvocationHandler handler = factory.create(target, methodToHandler);
    // 使用 jdk 代理生成对象,注意这里的 target 并非被 @FeignClient 注解的接口,而是 HardCodedTarget,但是它里面存储了被注解接口的信息
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

    return proxy;
  }

其实,源码中跳转了几次:

  1. FeignClientFactoryBean 使用 Targeter.target() 去生成被代理对象
  2. Targeter 使用 Feign.target() 生成被代理对象
  3. Feign 中的 target() 调用抽象方法 newInstance() 来生成对象
  4. ReflectiveFeign 继承自 Feign, 并实现了 newInstance() 方法
  5. ReflectiveFeign.newInstance() 中使用 jdk 代理,将被 @FeignClient 注解的接口的所有方法通过 jdk 代理来实现。

总结

经过上面的代码分析,我们大致能将整个流程串了起来:

  1. 在用户加上 EnableFeignClients 注解后,整个 Feign 组件被启用
  2. FeignClientsRegistrar 在启动时,将被加了 FeignClient 注解的(而且符合配置条件的)接口注册成 bean
  3. FeignClientFactoryBean 梳理 url 信息,并选择 HTTP 组件,将信息一并交给 ReflectiveFeign 生成代理类
  4. ReflectiveFeign 收到请求后,以 HardCodedTarget 为蓝本,使用 jdk 动态代理,生成代理对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值