SpringBoot 应用篇之从 0 到 1 实现一个自定义 Bean 注册器

我们知道在 spring 中可以通过@Component,@Service, @Repository 装饰一个类,通过自动扫描注册为 bean;也可以通过在配置类中,借助@Bean来注册 bean;那么除了这几种方式之外,还有什么其他的方式来声明一个类为 bean 么?
我们是否可以自定义一个注解,然后将这个注解装饰的类主动声明为 bean 注册到 spring 容器,从而实现类似@Component的效果呢?
接下来本文将介绍,如果通过ImportBeanDefinitionRegistrar结合自定义注解来实现 bean 注册,主要用到的知识点如下:
ImportBeanDefinitionRegistrarbean 注册的核心类@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类是否可以被其他beanor@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
1. 一灰灰 Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
- 一灰灰 Blog 个人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top

SpringBoot 应用篇之从 0 到 1 实现一个自定义 Bean 注册器的更多相关文章
- 【SpringBoot 基础系列】实现一个自定义配置加载器(应用篇)
[SpringBoot 基础系列]实现一个自定义配置加载器(应用篇) Spring 中提供了@Value注解,用来绑定配置,可以实现从配置文件中,读取对应的配置并赋值给成员变量:某些时候,我们的配置可 ...
- (二)SpringBoot基础篇- 静态资源的访问及Thymeleaf模板引擎的使用
一.描述 在应用系统开发的过程中,不可避免的需要使用静态资源(浏览器看的懂,他可以有变量,例:HTML页面,css样式文件,文本,属性文件,图片等): 并且SpringBoot内置了Thymeleaf ...
- SpringBoot系列教程web篇之如何自定义参数解析器
title: 190831-SpringBoot系列教程web篇之如何自定义参数解析器 banner: /spring-blog/imgs/190831/logo.jpg tags: 请求参数 cat ...
- SpringBoot分布式篇Ⅷ --- 整合SpringCloud
SpringCloud是一个分布式的整体解决方案.Spring Cloud为开发者提供了在分布式系统(配置管理,服务发现,熔断,路由,微代理,控制总线,一次性token,全局锁,leader选举.分布 ...
- 第一篇:SpringBoot2.0简单介绍
距离Spring Boot1.0发布已经4年了,今年3月份SpringBoot2.0正式发布.让我们一起来了解一下它. Spring Boot主要依赖于Spring,整合了很多框架的使用方式,帮助开发 ...
- 一篇对OAuth2.0开发实例的介绍
今天看到了博友对SSO的文章,SSO单点登录的讲解突然想写一篇关于OAuth2.0用户授权的介绍. 应用场景:在APP或者网页接入一些第三方应用时,时长会需要用户登录另一个合作平台,比如QQ,微博,微 ...
- SpringBoot应用篇(一):自定义starter
一.码前必备知识 1.SpringBoot starter机制 SpringBoot中的starter是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进starter,应用者只需要在mave ...
- 视频作品《springboot基础篇》上线了
1.场景描述 第一个视频作品出炉了,<springboot基础篇>上线了,有需要的朋友可以直接点击链接观看.(如需购买,请通过本文链接购买) 2. 课程内容 课程地址:https://ed ...
- SpringBoot Web篇(二)
摘要 继上一篇 SpringBoot Web篇(一) 文件上传 当我们服务器需要接收用户上传的文件时,就需要使用MultipartFile作为参数接收文件.如下: @PostMapping(" ...
随机推荐
- Newtonsoft.Json使用技巧
本篇将为大家介绍Newtonsoft.Json的一些高级用法,可以修改很少的代码解决上述问题. 阅读目录 Newtonsoft.Json介绍 基本用法 高级用法 总结 回到顶部 Newtonsoft. ...
- Winform c# 多线程处理实例
我们在用C# 开发程序时,经常会使用的多线程,实现多任务的处理.一般常用的方法是新建多个线程,进行处理. 今天我分享一个采用线程池的方式来实现的实例.对有需要的朋友做个借鉴. 实例: Winform ...
- Python关于多继承
大部分面向对象的编程语言(除了C++)都只支持单继承,而不支持多继承,为什么呢?因为多继承不仅增加编程复杂度,而且容易导致莫名其妙的错误. Python虽然语法上支持多继承,但是却不推荐使用多继承,而 ...
- python--进程初识详解
进程:通俗理解一个运行的程序或者软件,进程是操作系统资源分配的基本单位 1.1.导入进程模块 import multiprocessing 1.2.Process进程类的语法结构如下: Process ...
- 我为啥不想用Python
Python这门语言从一开始就是一个玩具语言,它不是给正经程序员用的东西. 运行效率低 Python运行效率很低,这就导致Python中很多库底层实际上是C++.很多时候,自己千方百计优化的结果就不如 ...
- JavaScript之二十三种设计模式
23种JavaScript设计模式 原文链接:https://boostlog.io/@sonuton/23-javascript-design-patterns-5adb006847018500 ...
- Android 一个TextView中设置多种不同大小的字体,设置超链接
以前项目中要是遇到这样的UI设计,都是傻不拉唧的分为三个TextView来实现,今天在微信中无意中看了一篇公众号文章,发现原来只要一个TextView就可以搞定啦,人生最悲哀的事情莫过于工作了这么久啦 ...
- mysql主从复制几个重要的参数
1. relay_log_recovery = 1 当slave从库宕机后,假如relay-log损坏了,导致一部分中继日志没有处理,则自动放弃所有未执行的relay-log,并且重新从master上 ...
- 移动端好用的下拉加载上拉刷新插件 dropload插件
入了很多下拉加载上拉刷新的插件,但是感觉都不好用,知道最近遇到这款dropload的插件,瞬间打开新世界的大门啊,无卡顿简单易用可配置 <!doctype html> <html&g ...
- 201871010110-李华《面向对象程序设计(java)》第6-7周学习总结
项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p ...