前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot 应用篇之从 0 到 1 实现一个自定义 Bean 注册器

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

作者头像
一灰灰blog
发布2019-12-19 19:14:12
9970
发布2019-12-19 19:14:12
举报
文章被收录于专栏:小灰灰小灰灰

我们知道在 spring 中可以通过@Component@Service, @Repository 装饰一个类,通过自动扫描注册为 bean;也可以通过在配置类中,借助@Bean来注册 bean;那么除了这几种方式之外,还有什么其他的方式来声明一个类为 bean 么?

我们是否可以自定义一个注解,然后将这个注解装饰的类主动声明为 bean 注册到 spring 容器,从而实现类似@Component的效果呢?

接下来本文将介绍,如果通过ImportBeanDefinitionRegistrar结合自定义注解来实现 bean 注册,主要用到的知识点如下:

  • ImportBeanDefinitionRegistrar bean 注册的核心类
  • @Import 导入配置
  • ClassPathBeanDefinitionScanner

<!-- more -->

I. 自定义 bean 注册器

虽然我们的目标比较清晰,但是突然让我们来实现这么个东西,还真有点手足无措,应该从哪里下手呢?

0. 寻找"致敬"对象

如果看过我之前关于 SpringBoot 结合 java web 三剑客(Filter, Servlet, Listener)的相关博文的同学,应该会记得一个重要的知识点:

  • @WebListener, @WebServlet, @WebFilter 这三个注解属于 Servlet3+ 规范
  • 在 SpringBoot 项目中,如需要上面注解生效,需要在启动类上添加注解 @ServletComponentScan

看到上面这个是不是会有一丝灵感被激发(在当时写上面博文的时候,特意的看了一下后面注解的逻辑),嘿嘿,感觉找到了一条通往成功之旅的道路

既然@WebXxx注解不是原生的 Spring 支持注解,所以让他生效的注解 @ServletComponentScan就显得很重要了,显然是它充当了桥梁(在搞事情了),然后我们致敬(抄袭)的对象就有了

代码语言:javascript
复制
@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注解的功能,我们弄简单一点即可

代码语言:javascript
复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Meta {
}
b. @MetaComponentScan 注解

这个注解和@ServletComponentScan作用差不多,主要是用来加载ImportBeanDefinitionRegistrar实现类,后者则是定义 bean 的核心类

实现如下

代码语言:javascript
复制
@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 定义注册器,其核心方法为

代码语言:javascript
复制
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注解的类

代码语言:javascript
复制
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的属性来获取

代码语言:javascript
复制
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 的实现就有了

代码语言:javascript
复制
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,则构造方法会被调用

代码语言:javascript
复制
@Meta
public class DemoBean1 {
    public  DemoBean1() {
        System.out.println("DemoBean1 register!");
    }
}

2. case1 Meat 注解类,依赖 Bean

定义一个普通的 bean 对象

代码语言:javascript
复制
@Component
public class NormalBean {
    public NormalBean() {
        System.out.println("normal bean");
    }
}

然后定义一个 Meta 装饰的类,依赖 NormalBean

代码语言:javascript
复制
@Meta
public class DependBean {
    public DependBean(NormalBean normalBean) {
        System.out.println("depend bean! " + normalBean);
    }
}

3. case2 bean 依赖 Meta 注解类

代码语言:javascript
复制
@Component
public class ABean {
    public ABean(DemoBean1 demoBean1) {
        System.out.println("a bean : " + demoBean1);
    }
}

4. 测试

启动类,注意需要添加上我们自定义的@MetaComponentScan注解

代码语言:javascript
复制
@SpringBootApplication
@MetaComponentScan
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

执行输出结果

5. 小结

本文主要介绍了如何通过ImportBeanDefinitionRegistrar来实现自定义的 bean 注册器的全过程,包括面向新手可以怎样通过"致敬"既有的代码逻辑,来"巧妙"的实现我们的目标

II. 其他

0. 项目

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • I. 自定义 bean 注册器
    • 0. 寻找"致敬"对象
      • 1. 准备篇
        • 2. 开始实现
          • a. @Meta 注解定义
          • b. @MetaComponentScan 注解
          • c. MetaAutoConfigureRegistrar
      • II. 测试与小结
        • 1. case0 Meta 注解类
          • 2. case1 Meat 注解类,依赖 Bean
            • 3. case2 bean 依赖 Meta 注解类
              • 4. 测试
                • 5. 小结
                • II. 其他
                  • 0. 项目
                  相关产品与服务
                  容器服务
                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档