191213-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 的值,看一下注解的basePackagesbasePackageClasses

我们知道@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. 项目

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

SpringBoot 应用篇之从 0 到 1 实现一个自定义 Bean 注册器的更多相关文章

  1. 【SpringBoot 基础系列】实现一个自定义配置加载器(应用篇)

    [SpringBoot 基础系列]实现一个自定义配置加载器(应用篇) Spring 中提供了@Value注解,用来绑定配置,可以实现从配置文件中,读取对应的配置并赋值给成员变量:某些时候,我们的配置可 ...

  2. (二)SpringBoot基础篇- 静态资源的访问及Thymeleaf模板引擎的使用

    一.描述 在应用系统开发的过程中,不可避免的需要使用静态资源(浏览器看的懂,他可以有变量,例:HTML页面,css样式文件,文本,属性文件,图片等): 并且SpringBoot内置了Thymeleaf ...

  3. SpringBoot系列教程web篇之如何自定义参数解析器

    title: 190831-SpringBoot系列教程web篇之如何自定义参数解析器 banner: /spring-blog/imgs/190831/logo.jpg tags: 请求参数 cat ...

  4. SpringBoot分布式篇Ⅷ --- 整合SpringCloud

    SpringCloud是一个分布式的整体解决方案.Spring Cloud为开发者提供了在分布式系统(配置管理,服务发现,熔断,路由,微代理,控制总线,一次性token,全局锁,leader选举.分布 ...

  5. 第一篇:SpringBoot2.0简单介绍

    距离Spring Boot1.0发布已经4年了,今年3月份SpringBoot2.0正式发布.让我们一起来了解一下它. Spring Boot主要依赖于Spring,整合了很多框架的使用方式,帮助开发 ...

  6. 一篇对OAuth2.0开发实例的介绍

    今天看到了博友对SSO的文章,SSO单点登录的讲解突然想写一篇关于OAuth2.0用户授权的介绍. 应用场景:在APP或者网页接入一些第三方应用时,时长会需要用户登录另一个合作平台,比如QQ,微博,微 ...

  7. SpringBoot应用篇(一):自定义starter

    一.码前必备知识 1.SpringBoot starter机制 SpringBoot中的starter是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进starter,应用者只需要在mave ...

  8. 视频作品《springboot基础篇》上线了

    1.场景描述 第一个视频作品出炉了,<springboot基础篇>上线了,有需要的朋友可以直接点击链接观看.(如需购买,请通过本文链接购买) 2. 课程内容 课程地址:https://ed ...

  9. SpringBoot Web篇(二)

    摘要 继上一篇 SpringBoot Web篇(一) 文件上传 当我们服务器需要接收用户上传的文件时,就需要使用MultipartFile作为参数接收文件.如下: @PostMapping(" ...

随机推荐

  1. SQL Server强制释放内存

    --强制释放内存 CREATE procedure [dbo].ClearMemory as begin --清除所有缓存 DBCC DROPCLEANBUFFERS --打开高级配置 EXEC (' ...

  2. HTML 统一资源定位器

    URL 也被称为网址. URL 可以由单词组成,比如 “w3school.com.cn”,或者是因特网协议(IP)地址:192.168.1.253.大多数人在网上冲浪时,会键入网址的域名,因为名称比数 ...

  3. win中Oracle简易客户端和plsql的配置

    连接数据库有2种方式:在本机安装Oracle数据库或者是安装一个oracle简易客户端 当然,简易客户端跟oracle数据库比较少了一些功能 连接方式: 1)简易连接 sqlplus scott/ti ...

  4. 压缩,解压缩 和tar详细介绍

    文件压缩/解压缩  gzip   bzip2  xz 只能压缩文件,不能压缩文件夹(压缩完后,文件会消失) 先建三个文件来进行演示 touch ./{1..3}.txt  文件已经创建好,下面就开始介 ...

  5. Linux文件服务管理之nfs

    NFS(Network File System)即网络文件系统, 是FreeBSD支持的文件系统中的一种,它允许网络中的计算机之间通过TCP/IP网络共享资源. 在NFS的应用中,本地NFS的客户端应 ...

  6. shell之命令代换,将当前路径存放在变量中,然后使用变量

    重要的 命令代换`` 反引号 shell先执行该命令,然后将命令的结果存放在 变量中 例如 var=`pwd` echo $var 也可以用其$()替换 var=$(date) echo $var 删 ...

  7. 【jsp】案例:显示商品列表 & 问题:List内添加元素,为什么值都变成一样的了

    代码: 1.Product: package domain; public class Product { /* `pid` varchar(50) NOT NULL, `pname` varchar ...

  8. 【Tomcat】Web应用的目录结构

    创建时间:6.14 Web应用的目录结构 .xml文件不用自己写,抄头抄尾就可以 (别人的) (抄头抄尾) *注意:WEB-INF目录是受保护的,外界不能直接访问 如果访问WEB-INF目录下的htm ...

  9. Leetcode79 Word Search

    题目描述 Given a 2D board and a word, find if the word exists in the grid. The word can be constructed f ...

  10. 网传英特尔酷睿第十代桌面处理器(Comet Lake 14nm)规格

    自从农企(AMD)2016年开始崛起时,牙膏厂(英特尔)就开始发力,陆续两代推出性价比颇高的桌面处理器, 第八代.第九代酷睿桌面处理器相当的给力,而第十代酷睿桌面处理器会很猛啊,据传从酷睿i3到酷睿i ...