掀起Spring扩展点的盖头来ImportBeanDefinitionRegistrar分析与实战

分布式朝闻道

共 8168字,需浏览 17分钟

 · 2022-02-20

本文开始,我们将系统地对Spring框架的扩展点进行学习,通过案例分析与图例结合,step by step地对Spring看似神秘的扩展点的机理与应用进行研究。

首先通过一张图对Spring框架各种扩展点的调用顺序(Bean生命周期)进行先入为主的概览。

e13e4845e38f83ce6cb6f4aad94e79f7.webp

可以看到图片的一开始便是Spring对Bean定义(BeanDefinition)进行解析和注册,Bean的注册主要就是通过ImportBeanDefinitionRegistrar实现的。

Spring框架主要就是通过ImportBeanDefinitionRegistrar实现对bean的动态注册。源码如下:

    public interface ImportBeanDefinitionRegistrar {
        // 通过解析给定的注解元信息,向Spring容器中注册Bean定义
        default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
                BeanNameGenerator importBeanNameGenerator) {

            registerBeanDefinitions(importingClassMetadata, registry);
        }
        default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        }
    }

实现ImportBeanDefinitionRegistrar接口的类的都会被ConfigurationClassPostProcessor处理,ConfigurationClassPostProcessor实现了BeanFactoryPostProcessor接口,所以ImportBeanDefinitionRegistrar中动态注册的bean是优先于依赖其的bean初始化的,同时它也可以被aop、validator等机制处理。

编码实现手动Bean注入

日常的业务开发中,我们很少会通过ImportBeanDefinitionRegistrar来对bean进行注入。

而是通过xml文件声明或者注解如:@Component、@Service、@Bean等方式对bean进行注入和声明。

那么什么场景下才需要通过ImportBeanDefinitionRegistrar注册并注入bean呢?

在中间件开发场景下,就会用到手动bean注入。原因在于中间件/框架的开发者并不知道调用方/框架使用者是通过什么方式对bean进行注入的。

当然我们也可以让使用者们显式的对框架中的bean进行定义,但是这样就显著的增加了工作量和出错率,因此对于框架开发而言,常常通过ImportBeanDefinitionRegistrar实现bean的隐式注入和声明,减少调用方整合框架的复杂度。

我们通过一个模拟场景来介绍一下如何通过编码实现bean的手动隐式注入。、

1、定义ImportBeanDefinitionRegistrar实现类

首先定义一个实现ImportBeanDefinitionRegistrar接口的类,并编写bean注册逻辑。

    public class MyBeanDefinationRegistry implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanFactoryAware {

        private BeanFactory beanFactory;
        private ResourceLoader resourceLoader;

        @Override
        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
            this.beanFactory = beanFactory;
        }

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

        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

            MyClassPathBeanDefinitionScanner scanner = new MyClassPathBeanDefinitionScanner(registry, false);
            scanner.setResourceLoader(resourceLoader);
            scanner.registerFilters();
            scanner.doScan("com.spring.framework");

            GenericBeanDefinition genericBeanDefinition = new GenericBeanDefinition();
            genericBeanDefinition.setBeanClass(TestBean.class);
            genericBeanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
            registry.registerBeanDefinition("testBean", genericBeanDefinition);
        }
    }

重点关注 「BeanDefinitionRegistry」 方法,这里提供了两种bean扫描方式。

方式1:基于包路径的扫描

        MyClassPathBeanDefinitionScanner scanner = 
            new MyClassPathBeanDefinitionScanner(registry, false);
        scanner.setResourceLoader(resourceLoader);
        scanner.registerFilters();
        scanner.doScan("com.spring.framework");

  1. 自定义一个ClassPathBeanDefinitionScanner实例,并将bean定义注册器BeanDefinitionRegistry引用传递进去,这是一种委托机制;
  2. 设置ResourceLoader,ResourceLoader的引用通过ResourceLoaderAware获得,并指向当前类的成员变量;
  3. 调用registerFilters方法(该方法为自定义方法,本质是调用了addIncludeFilter),让Spring去扫描带有特定标志的类进行管理与加载;(具体的代码稍后进行分析);
  4. 调用doScan,传递需要扫描的包路径,这个路径就是框架开发者自定义的包路径,该路径下存放的就是框架本身的bean,「这个路径是完全由框架的开发者决定的,而且我们一般可以认为,该路径一旦定义就不会更改。并且该路径也不适合暴露给框架的调用者」

方式2:直接注册BeanDefinition

        GenericBeanDefinition genericBeanDefinition = new GenericBeanDefinition();
        genericBeanDefinition.setBeanClass(TestBean.class);
        genericBeanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
        registry.registerBeanDefinition("testBean", genericBeanDefinition);

方式2比较简单,但是相对的也比方式1繁琐。

TestBean 是模拟的一个框架的内部bean组件,实际开发中可以根据需要填充必要的属性和方法,这里只是作为演示。

    public class TestBean {
    }

通过声明GenericBeanDefinition,并为其添加需要注册的Bean的class,scope(单例or多例),beanName等属性,具体的属性可以看下图:

7be51fb94995077ed5470c240adec9a3.webp

最后通过 「registry.registerBeanDefinition」 将设置好属性的GenericBeanDefinition注册,并设置beanName;

对比方式1方式2

通过代码我们可以直观的看到,方式1比方式2更加方便,可以实现批量bean的扫描与注入;

而方式2则需要逐个bean进行注入,但是相对的,方式2也更加灵活,能够实现 「细粒度」 的beanDefinition声明和定义。

2、定义ClassPathBeanDefinitionScanner实现类

通过定义ClassPathBeanDefinitionScanner的实现类,告诉Spring需要对哪些类进行管理(addIncludeFilter)以及不需要关注哪些类(addExcludeFilter)。

    public class MyClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {

        public MyClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
            super(registry, useDefaultFilters);
        }

        /**
        * 比较重要的一个点就是registerFilters()这个方法,
        * 在里面我们可以定义让Spring去扫描带有特定标志的类选择进行管理或者是选择不管理;
        * 通过addIncludeFilter()方法和通过addExcludeFilter()方法;
        */
        protected void registerFilters() {
            /**
            *  TODO addIncludeFilter  满足任意includeFilters会被加载
            */
            addIncludeFilter(new AnnotationTypeFilter(SnoWalkerAutoInject.class));
        }

        @Override
        protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
            return super.doScan(basePackages);
        }
    }

可以看到,扫描器ClassPathBeanDefinitionScanner扫描类路径上的需要被管理的类,通过BeanFactory创建Bean给ApplicationComtext(Spring容器)管理;

registerFilters分析

registerFilters是自定义的方法,核心的逻辑就是通过addIncludeFilter添加了一个包扫描的规则:

这里是通过注解类型的Filter通知Spring容器对添加了SnoWalkerAutoInject自定义注解的bean进行管理。

我们可以看到,自定义的MyClassPathBeanDefinitionScanner重写了父类的doScan方法,本质上调用了父类的doScan,以实现对指定路径下的bean进行扫描。

最终实际上是在ApplicationContext中调用了doScan,实现了对bean定义的扫描及实例化,我们可以看一下源码实现:

 /**
  * Create a new AnnotationConfigApplicationContext, scanning for components
  * in the given packages, registering bean definitions for those components,
  * and automatically refreshing the context.
  * @param basePackages the packages to scan for component classes
  */
 public AnnotationConfigApplicationContext(String... basePackages) {
  this();
  scan(basePackages);
  refresh();
 }

AnnotationConfigApplicationContext构造方法中,对package进行了扫描,并调用refresh方法对bean进行初始化和实例化。

3、自定义注解

自定义注解,并添加到需要装载到Spring容器中的框架类上:

    @Documented
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
    public @interface SnoWalkerAutoInject {
    }

定义几个模拟的框架类,用以模拟框架的逻辑。实际的开发中,我们可以按照需求的实际需要,开发框架代码,并标记自定义的注解。

    @SnoWalkerAutoInject
    public class FrameWorkConfigA {

        public FrameWorkConfigA() {
            System.out.println("自定义框架组件A-初始化逻辑");
        }
    }

    @SnoWalkerAutoInject
    public class FrameWorkConfigB {

        public FrameWorkConfigB() {
            System.out.println("自定义框架组件B-初始化逻辑");
        }
    }

    @SnoWalkerAutoInject
    public class FrameWorkConfigC {

        public FrameWorkConfigC() {
            System.out.println("自定义框架组件C-初始化逻辑");
        }
    }

4、配置ImportBeanDefinitionRegistrar实现类

如何使用自定义的ImportBeanDefinitionRegistrar实现类对bean进行装载呢?

最终我们还是需要定义一个配置类,通过@Import注解配置ImportBeanDefinitionRegistrar实现。

    @Configuration
    @Import(MyBeanDefinationRegistry.class)
    @ComponentScan("com.spring.framework")
    public class MyConf {
    }
  1. MyConf是自定义的配置类,标注了 @Configuration 注解。
  2. 通过@Import将实现了ImportBeanDefinitionRegistrar接口的MyBeanDefinationRegistry包含进来;
  3. 添加扫描包,以方便spring对该包下的类进行扫描并进行选择性的装载;

4、测试

编写测试类:

    public class App {

        public static void main(String[] args) {
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.spring");

            final TestBean testBean = (TestBean) applicationContext.getBean("testBean");
            System.out.println(testBean.getClass());
        }
    }

  1. 首先我们声明并初始化一个AnnotationConfigApplicationContext容器;
  2. 接着从容器中通过BeanName获取通过GenericBeanDefinition定义的TestBean实例,打印其Class类型;
  3. 观察日志输出,期望能够看到框架代码FrameWorkConfigA、FrameWorkConfigB、FrameWorkConfigC的构造方法日志打印,并看到TestgBean的Class类型打印。

运行测试类,观察控制台日志输出:

    自定义框架组件A-初始化逻辑
    自定义框架组件B-初始化逻辑
    自定义框架组件C-初始化逻辑

    class com.spring.TestBean

可以看到符合预期,这表明,通过ImportBeanDefinitionRegistrar自定义手动bean注入符合预期。

总结

本文我们全篇对ImportBeanDefinitionRegistrar在Spring容器装载bean的过程进行了综述,并通过一个模拟框架开发的案例,对如何通过ImportBeanDefinitionRegistrar实现bean的自定义注入进行了代码级别的讲解和分析。

如果在实际的开发案例中需要实现自定义的bean注入,减少调用方整合的复杂度,那么我们完全可以通过本文讲解的方式,利用ImportBeanDefinitionRegistrar扩展点实现。

下期预告:

下期我们将分析讲解BeanPostProcessor扩展点在Spring框架中的作用,并讲解BeanPostProcessor在实战开发中的使用。


浏览 91
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报