【OpenFeign】@FeignClient 注入过程源码分析

1  前言

微服务之间的调用,OpenFeign 是一种选择,并且还提供了很多功能,比如我们有多个节点,它能负载均衡,当服务发生异常时,它还能提供熔断机制。所以它是怎么实现的,因为我们平时只需要写 @FeignClient 是个接口,所以它势必会走代理,所以是不是要从我们的 @FeignClient  下手。那么这节我们就先简单看下OpenFeign 中 @FeignClient 代理的创建过程,当代理的创建过程知道了,然后我们就可以深入它的增强逻辑,继而能看到它的负载均衡、熔断又是如何做的对吧。

如果你之前看过我的 【Mybatis】【二】源码分析-Mapper 接口都是怎么注入到 Spring容器中的?  以及【Mybatis】【三】源码分析- MapperFactoryBean 的创建过程以及 Mapper 接口代理的生成过程详解 那么这节你就能很好的理解,因为他俩的注入过程太像了,基本都差不多。大体都是先扫描包、然后注入FactoryBean 形式的 BeanDefinition 来实现的。

那么废话不多说,我们直接看。

2  源码分析

2.1  入口 EnableFeignClients 

起点在哪里,是不是我们的启动类上,是不是会配置基本的扫描包,@EnableFeignClients 它就是入口。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

可以看到它引入了 FeignClientsRegistrar,从名称上就可以看出,它就是 FeignClient 的注册器。

2.2  注册器 FeignClientsRegistrar 

那我们看看注册器里都干了什么:

// 可以看到它实现了 ImportBeanDefinitionRegistrar
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    // 注册 BeanDefinition
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 注册默认配置  @FeignClient 注解有个属性是 defaultConfiguration 自定义全局默认配置比如可以配置你自己的重试机制、编码器、解码器等也就是控制 @FeignClient 的行为的(通常不需要配)
        this.registerDefaultConfiguration(metadata, registry);
        // 注册 @FeignClient
        this.registerFeignClients(metadata, registry);
    }
}

如上可以看到大概两部分,一部分是默认的全局配置、一部分是注册我们的 @FeignClient。

2.2.1  全局默认配置类 registerDefaultConfiguration 

这个我们就先草草略过:

// @see FeignClientsRegistrar#registerDefaultConfiguration
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
    if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
        String name;
        if (metadata.hasEnclosingClass()) {
            name = "default." + metadata.getEnclosingClassName();
        } else {
            name = "default." + metadata.getClassName();
        }
        this.registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
    }
}
// @see FeignClientsRegistrar#registerClientConfiguration
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
    // FeignClientSpecification
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
    registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition());
}

2.2.2  注册 registerFeignClients

我们瞅瞅具体都做了哪些内容:

// 开始注册 FeignClient
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 存放候选的 FeignClient 的 BeanDefinition
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
    // 获取注解信息
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    // @EnableFeignClients 可以直接配置 Client 我们平时基本不配这个,因为当存在大量的 Client 的话那不是费劲么,每新加一个还要配一个多麻烦 所以我们平时配一个或者多个包名    
    Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
    // 当配置了 Client 属性值的话,用 Annotated 的 BeanDefinition 包起来
    if (clients != null && clients.length != 0) {
        Class[] var12 = clients;
        int var14 = clients.length;
        for(int var16 = 0; var16 < var14; ++var16) {
            Class<?> clazz = var12[var16];
            candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
        }
    } else {
        // 扫描器
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        // 指定扫描的筛子 也就是过滤出 @FeignClient 注解的类
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        // 获取注解上配的包名
        Set<String> basePackages = this.getBasePackages(metadata);
        // 逐个循环扫描包下的类
        Iterator var8 = basePackages.iterator();
        while(var8.hasNext()) {
            String basePackage = (String)var8.next();
            // 都加入到集合中
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }
    // 对集合中的每个类进行注入
    Iterator var13 = candidateComponents.iterator();
    while(var13.hasNext()) {
        BeanDefinition candidateComponent = (BeanDefinition)var13.next();
        // 对每一个进行基础检查 然后进行注入
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            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 = this.getClientName(attributes);
            // 每个 @FeignClient 也可以设置自己的配置
            this.registerClientConfiguration(registry, name, attributes.get("configuration"));
            // 注册 FeignClient
            this.registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
    registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition());
}

可以看到会从我们配置的包下扫描出 @FeignClient 的接口,然后进行注入,并且每个 Client 也都会注入一个自己的 ClientConfiguration,即使没有配置也会注入一个这样的 BeanDefinition 哈。

那我们继续看下 FeignClient 的具体注入:

// annotationMetadata 就是 @FeignClient 注解信息
// attributes 就是每个属性的值
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    // 我们的 @FeignClient 下的接口名
    String className = annotationMetadata.getClassName();
    Class clazz = ClassUtils.resolveClassName(className, (ClassLoader)null);
    ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory)registry : null;
    // 我们常设置的 contextId 和 name  contextId没有设置的会拿 name 属性的值  name 属性的值又会先获取 serviceId 的值,没有的话拿 name 再没有的话会拿 value  并且他俩都可以设置表达式 ${}这种写法 会根据环境变量等配置属性解析的
    String contextId = this.getContextId(beanFactory, attributes);
    String name = this.getName(attributes);
    // 嘿嘿, 外壳是用 FeignClientFactoryBean 来包装的
    FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
    factoryBean.setBeanFactory(beanFactory);
    factoryBean.setName(name);
    factoryBean.setContextId(contextId);
    factoryBean.setType(clazz);
    // 是否开启配置动态刷新这个跟 feign.client.refresh-enabled 有关默认是 false
    factoryBean.setRefreshableClient(this.isClientRefreshEnabled());
    BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
        factoryBean.setUrl(this.getUrl(beanFactory, attributes));
        factoryBean.setPath(this.getPath(beanFactory, attributes));
        factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
        Object fallback = attributes.get("fallback");
        // 两个异常钩子
        if (fallback != null) {
            factoryBean.setFallback(fallback instanceof Class ? (Class)fallback : ClassUtils.resolveClassName(fallback.toString(), (ClassLoader)null));
        }
        
        Object fallbackFactory = attributes.get("fallbackFactory");
        if (fallbackFactory != null) {
            factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class)fallbackFactory : ClassUtils.resolveClassName(fallbackFactory.toString(), (ClassLoader)null));
        }
        
        return factoryBean.getObject();
    });
    // 装配模式=2 也就是根据类型装配 和 mabatis 里的 MapperFactoryBean 异曲同工
    definition.setAutowireMode(2);
    // 延迟加载的 也就是没有人依赖这个 FeignClient 它就不会创建  很合理
    definition.setLazyInit(true);
    // 校验属性 主要是检查两个失败回调的设置
    this.validate(attributes);
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
    beanDefinition.setAttribute("factoryBeanObjectType", className);
    beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
    // 哟 还能设置主的
    boolean primary = (Boolean)attributes.get("primary");
    beanDefinition.setPrimary(primary);
    String[] qualifiers = this.getQualifiers(attributes);
    if (ObjectUtils.isEmpty(qualifiers)) {
        qualifiers = new String[]{contextId + "FeignClient"};
    }
    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    // 如果开启了动态配置刷新的话  会更改 BeanDefinition 的 scope 来进行增强
    this.registerOptionsBeanDefinition(registry, contextId);
}

别看这么二三十行代码,它东西还挺全乎,我们看看它取了哪些重要属性哈:

(1)contextId 

取的就是注解里的 contextId 属性值,没有的话就等于 name 的属性值,并且它配有 BeanFactory 的 resolve,也就是说支持 spring 的表达式 类似我们平时 @Value 或者配置文件里的表达式写法

private String getContextId(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
    String contextId = (String)attributes.get("contextId");
    // 没有 contextId 那么 contextId 就等于 name 属性的值
    if (!StringUtils.hasText(contextId)) {
        return this.getName(attributes);
    } else {
        // 设置了的话,通过 getName 检查一下 检查的方式主要是 加上协议前缀然后 URI.host 一下 这是为什么的  我不得其解
        contextId = this.resolve(beanFactory, contextId);
        return getName(contextId);
    }
}
static String getName(String name) {
    if (!StringUtils.hasText(name)) {
        return "";
    } else {
        String host = null;
        try {
            String url;
            if (!name.startsWith("http://") && !name.startsWith("https://")) {
                url = "http://" + name;
            } else {
                url = name;
            }
            host = (new URI(url)).getHost();
        } catch (URISyntaxException var3) {
        }
        Assert.state(host != null, "Service id not legal hostname (" + name + ")");
        return name;
    }
}

(2)name

我们继续看看 name 的获取,可以看到顺序获取的优先级:serviceId > name > value:

String getName(Map<String, Object> attributes) {
    return this.getName((ConfigurableBeanFactory)null, attributes);
}
// 重载
String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
    // 先获取 serviceId 的属性值
    String name = (String)attributes.get("serviceId");
    // 没有的话 再获取 name的值
    if (!StringUtils.hasText(name)) {
        name = (String)attributes.get("name");
    }
    // 还没有的话 获取 value的值
    if (!StringUtils.hasText(name)) {
        name = (String)attributes.get("value");
    }
    // 表达式解析 虽然beanFactory为空,它有 environment 进行解析
    name = this.resolve(beanFactory, name);
    // 一样还是要检查一下 URI.host  还是不得其解
    return getName(name);
}
static String getName(String name) {
    if (!StringUtils.hasText(name)) {
        return "";
    } else {
        String host = null;
        try {
            String url;
            if (!name.startsWith("http://") && !name.startsWith("https://")) {
                url = "http://" + name;
            } else {
                url = name;
            }
            host = (new URI(url)).getHost();
        } catch (URISyntaxException var3) {
        }
        Assert.state(host != null, "Service id not legal hostname (" + name + ")");
        return name;
    }
}

(3)url 的获取

private String getUrl(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
    // 一样解析表达式
    String url = this.resolve(beanFactory, (String)attributes.get("url"));
    // url 通过 URL 解析 这个我能理解 上边那俩我就不太理解为什么要用 URL.host
    return getUrl(url);
}
static String getUrl(String url) {
    if (StringUtils.hasText(url) && (!url.startsWith("#{") || !url.contains("}"))) {
        // 会补 http://
        if (!url.contains("://")) {
            url = "http://" + url;
        }
        try {
            new URL(url);
        } catch (MalformedURLException var2) {
            throw new IllegalArgumentException(url + " is malformed", var2);
        }
    }
    return url;
}

(4)path 路径的获取

private String getPath(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
    String path = this.resolve(beanFactory, (String)attributes.get("path"));
    return getPath(path);
}
static String getPath(String path) {
    if (StringUtils.hasText(path)) {
        // 去除多余空格
        path = path.trim();
        // 贴心自动补 / 前缀
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        // 去掉最后的 /
        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }
    }
    return path;
}

BeanDefinition 是 FactoryBean 方式的,类型为FeignClientFactoryBean,还有 lazyInit 为 true 表示懒加载开启,装配模式=2表示根据类型自动装配,当多个冲突的时候还可以设置 Primary,最后还有一个重要的就是配置热加载的,这个就涉及到 spring 中Scope 的原理哈。这里就不详细解释了,后序单独讲这个 Scope。

那么最后我们画个图小小的捋一下:

大家看源码一定要画图,真的,不画图的话你后续再回忆的时候,又要一点点再看起,这样当你看完画个思路图,下次看的时候直接看图就大概能想起来怎么个过程,是不是。

3  小结

好啦,本节就看到这里,下节我们看看代理的创建以及执行过程,有理解不对的地方欢迎指正哈。
posted @ 2024-03-21 08:56  酷酷-  阅读(266)  评论(0编辑  收藏  举报