【基础系列】从0到1实现一个自定义Bean注册器(应用篇)

文章目录
  1. I. 自定义bean注册器
    1. 0. 寻找”致敬”对象
    2. 1. 准备篇
    3. 2. 开始实现
      1. a. @Meta注解定义
      2. b. @MetaComponentScan注解
      3. c. MetaAutoConfigureRegistrar
  2. II. 测试与小结
    1. 1. case0 Meta注解类
    2. 2. case1 Meat注解类,依赖Bean
    3. 3. case2 bean 依赖 Meta注解类
    4. 4. 测试
    5. 5. 小结
  3. II. 其他
    1. 0. 项目
    2. 1. 一灰灰Blog

我们知道在spring中可以通过@Component@Service, @Repository 装饰一个类,通过自动扫描注册为bean;也可以通过在配置类中,借助@Bean来注册bean;那么除了这几种方式之外,还有什么其他的方式来声明一个类为bean么?

我们是否可以自定义一个注解,然后将这个注解装饰的类主动声明为bean注册到spring容器,从而实现类似@Component的效果呢?

接下来本文将介绍,如果通过ImportBeanDefinitionRegistrar结合自定义注解来实现bean注册,主要用到的知识点如下:

  • ImportBeanDefinitionRegistrar bean注册的核心类
  • @Import 导入配置
  • ClassPathBeanDefinitionScanner

I. 自定义bean注册器

虽然我们的目标比较清晰,但是突然让我们来实现这么个东西,还真有点手足无措,应该从哪里下手呢?

0. 寻找”致敬”对象

如果看过我之前关于SpringBoot结合java web三剑客(Filter, Servlet, Listener)的相关博文的同学,应该会记得一个重要的知识点:

  • @WebListener, @WebServlet, @WebFilter 这三个注解属于Servlet3+ 规范
  • 在SpringBoot项目中,如需要上面注解生效,需要在启动类上添加注解 @ServletComponentScan

看到上面这个是不是会有一丝灵感被激发(在当时写上面博文的时候,特意的看了一下后面注解的逻辑),嘿嘿,感觉找到了一条通往成功之旅的道路

既然@WebXxx注解不是原生的Spring支持注解,所以让他生效的注解 @ServletComponentScan就显得很重要了,显然是它充当了桥梁(在搞事情了),然后我们致敬(抄袭)的对象就有了

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ServletComponentScanRegistrar.class)
public @interface ServletComponentScan {
true@AliasFor("basePackages")
trueString[] value() default {};

true@AliasFor("value")
trueString[] basePackages() default {};

trueClass<?>[] basePackageClasses() default {};
}

注解定义比较简单,最终生效的不用说,肯定是ServletComponentScanRegistrar了,再接着瞅一眼

(不同的SpringBoot版本,上面的实现类可能会有一定的差异,上面的源码截取自spring-boot 2.1.2.RELEASE版本的包内)

1. 准备篇

致敬对象找到了,接下来开始正式实现前的一些准备工作,首先我们把目标具体事例化

  • 所有类上拥有自定义注解@Meta的类,会注册到Spring容器,作为一个普通的Bean对象

然后就是测试测试验证是否生效的关键case了

  • 无外部依赖的@Meta类是否可以正常被spring识别
  • @Meta类是否可以被其他bean or @Meta类通过@Autowired引入
  • @Meta类是否可以正常依赖普通的bean@Meta

2. 开始实现

a. @Meta注解定义

类似@Component注解的功能,我们弄简单一点即可

1
2
3
4
5
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Meta {
}

b. @MetaComponentScan注解

这个注解和@ServletComponentScan作用差不多,主要是用来加载ImportBeanDefinitionRegistrar实现类,后者则是定义bean的核心类

实现如下

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({MetaAutoConfigureRegistrar.class})
public @interface MetaComponentScan {
@AliasFor("basePackages") String[] value() default {};

@AliasFor("value") String[] basePackages() default {};

Class<?>[] basePackageClasses() default {};
}

先暂时无视Import的值,看一下注解的basePackagesbasePackageClasses

我们知道@ComponentScan的作用主要是用来指定哪些包路径下的类开启注解扫描;MetaComponentScan的几个成员主要作用和上面相同;

  • 当指定了值的时候,主要加载这些包路径下,包含@Meta注解的类;
  • 如果全是默认值(即为空),则扫描这个注解所在类对应的包路径下所有包含@Meta的类

c. MetaAutoConfigureRegistrar

接下来进入我们的核心类,它主要继承自ImportBeanDefinitionRegistrar,bean定义注册器,其核心方法为

1
2
void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

两个参数,第一个顾名思义,注解元数据,多半是用来获取注解的属性;第二个bean定义注册器,我们在学习bean的动态注册时(详情参考: 181013-SpringBoot基础篇Bean之动态注册) 知道可以用BeanDefinitionRegistry注册bean,因为我们这里的目标是注册所有带 @Meta 注解的类

自然而然的想法

  • 扫描所有的类,判断是否有@Meta注解,有则通过registry手动注册

然而在实际动手之前,再稍微停一停;扫描所有类判断是否有某个注解,这个操作在spring中应该属于比较常见的case(why?),应该是有一些可供我们使用的辅助类

继续撸”致敬”的对象,ServletComponentScanRegistrar类主要是注册servletComponentRegisteringPostProcessor,所以我们再转移目标到后者的详情(下图来自org.springframework.boot.web.servlet.ServletComponentRegisteringPostProcessor#createComponentProvider)

到这里我们的思路又打开了,可以借助ClassPathScanningCandidateComponentProvider来实现bean注册


上面的一段内容属于前戏,放在脑海里迅速的过一过就好了,接下来进入正文;

首先是创建一个ClassPathScanningCandidateComponentProvider的子类,注册一个AnnotationTypeFilter,确保过滤获取所有@Meta注解的类

1
2
3
4
5
6
7
8
9
10
11
private static class MetaBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
public MetaBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
Environment environment, ResourceLoader resourceLoader) {
super(registry, useDefaultFilters, environment, resourceLoader);
registerFilters();
}

protected void registerFilters() {
addIncludeFilter(new AnnotationTypeFilter(Meta.class));
}
}

然后就是获取扫描的包路径了,通过解析前面定义的MetaComponentScan的属性来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
AnnotationAttributes attributes =
AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(MetaComponentScan.class.getName()));
String[] basePackages = attributes.getStringArray("basePackages");
Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");

Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
for (Class clz : basePackageClasses) {
packagesToScan.add(ClassUtils.getPackageName(clz));
}

if (packagesToScan.isEmpty()) {
packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
}

return packagesToScan;
}

所以完整的MetaAutoConfigureRegistrar的实现就有了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MetaAutoConfigureRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

private ResourceLoader resourceLoader;

private Environment environment;

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

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

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
MetaBeanDefinitionScanner scanner =
new MetaBeanDefinitionScanner(registry, this.environment, this.resourceLoader);
Set<String> packagesToScan = this.getPackagesToScan(importingClassMetadata);
scanner.scan(packagesToScan.toArray(new String[]{}));
}

private static class MetaBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
// ... 参考前面,这里省略
}

private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
// ... 参考前面,这省略
}
}

II. 测试与小结

上面实现现在看来非常简单了(两个注解定义,一个核心类,也复杂不到哪里去了);接下来就需要验证这个是否生效了

1. case0 Meta注解类

如果被spring识别为bean,则构造方法会被调用

1
2
3
4
5
6
@Meta
public class DemoBean1 {
public DemoBean1() {
System.out.println("DemoBean1 register!");
}
}

2. case1 Meat注解类,依赖Bean

定义一个普通的bean对象

1
2
3
4
5
6
@Component
public class NormalBean {
public NormalBean() {
System.out.println("normal bean");
}
}

然后定义一个Meta装饰的类,依赖 NormalBean

1
2
3
4
5
6
@Meta
public class DependBean {
public DependBean(NormalBean normalBean) {
System.out.println("depend bean! " + normalBean);
}
}

3. case2 bean 依赖 Meta注解类

1
2
3
4
5
6
@Component
public class ABean {
public ABean(DemoBean1 demoBean1) {
System.out.println("a bean : " + demoBean1);
}
}

4. 测试

启动类,注意需要添加上我们自定义的@MetaComponentScan注解

1
2
3
4
5
6
7
@SpringBootApplication
@MetaComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

执行输出结果

5. 小结

本文主要介绍了如何通过ImportBeanDefinitionRegistrar来实现自定义的bean注册器的全过程,包括面向新手可以怎样通过”致敬”既有的代码逻辑,来”巧妙”的实现我们的目标

II. 其他

0. 项目

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到