zoukankan      html  css  js  c++  java
  • SpringBoot 应用篇之从 0 到 1 实现一个自定义 Bean 注册器

    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 或者有更好的建议,欢迎批评指正,不吝感激

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

    一灰灰blog

  • 相关阅读:
    排序与查找
    gdb
    编码风格
    数组
    结构体
    循环语句
    深入理解函数
    分支语句
    简单函数
    解决国内NPM安装依赖速度慢问题
  • 原文地址:https://www.cnblogs.com/yihuihui/p/12038632.html
Copyright © 2011-2022 走看看