SpringBoot 应用篇之从 0 到 1 实现一个自定义 Bean 注册器
我们知道在 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
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(" ...
随机推荐
- C#给企业微信中的成员发送消息
先去企业微信门户网站获得密钥和应用ID 创建一个静态工具类 using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using ...
- Javascript屏蔽Backspace回退页面
允许对输入框密码框等控件删除字符,但是不允许页面进行回退 <html lang="en" xmlns="http://www.w3.org/1999/xhtml&q ...
- log4net 写数据到sql数据库
最近需要把用户的一些行为添加到数据库中,所以想到了用log4net ,如果有别的好的方案,大家可以给我指正. 先看一下配置文件 我这个是控制台文件 app.config <layout type ...
- 将MySQL升级到8.0.x后的遇到到错误及解决
一,安装的时遇到的坑 我下的是Mysql 8.0.13 地址:https://dev.mysql.com/downloads/mysql/ 下的是解压版(个人能不用安装就不想用安装版的强迫症(/▽\) ...
- Mysql-修改用户连接数据库IP地址和用户名
将用户连接数据库(5.7.14-7)的IP地址从 10.10.5.16 修改为 10.11.4.197 Mysql> rename user 'username'@'10.10.5.16' ...
- SQLServer2017自动备份数据库
本文截图来源: 地址一:https://blog.csdn.net/u010944388/article/details/86674078 地址二:https://www.cnblogs.com/ja ...
- youtube-dll工具使用,很好用!!
最近喜欢上youtube-dll这个插件,下载东西真的很好用,墙裂推荐,github地址如下 https://github.com/ytdl-org/youtube-dl 安装 1.Linux 1.1 ...
- Redis_数据类型
Redis支持的键值数据类型如下: 字符串类型 散列类型 列表类型 集合类型 有序集合类型 一.字符串类型 字符串类型是Redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据.一个字 ...
- PHP redis 常用操作
//在列表头部插入一个值one,当列表不存在时自动创建一个列表,key1为列表名 $redis->lpush("key1", "one"); //在列表尾 ...
- Congigure SSL in StoreFront
StoreFront SSL Requirements StoreFront website must be up and running in http Joined to the domain C ...