我们知道在 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
就显得很重要了,显然是它充当了桥梁(在搞事情了),然后我们致敬(抄袭)的对象就有了
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(ServletComponentScanRegistrar.class) public @interface ServletComponentScan { @AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {}; Class<?>[] 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
注解的功能,我们弄简单一点即可
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Meta { } 复制代码
b. @MetaComponentScan 注解
这个注解和@ServletComponentScan
作用差不多,主要是用来加载
ImportBeanDefinitionRegistrar
实现类,后者则是定义 bean 的核心类
实现如下
@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 的值,看一下注解的basePackages
和basePackageClasses
我们知道@ComponentScan
的作用主要是用来指定哪些包路径下的类开启注解扫描;
MetaComponentScan
的几个成员主要作用和上面相同;
- 当指定了值的时候,主要加载这些包路径下,包含
@Meta
注解的类; - 如果全是默认值(即为空),则扫描这个注解所在类对应的包路径下所有包含
@Meta
的类
c. MetaAutoConfigureRegistrar
接下来进入我们的核心类,它主要继承自ImportBeanDefinitionRegistrar
,bean 定义注册器,其核心方法为
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
注解的类
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
的属性来获取
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 的实现就有了
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,则构造方法会被调用
@Meta public class DemoBean1 { public DemoBean1() { System.out.println("DemoBean1 register!"); } } 复制代码
2. case1 Meat 注解类,依赖 Bean
定义一个普通的 bean 对象
@Component public class NormalBean { public NormalBean() { System.out.println("normal bean"); } } 复制代码
然后定义一个 Meta 装饰的类,依赖 NormalBean
@Meta public class DependBean { public DependBean(NormalBean normalBean) { System.out.println("depend bean! " + normalBean); } } 复制代码
3. case2 bean 依赖 Meta 注解类
@Component public class ABean { public ABean(DemoBean1 demoBean1) { System.out.println("a bean : " + demoBean1); } } 复制代码
4. 测试
启动类,注意需要添加上我们自定义的@MetaComponentScan
注解
@SpringBootApplication @MetaComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } } 复制代码
执行输出结果
5. 小结
本文主要介绍了如何通过ImportBeanDefinitionRegistrar
来实现自定义的 bean 注册器的全过程,包括面向新手可以怎样通过"致敬"既有的代码逻辑,来"巧妙"的实现我们的目标