动态注册Bean到Spring上下文中——基于FeignClient源码的阅读

在上一篇博文《基于Feign的局部请求拦截》的最后,我提出了如何实现系统启动将自定义注解的bean注入到Spring的ApplicationContext中,那么本博文我们就来探讨下具体的代码流程

基于Feign的局部请求拦截

小伙伴们在使用SpringCloud中集成的Feign功能时,只需要编写一个接口,然后再给接口上添加注解@FeignClient,然后配置上相关信息既可以调用其他系统的业务接口,非常的方便;

这里我们不讲解如果在SpringCloud中集成Feign功能,这个网上有大把的博文来讲解该如何使用,说的肯定比我的要详细,精彩;在这里我就基于上一篇博文中,如果将添加自定义注解的组件扫描注入Spring的上下文中;

说明:本博文主要实现的功能是将指定路径下的接口上添加指定注解的对象,通过代理工厂来生成对应的实例对象,然后将该对象注册到Spring的上下文中

具体的业务流程逻辑是:

  1. 在SpringBoot的启动类中添加自定义注解,在该注解中通过 @Import导入自定义注册器
  2. 自定义注册器主要实现如下接口:
    1. ImportBeanDefinitionRegistrar: 该类只能通过其他类@Import的方式来加载,通常是启动类或配置类,通过实现registerBeanDefinitions方法来向Spring上下文中注册自定义的bean组件
    2. ResourceLoaderAware: 获取资源加载器,可以获得外部资源文件
    3. BeanClassLoaderAware: 该接口有个setBeanClassLoader方法,与前两个接口类似,实现了该接口后,可以向bean中注入加载该bean的ClassLoader
    4. EnvironmentAware: 获取项目的环境信息
  3. ImportBeanDefinitionRegistrar中的registerBeanDefinitions来实现注册的功能
  4. 通过注解中指定的扫描路径,然后扫描添加指定注解的接口对象
  5. 然后通过代理工厂的方式来生成该接口的实例对象
  6. 将该实例对象注册到Spring的上下文中

代码

知道了大体的代码流程逻辑,我们就废话不多说了,直接上代码:

  1. 启动类上添加自定义注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.amos.baseframework.remote"})
@EnableRxFeignLocality(scanPackages = {"com.amos.baseframework.remote"})
public class BaseFrameworkApplication {

    public static void main(String[] args) {
        SpringApplication.run(BaseFrameworkApplication.class, args);
    }

EnableRxFeignLocality就是我们自定义的注解,具体的代码如下:

/**
 * Copyright © 2018 五月工作室. All rights reserved.
 *
 * @Project: springcloudfunctionsample
 * @ClassName: EnableRxFeignLocality
 * @Package: com.amos.baseframework.anno
 * @author: amos
 * @Description: Fegin局部拦截注册器
 * <p>
 * 主要项目启动时扫描指定的包路径下面含有指定注解的组件,
 * 并且使用代理工厂生成对象,然后注册到Spring的ApplicationContext中
 * @date: 2020/2/21 0021 下午 16:09
 * @Version: V1.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RxFeignLocalityRegister.class)
public @interface EnableRxFeignLocality {
    /**
     * 扫描包路径
     *
     * @return
     */
    String[] scanPackages() default {};
}

这里我们就看到 @Import(RxFeignLocalityRegister.class) 这段代码,这里就是我们自定义的注册器

  1. 自定义注册器
/**
 * Copyright © 2018 五月工作室. All rights reserved.
 *
 * @Project: springcloudfunctionsample
 * @ClassName: RxFeignLocalityRegister
 * @Package: com.amos.baseframework.register
 * @author: amos
 * @Description:
 * @date: 2020/2/21 0021 下午 16:29
 * @Version: V1.0
 */
public class RxFeignLocalityRegister implements ImportBeanDefinitionRegistrar,
        ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {

    public static final Logger logger = LoggerFactory.getLogger(RxFeignLocalityRegister.class);

    private ClassLoader classLoader;

    private ResourceLoader resourceLoader;

    private Environment environment;

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    /**
     * 存放 @EnableRxFeignLocality 注解的所有属性
     */
    private Map<String, Object> enableRxFeignLocalityAttributes = null;

    /**
     * 实现该方法,向Spring上下文中注册指定路径下,指定注解的Bean对象
     *
     * @param metadata 注解的元信息
     * @param registry Spring内置的注册器
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        enableRxFeignLocalityAttributes = metadata.getAnnotationAttributes(EnableRxFeignLocality.class.getName(), Boolean.TRUE);
        logger.info("@EnableRxFeignLocality 注解中属性:{}", enableRxFeignLocalityAttributes);
        // 扫描自定义注解中指定路径下的Bean组件,并且将其注册到Spring的上下文中
        this.registerRxFeignClient(metadata, registry);
    }
    
    ......
}

这里我们主要来重写 registerBeanDefinitions方法来具体的功能

    /**
     * 扫描自定义注解中指定路径下的Bean组件,并且将其注册到Spring的上下文中
     *
     * @param metadata
     * @param registry
     */
    private void registerRxFeignClient(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 获取Provider 其实就是一个是扫描器,提供扫描的功能
        ClassPathScanningCandidateComponentProvider provider = this.getScanner();
        // 给扫描器设置资源加载器
        provider.setResourceLoader(resourceLoader);

        // 添加扫描组件需要过滤的类型,这里我们需要扫描所有添加注解 @RxFeignClient 的class
        AnnotationTypeFilter typeFilter = new AnnotationTypeFilter(RxFeignClient.class);
        // 扫描器
        provider.addIncludeFilter(typeFilter);

        String[] scanPackageArr = (String[]) enableRxFeignLocalityAttributes.get("scanPackages");
        // 如果没有配置则直接就不扫描了  方法直接返回即可
        if (null == scanPackageArr && scanPackageArr.length == 0) {
            logger.info("@RxFeignLocality 中的scanPackages值为空");
            return;
        }
        // 将需要扫描的路径数组 转化为 Set集合
        Set<String> scanPackages = new HashSet<>(CollectionUtils.arrayToList(scanPackageArr));

        Iterator<String> iterable = scanPackages.iterator();
        while (iterable.hasNext()) {
            String packages = iterable.next();
            // 获取指定包路径下面所有添加注解的bean
            Set<BeanDefinition> beanDefinitions = provider.findCandidateComponents(packages);
            Iterator<BeanDefinition> bi = beanDefinitions.iterator();
            while (bi.hasNext()) {
                BeanDefinition beanDefinition = bi.next();
                // 含有注解的bean
                if (beanDefinition instanceof AnnotatedBeanDefinition) {
                    AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition;
                    AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
                    // 该注解只能添加在接口上
                    Assert.isTrue(annotationMetadata.isInterface(), "@" + RxFeignClient.class.getName() + " 只能标记在接口上");

                    // 将扫描到的接口 根据代理工厂生成实例对象 并且将该实例对象注册到Spring的上下文中
                    this.registerRxFeignBean(registry, annotationMetadata, annotationMetadata.getAnnotationAttributes(RxFeignClient.class.getCanonicalName()));

                }
            }
        }
    }

    /**
     * 将接口根据代理工厂生成实例对象,并且将该实例对象注册到Spring的上下文中
     *
     * @param registry
     * @param annotationMetadata
     * @param annotationAttributes
     */
    private void registerRxFeignBean(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> annotationAttributes) {
        // 获取注解所在的类名
        String className = annotationMetadata.getClassName();
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(RxFeignClientFactoryBean.class);

        // 这里直接使用反射的方式 给通过代理工厂生成的实例对象进行赋值

        // 注意 这里annotationMetadata.getClassName() 是字符串类型的,而在代理工厂类中 resourceClass 是Class类型的
        // 按理说 赋值的话应该会报不合理参数的,但是这里运行没有问题,可能是Spring内部做了处理
        definition.addPropertyValue("resourceClass", annotationMetadata.getClassName());
        definition.addPropertyValue("instanceId", annotationAttributes.get("instanceId"));
        definition.addPropertyValue("url", annotationAttributes.get("directUrl"));
        definition.addPropertyValue("requestProtocolEnum", annotationAttributes.get("RequestProtocol"));
        definition.addPropertyValue("requestInterceptorClass", annotationAttributes.get("requestInterceptor"));
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        String clazzName = className.substring(className.lastIndexOf(".") + 1);
        String alias = this.lowerFirstCapse(clazzName);

        // 向Spring的上下文中注册bean组件
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

    }

    /**
     * 首字母变小写
     *
     * @param str
     * @return
     */
    public String lowerFirstCapse(String str) {
        char[] chars = str.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

    /**
     * 项目路径下的扫描器
     * <p>
     * ClassPathScanningCandidateComponentProvider 是Spring提供的工具,可以按照自定义的类型,查找classpath下符合要求的class文件
     *
     * @return
     */
    protected ClassPathScanningCandidateComponentProvider getScanner() {
        return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                boolean isCandidate = false;
                // 过滤掉不是注解的 bean
                if (beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation()) {
                    isCandidate = true;
                }
                return isCandidate;
            }
        };
    }

在上面的代码中我看到了代理工厂RxFeignClientFactoryBean,通过这个代理工厂来生成接口对应的实例对象

  1. 代理工厂
/**
 * Copyright © 2018 五月工作室. All rights reserved.
 *
 * @Package com.amos.baseframework.beanfactory
 * @ClassName RxFeignClientFactoryBean
 * @Description TODO
 * @Author Amos
 * @Modifier
 * @Date 2020/2/23 21:36
 * @Version 1.0
 **/
public class RxFeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private Class<?> resourceClass;

    private String instanceId;
    private String url;

    private RequestProtocolEnum requestProtocolEnum;

    private Class<? extends RequestInterceptor> requestInterceptorClass;

    public static final String HTTPS = "https://";
    public static final String HTTP = "http://";

    @Override
    public Object getObject() throws Exception {
        return target();
    }

    /**
     * 获取目标对象
     *
     * @param <T>
     * @return
     */
    public <T> T target() {
        Client client = (Client) getFeignContext().getInstances(instanceId, Client.class);
        T t = (T) Feign.builder().decoder(new GsonDecoder())
                .encoder(new GsonEncoder())
                .client(client)
                .requestInterceptor(requestInterceptorNewInstance())
                .target(resourceClass, parseProtocol());
        return t;
    }

    public RequestInterceptor requestInterceptorNewInstance() {
        try {
            return requestInterceptorClass.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private String parseProtocol() {
        if (!StringUtils.isEmpty(url)) {
            return url;
        }
        switch (requestProtocolEnum) {
            case HTTP:
                return HTTP + instanceId;
            case HTTPS:
                return HTTPS + instanceId;
            default:
                return null;
        }

    }

    private FeignContext getFeignContext() {
        return this.applicationContext.getBean(FeignContext.class);
    }

    @Override
    public Class<?> getObjectType() {
        return resourceClass;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(resourceClass, "标记类不能为空");
        if (StringUtils.isEmpty(instanceId) && StringUtils.isEmpty(url)) {
            throw new IllegalArgumentException("实例名和url不能同时为空");
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public Class<?> getResourceClass() {
        return resourceClass;
    }

    public void setResourceClass(Class<?> resourceClass) {
        this.resourceClass = resourceClass;
    }

    public String getInstanceId() {
        return instanceId;
    }

    public void setInstanceId(String instanceId) {
        this.instanceId = instanceId;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public RequestProtocolEnum getRequestProtocolEnum() {
        return requestProtocolEnum;
    }

    public void setRequestProtocolEnum(RequestProtocolEnum requestProtocolEnum) {
        this.requestProtocolEnum = requestProtocolEnum;
    }

    public Class<? extends RequestInterceptor> getRequestInterceptorClass() {
        return requestInterceptorClass;
    }

    public void setRequestInterceptorClass(Class<? extends RequestInterceptor> requestInterceptorClass) {
        this.requestInterceptorClass = requestInterceptorClass;
    }
}

至此,基本的功能就已经实现了,我们可以通过actuator来监控bean组件是否注册到Spring上下文中

  1. 引入 actuator
    pom文件中引入
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后 application.yml文件中添加

management:
  endpoint:
    web:
      base-path: /actuator
  endpoints:
    web:
      exposure:
        include: "*"

最后启动项目即可

完整的代码可以参考: spring-cloud-function-sample

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容