3.从0到1实现一个自定义Bean注册器(应用篇)
Categories:
2 分钟阅读
我们知道在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注册器的全过程,包括面向新手可以怎样通过"致敬"既有的代码逻辑,来"巧妙"的实现我们的目标
II. 其他
0. 项目
- 181013-SpringBoot基础篇Bean之动态注册
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 项目:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/006-importbean
反馈
这篇文章对您有帮助么?
Glad to hear it! Please tell us how we can improve/告诉作者如何改进.
Sorry to hear that. Please tell us how we can improve/告诉作者如何改进.