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. C#给企业微信中的成员发送消息

    先去企业微信门户网站获得密钥和应用ID 创建一个静态工具类 using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using ...

  2. Javascript屏蔽Backspace回退页面

    允许对输入框密码框等控件删除字符,但是不允许页面进行回退 <html lang="en" xmlns="http://www.w3.org/1999/xhtml&q ...

  3. log4net 写数据到sql数据库

    最近需要把用户的一些行为添加到数据库中,所以想到了用log4net ,如果有别的好的方案,大家可以给我指正. 先看一下配置文件 我这个是控制台文件 app.config <layout type ...

  4. 将MySQL升级到8.0.x后的遇到到错误及解决

    一,安装的时遇到的坑 我下的是Mysql 8.0.13 地址:https://dev.mysql.com/downloads/mysql/ 下的是解压版(个人能不用安装就不想用安装版的强迫症(/▽\) ...

  5. Mysql-修改用户连接数据库IP地址和用户名

    将用户连接数据库(5.7.14-7)的IP地址从 10.10.5.16   修改为 10.11.4.197 Mysql> rename user 'username'@'10.10.5.16' ...

  6. SQLServer2017自动备份数据库

    本文截图来源: 地址一:https://blog.csdn.net/u010944388/article/details/86674078 地址二:https://www.cnblogs.com/ja ...

  7. youtube-dll工具使用,很好用!!

    最近喜欢上youtube-dll这个插件,下载东西真的很好用,墙裂推荐,github地址如下 https://github.com/ytdl-org/youtube-dl 安装 1.Linux 1.1 ...

  8. Redis_数据类型

    Redis支持的键值数据类型如下: 字符串类型 散列类型 列表类型 集合类型 有序集合类型  一.字符串类型 字符串类型是Redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据.一个字 ...

  9. PHP redis 常用操作

    //在列表头部插入一个值one,当列表不存在时自动创建一个列表,key1为列表名 $redis->lpush("key1", "one"); //在列表尾 ...

  10. Congigure SSL in StoreFront

    StoreFront SSL Requirements StoreFront website must be up and running in http Joined to the domain C ...