掀起Spring扩展点的盖头来ImportBeanDefinitionRegistrar分析与实战
共 8168字,需浏览 17分钟
·
2022-02-20 09:42
本文开始,我们将系统地对Spring框架的扩展点进行学习,通过案例分析与图例结合,step by step地对Spring看似神秘的扩展点的机理与应用进行研究。
首先通过一张图对Spring框架各种扩展点的调用顺序(Bean生命周期)进行先入为主的概览。
可以看到图片的一开始便是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");
- 自定义一个ClassPathBeanDefinitionScanner实例,并将bean定义注册器BeanDefinitionRegistry引用传递进去,这是一种委托机制;
- 设置ResourceLoader,ResourceLoader的引用通过ResourceLoaderAware获得,并指向当前类的成员变量;
- 调用registerFilters方法(该方法为自定义方法,本质是调用了addIncludeFilter),让Spring去扫描带有特定标志的类进行管理与加载;(具体的代码稍后进行分析);
- 调用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等属性,具体的属性可以看下图:
最后通过 「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 {
}
- MyConf是自定义的配置类,标注了 @Configuration 注解。
- 通过@Import将实现了ImportBeanDefinitionRegistrar接口的MyBeanDefinationRegistry包含进来;
- 添加扫描包,以方便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());
}
}
- 首先我们声明并初始化一个AnnotationConfigApplicationContext容器;
- 接着从容器中通过BeanName获取通过GenericBeanDefinition定义的TestBean实例,打印其Class类型;
- 观察日志输出,期望能够看到框架代码FrameWorkConfigA、FrameWorkConfigB、FrameWorkConfigC的构造方法日志打印,并看到TestgBean的Class类型打印。
运行测试类,观察控制台日志输出:
自定义框架组件A-初始化逻辑
自定义框架组件B-初始化逻辑
自定义框架组件C-初始化逻辑
class com.spring.TestBean
可以看到符合预期,这表明,通过ImportBeanDefinitionRegistrar自定义手动bean注入符合预期。
总结
本文我们全篇对ImportBeanDefinitionRegistrar在Spring容器装载bean的过程进行了综述,并通过一个模拟框架开发的案例,对如何通过ImportBeanDefinitionRegistrar实现bean的自定义注入进行了代码级别的讲解和分析。
如果在实际的开发案例中需要实现自定义的bean注入,减少调用方整合的复杂度,那么我们完全可以通过本文讲解的方式,利用ImportBeanDefinitionRegistrar扩展点实现。
下期预告:
下期我们将分析讲解BeanPostProcessor扩展点在Spring框架中的作用,并讲解BeanPostProcessor在实战开发中的使用。