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

简介: 我们知道在 spring 中可以通过@Component,@Service, @Repository 装饰一个类,通过自动扫描注册为 bean;也可以通过在配置类中,借助@Bean来注册 bean;那么除了这几种方式之外,还有什么其他的方式来声明一个类为 bean 么?我们是否可以自定义一个注解,然后将这个注解装饰的类主动声明为 bean 注册到 spring 容器,从而实现类似@Component的效果呢?

我们知道在 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 注册器的全过程,包括面向新手可以怎样通过"致敬"既有的代码逻辑,来"巧妙"的实现我们的目标



相关文章
|
1月前
|
XML Java 开发者
Spring Boot中的bean注入方式和原理
Spring Boot中的bean注入方式和原理
61 0
|
1月前
|
存储 NoSQL Java
Spring Boot统计一个Bean中方法的调用次数
Spring Boot统计一个Bean中方法的调用次数
35 1
|
2月前
|
存储 Java Maven
QR码应用实战:Spring Boot与ZXing完美结合
QR码应用实战:Spring Boot与ZXing完美结合
30 0
|
2月前
|
安全 Java Spring
SpringBoot2 | SpringBoot监听器源码分析 | 自定义ApplicationListener(六)
SpringBoot2 | SpringBoot监听器源码分析 | 自定义ApplicationListener(六)
47 0
|
2月前
|
消息中间件 存储 监控
搭建消息时光机:深入探究RabbitMQ_recent_history_exchange在Spring Boot中的应用【RabbitMQ实战 二】
搭建消息时光机:深入探究RabbitMQ_recent_history_exchange在Spring Boot中的应用【RabbitMQ实战 二】
34 1
|
1月前
|
Java 数据库 数据安全/隐私保护
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
109 1
|
1天前
|
缓存 Java Sentinel
Springboot 中使用 Redisson+AOP+自定义注解 实现访问限流与黑名单拦截
Springboot 中使用 Redisson+AOP+自定义注解 实现访问限流与黑名单拦截
|
7天前
|
Java Spring 容器
SpringBoot 使用Quartz执行定时任务对象时无法注入Bean问题
SpringBoot 使用Quartz执行定时任务对象时无法注入Bean问题
10 1
|
28天前
|
Prometheus 监控 Cloud Native
Spring Boot 应用可视化监控
Spring Boot 应用可视化监控
17 0
|
1月前
|
XML Java 数据格式
【springboot原理篇】Bean的加载方式,面试必看
【springboot原理篇】Bean的加载方式,面试必看