我们知道在spring中可以通过 @Component , @Service , @Repository 装饰一个类,通过自动扫描注册为bean;也可以通过在配置类中,借助 @Bean 来注册bean;那么除了这几种方式之外,还有什么其他的方式来声明一个类为bean么?
我们是否可以自定义一个注解,然后将这个注解装饰的类主动声明为bean注册到spring容器,从而实现类似 @Component 的效果呢?
接下来本文将介绍,如果通过 ImportBeanDefinitionRegistrar 结合自定义注解来实现bean注册,主要用到的知识点如下:
ImportBeanDefinitionRegistrar @Import ClassPathBeanDefinitionScanner
虽然我们的目标比较清晰,但是突然让我们来实现这么个东西,还真有点手足无措,应该从哪里下手呢?
如果看过我之前关于SpringBoot结合java web三剑客(Filter, Servlet, Listener)的相关博文的同学,应该会记得一个重要的知识点:
@WebListener , @WebServlet , @WebFilter 这三个注解属于Servlet3+ 规范 @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版本的包内)
致敬对象找到了,接下来开始正式实现前的一些准备工作,首先我们把目标具体事例化
@Meta 的类,会注册到Spring容器,作为一个普通的Bean对象 然后就是测试测试验证是否生效的关键case了
@Meta 类是否可以正常被spring识别 @Meta 类是否可以被其他 bean or @Meta 类通过 @Autowired 引入 @Meta 类是否可以正常依赖普通的 bean , @Meta 类 类似 @Component 注解的功能,我们弄简单一点即可
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Meta {
}
这个注解和 @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
接下来进入我们的核心类,它主要继承自 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) {
// ... 参考前面,这省略
}
}
上面实现现在看来非常简单了(两个注解定义,一个核心类,也复杂不到哪里去了);接下来就需要验证这个是否生效了
如果被spring识别为bean,则构造方法会被调用
@Meta
public class DemoBean1 {
public DemoBean1() {
System.out.println("DemoBean1 register!");
}
}
定义一个普通的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);
}
}
@Component
public class ABean {
public ABean(DemoBean1 demoBean1) {
System.out.println("a bean : " + demoBean1);
}
}
启动类,注意需要添加上我们自定义的 @MetaComponentScan 注解
@SpringBootApplication
@MetaComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
执行输出结果
本文主要介绍了如何通过 ImportBeanDefinitionRegistrar 来实现自定义的bean注册器的全过程,包括面向新手可以怎样通过”致敬”既有的代码逻辑,来”巧妙”的实现我们的目标
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
打赏 如果觉得我的文章对您有帮助,请随意打赏。