手写Spring系列1-注解版IOC容器的设计

事先说明一下:本人是菜鸟一个(大佬别喷我),意愿是自己实现一个简单版的Spring,尽可能实现Spring当中尽可能多的功能,并且整合Netty去做一个简单的HttpWebServer

完整博客地址会在http://wanna1314y.top:8090/中进行慢慢地更新,其它平台也许会更新(也许就忘掉了,比较懒)。

目前项目中已经实现足够多的功能,当然也还有很多bug没改,项目源码已经开源到Github:https://github.com/wanna280/My-Spring-Framework

1.AnnotationConfigApplicationContext的类设计

Spring中,支持注解版的IOC容器使用的是组件AnnotationConfigApplicationContext,因此我们这里也要去实现这个组件。这个类是支持传入一个packages列表作为要扫描的包,也支持传入一个Class作为导入所有组件的基础配置类。因此我们实现如下的构造器

@Slf4j
public class AnnotationConfigApplicationContext extends AbstractApplicationContext implements ApplicationContext {

    private DefaultListableBeanFactory beanFactory;

    /**
     * 指定扫描类路径下的候选BeanDefinition列表
     */
    private final ClassPathBeanDefinitionScanner scanner;

    private Environment environment;

    public AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        this.environment = getOrCreateEnvironment();  // 创建环境对象
        this.scanner = new ClassPathBeanDefinitionScanner(beanFactory);
        AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);
    }

    public AnnotationConfigApplicationContext() {
        this(new DefaultListableBeanFactory());
    }

    public AnnotationConfigApplicationContext(String... packages) {
        this();
        scanner.doScan(packages);  // 进行包扫描,扫描里面的相关组件
        this.refresh();  // 刷新容器,里面有启动容器的核心方法
    }

    public AnnotationConfigApplicationContext(Class<?> componentClass) {
        this();
        this.register(componentClass);  // 将配置类注册到Spring容器中去
        this.refresh();  // 刷新容器,里面有启动容器的核心方法
    }

    /**
     * 如果环境对象还没创建,那么就创建一个环境对象,并且return
     */
    private Environment getOrCreateEnvironment() {
        if (this.environment == null) {
            return new StandardEnvironment();
        }
        return this.environment;
    }
}

对于传入了packages/Class这两种情况,在创建了BeanFactory(DefaultListableBeanFactory)以及完成了相关的组件的注册之后,要做的自然就是调用refresh方法去刷新容器,这个方法的逻辑在它的父类AbstractApplicationContext中已经使用模板方法的形式去进行了自定义。

对于每个ApplicationContext(翻译过来叫应用程序上下文),它本身都是一个BeanFactory,但是它组合了一个DefaultListableBeanFactory这样的一个beanFactory去存放真正的对象,实际上创建出来的Bean什么的,都会存放在DefaultListableBeanFactory当中,而ApplicationContext中只是组合一个BeanFactory去对BeanFactory去做增强,这可以看做是装饰器模式。

image.png

关于ApplicationContextBeanFactory它们之间最常见的区别点:

  • 1.在ApplicationContext中新定义了BeanFactoryPostProcessor这个机制,去对BeanFactory去进行增强,可以在里面通过一些别的渠道去导入组件。
  • 2.在ApplicationContext中新增了事件监听机制,可以使用事件多拨器去发布事件。

ApplicationContext的一个抽象类的实现AbstractApplicationContext,它在refresh方法中对每个IOC容器启动过程中要完成的步骤,使用模板方法的形式去进行定义,子类如果想要自定义一些相关的逻辑,完全就可以继承自AbstractApplicationContext,然后去重写相关钩子方法去进行自己的逻辑定制即可。

下面是AbstractApplicationContext对容器启动的模板方法的步骤

    @Override
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

            // Prepare this context for refreshing.
            prepareRefresh();

            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);

            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);
                beanPostProcess.end();

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.
                finishRefresh();
            }

            catch (BeansException ex) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
                }

                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();

                // Reset 'active' flag.
                cancelRefresh(ex);

                // Propagate exception to caller.
                throw ex;
            }

            finally {
                // Reset common introspection caches in Spring's core, since we
                // might not ever need metadata for singleton beans anymore...
                resetCommonCaches();
                contextRefresh.end();
            }
        }
    }

我们通常使用到的都是ApplicationContext的相关实现类,比如ClassPathXmlApplicationContext(XML版本的IOC容器)、AnnotationConfigApplicationContext(注解版的IOC容器)甚至是一些在Web环境下的定制版的IOC容器,它们也都是继承自AbstractApplicationContext的实现类罢了。

2. 解决注解版IOC容器中需要进行包扫描工具的问题

要给定一个包,然后扫描指定的包下的候选的组件,肯定要用到对应的工具,对于注解版的候选组件的扫描,在Spring中用到的组件是ClassPathBeanDefinitionScanner,我们尝试去自己去实现这个类的代码如下:

@Slf4j
public class ClassPathBeanDefinitionScanner {

    /**
     * 已经扫描的包列表,用来解决某个包被重复扫描过的问题,如果缺少这个组件,很可能出现递归SOF
     */
    private static final Set<String> scannedPackages = new HashSet<>();

    private BeanDefinitionRegistry registry;

    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
        this.registry = registry;
    }

    /**
     * 扫描指定的包,扫描到合适的bd
     *
     * @param packages 指定的包列表
     * @return 扫描到的合适的bd
     */
    public Set<BeanDefinition<?>> doScan(String... packages) {
        Set<BeanDefinition<?>> candidateComponents = new HashSet<>();
        for (String pkg : packages) {
            Set<Class<?>> classes = ScanUtils.scan(pkg);  // 获取这个包下所有的类的集合
            // 如果当前的这个包在之前已经扫描过了,那么continue,不用继续扫描
            // 如果没扫描过,那么加入到列表中去...
            if (hasScannedPackage(pkg)) {
                continue;
            }
            scannedPackages.add(pkg);  // 标注当前包是已经扫描过的
            log.info("[{}] is scanning package : [{}]", this.getClass(), pkg);
            for (Class<?> clazz : classes) {
                // 1.如果该beanClass是接口或者是注解,那么continue
                // 2.如果该类上没有标注@Component注解,那么continue
                // 3.如果该类上有@Component注解,并且不是接口和注解,那么才需要将其进行注册到BeanFactory中
                if (ConfigurationClassUtils.isMetaAnnotated(clazz) ||
                        !ConfigurationClassUtils.isCandidateComponent(clazz)) {
                    continue;
                }
                // 根据给定的Class获取到BeanName(解析@Component注解/@Configuration注解)
                String beanName = BeanNameUtils.getBeanName(clazz);
                // 构建BeanDefinition,并加入到最终扫描结果的set中去,以及放到Spring的bdMap中去
                BeanDefinition<?> bd = new RootBeanDefinition<>(beanName, clazz);
                registry.registerBeanDefinition(beanName, bd);

                // 处理通用注解,@Primary/@Lazy等注解
                AnnotationConfigUtils.processCommonDefinitionAnnotations(bd, AnnotationMetadata.introspect(clazz));
                candidateComponents.add(bd);
            }
        }
        return candidateComponents;
    }

    /**
     * 判断当前的包是否在之前已经扫描过了
     *
     * @param pkg 指定的package
     * @return 如果扫描过了,return true,不然return false
     */
    private boolean hasScannedPackage(String pkg) {
        for (String scannedPackage : scannedPackages) {
            if (pkg.startsWith(scannedPackage)) {
                return true;
            }
        }
        return false;
    }
}

其中用到了一个工具类ScanUtils,它的作用就是给的一个package,然后把该包下的所有类的Class对象解析出来,这个类太多代码了,也没太多的暂时的价值,对这个类感兴趣的直接去Github找吧,其实网上对于扫描指定包下的类的方案有很多,比如使用谷歌的guava,这里只是自己实现了简单功能罢了。

3. 关于使用配置类中导入的方式的Bean如何处理

3.1 内置核心组件的导入以及它的作用

比如使用到注解版的IOC容器时,肯定会使用到类似如下这样的代码去指定一个配置类App,然后就去启动IOC容器:

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(App.class)

而在这个App类上会去使用@ComponentScan等渠道去导入Bean

其实这些渠道导入的Bean并不会在AnnotationConfigApplicationContext这个类当中就被处理到,而是调用了refresh方法之后,在invokeBeanFactoryPostProcessors这个很关键的一个步骤去导入的。

其实在这个类的构造器中,我们使用了下面这样的代码,去给容器中添加BeanFactoryPostProcessor等组件,方便容器进行启动。

AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory)

其中一个很关键的组件叫做ConfigurationClassPostProcessor,它的作用,其实就是注解版IOC容器实现的核心,在它里面就定义了处理配置类的逻辑,比如处理@Import注解、@Bean注解、@ImportSource注解等一堆注解。

3.2 自制版ConfigurationClassPostProcessor类的源码实现

@Slf4j
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {

    private ConfigurationClassBeanDefinitionReader reader;

    private BeanDefinitionRegistry registry;

    private ConfigurationClassParser parser;

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        reader = new ConfigurationClassBeanDefinitionReader(registry);
        parser = new ConfigurationClassParser(registry);
        // 使用ConfigurationClassParser去扫描所有的配置类(@ComponentScan/@Component/@Configuration)
        // 并且还将@Bean/@Import/@ImportSource的导入的Bean进行了处理,后续可以方便地直接进行注册
        // 扫描到的配置类列表可以通过parser.getConfigurationClasses获取到
        parser.parse();

        // 获取Parser解析出来的配置类集合
        Set<ConfigurationClass> configurationClasses = new HashSet<>(parser.getConfigurationClasses());

        // 使用Reader去加载Bean(处理@Bean/Registrar/ImportSource等)
        reader.loadBeanDefinitions(configurationClasses);

        log.info("[{}] has imported [{}] ConfigurationClass", this.getClass(), configurationClasses.size());
    }

    /**
     * 执行ImportBeanDefinitionRegistrar.registerBeanDefinitions
     *
     * @param registry BeanDefinitionRegistry
     * @see ImportBeanDefinitionRegistrar
     */
    private void processImportBeanDefinitionRegistrar(BeanDefinitionRegistry registry) {

        // 从容器中拿到所有的ImportBeanDefinitionRegistrar执行它的registerBeanDefinitions去注册信息
        List<ImportBeanDefinitionRegistrar> registrars = BeanFactoryUtils.findAll((ConfigurableListableBeanFactory) registry,
                ImportBeanDefinitionRegistrar.class);
        for (ImportBeanDefinitionRegistrar registrar : registrars) {
            registrar.registerBeanDefinitions(null, registry);
        }
    }

    @Override
    public int getOrder() {
        return ORDER_LOWEST;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {

    }

}

这个组件中主要会使用到两个很关键的组件,ConfigurationClassBeanDefinitionReaderConfigurationClassParser

  • 1.ConfigurationClassParser的作用,从名字当中我们就可以看到,它是一个配置类的解析器,当然是用来解析配置类的。真正的处理逻辑,其实在它的doProcessConfigurationClass方法里,从这个doXXX的方法命名当中,我们也可以很清楚的看到一个事情,那就是,这个方法,它是真正要干事情的,在它之前,都是做些准备工作,还没做事!
  • 2.ConfigurationClassBeanDefinitionReader的作用呢,其实就是处理@ImportSource@Import(ImportBeanDeinitionRegistrar.class)@Bean这些情况,将这些情况导入的Bean都导入到Spring容器当中去

3.3 对于ConfigurationClassParser的核心代码

    // 处理配置类
    private void doProcessConfigurationClass(ConfigurationClass configurationClass) {

        // 如果目标配置类加了@Component注解
        if (configurationClass.getMetadata().hasAnnotation(Component.class.getName())) {

        }

        // 如果目标配置类加了@Configuration注解
        if (configurationClass.getMetadata().hasAnnotation(Configuration.class.getName())) {

        }

        // 如果目标配置类标注了@PropertySource/@PropertySources(容器)注解,那么需要将指定的配置文件加载到容器的环境当中去...
        processPropertySources(configurationClass);

        // 如果目标配置类加了@ComponentScan/@ComponentScans(容器)注解,那么这里可以获取到配置的包名信息,并进行递归扫描和处理
        processComponentScans(configurationClass);

        // 如果标注了@Import注解(处理ImportSelector/BeanDefinitionRegistrar)
        processImports(configurationClass, getImportCandidates(configurationClass));

        // 如果标注了@ImportSource注解,那么需要进行解析
        processImportResources(configurationClass);

        // 如果有标注@Bean注解的方法,那么需要进行处理@Bean注解
        processBeans(configurationClass);
    }

在这里其实处理@ComponenScan注解时,还用到了ComponentScanAnnotationParser这个组件,它其实就是组合了一个之前我们讲过的ClassPathBeanDefinitionScanner去做包扫描。

#Java学习##学习路径##Spring#
全部评论
这也太牛b了吧,牛b格拉斯!
点赞 回复 分享
发布于 2022-01-12 15:48

相关推荐

牛至超人:哈工大已经很棒了,不需要加括号了,然后咋没有实习经历呢?火速趁寒假整一段实习,导师不让就狠狠肘击
投了多少份简历才上岸
点赞 评论 收藏
分享
写下这篇文章的时候,我正坐在从学校飞往北京的飞机上。就在今天,我的秋招终于算是有了结论,一共60场面试,拿到了字节百度美团等10+大厂offers,最终确认了腾讯给的机会。同时给我的这三个月,这三年以及从今天往前的所有人生做了个结。这句话写的真好,为什么这么说呢?本来挺久之前我就想写点什么,有特别多想记录的,从选择这个专业到选择这个岗位,从科研的疲惫到未来生活的期待,但总感觉这样写没个纲,乱成一团。直到我今天正式在系统中点击了三方的确认,我才突然发现这种感觉就是“不可逃避的结束”在向我走来,于是纲便有了。首先是这三个月的结果吧,或者换句话说,其实是秋招的结果。从我硕士选择了强化学习的研究方向,我就知道并不会有太多的岗位。从试错中学习,这听起来很符合人类的学习方式,但实际场景中哪来那么多试错的成本?除了游戏产业和机器人行业,我想不到特别对口的赛道,而这两个行业国内又只有寡头,让我望而生畏。整个秋招,我没法像学后端开发的同学一样投递大量的简历,我没法像学大模型的同学一样是时代的香饽饽,我只能盯着那几家公司去投,或者想方设法的在别的不太相关的算法岗上沾沾边。方向是大于努力的,但努力一定不是不重要的。秋招整体对我来说还算顺利,前文就自然变成了只有我自己懂的无病呻吟,不再赘述。从结果来说,我的秋招是非常成功的,至少我自己是满意的。命运给了我很大的惊喜,我从未想过能够在这次有多个远超期待的offer,所以我如今是心满意足。虽说很多事都是焉知非福吧,但对口的工作内容,熟悉的工作环境,我一定不会后悔。我就是这样,毕竟让我在做一百次选择也不会变,那为什么要在不可预测的未来后悔。然后是三年,三年即将过去,我的硕士生涯来到了最后一章。回想过往,我在其中反复感受井底之蛙的狭隘。从我在二十多个四点睡的凌晨产出的论文初稿开始,链式反应就这样发生了。把论文投出去,我发了一篇很长的朋友圈,那时候觉得压力真的好大,尽管其实根本没人要求我什么。那时,我第一次觉得我比本科毕业时的自己进步了太多,可以独当一面了。然后去了北京自所交流,尽管大多的时间都在修改那篇返稿的文章,但也在不一样的平台中见识了人外有人的世界。回来后,我第二次觉得自己有了很大的进步,而鄙夷去北京前的自己是如此短浅。那是11月,我开始纠结到底未来该从事开发岗还是算法岗,但时间并没有给我机会。我偷懒了,两个月根本没有做任何开发岗的准备,于是只能硬闯算法。期间只有那篇论文中了让我稍微有些自信,毕竟只有两周的理论准备时间让我心里太虚了,这甚至还算上了刷题的时间。第一面就是最想去的公司,我甚至紧张到大脑一片空白。好在后面算是有惊无险,拿到了腾讯给我的实习机会。去腾讯工作的时间是幸福的,组里氛围也很好,在公司获得的提升我觉得甚至超过了我在学校一年的量。毕竟做算法,思维的敏捷度和见识广度都是如此重要。看着同事前辈们的工作能力,和工业级的项目架构,我又一次不由得感叹曾经自己的狭隘。于是每天我只睡五小时,忙完工作忙学校,每每想到这里,我也不觉得我的成功是侥幸了。我真的建议大家离开自己舒适的环境到外面看看,鸡头或许真的不如凤尾。硕士是一个连锁反应最直接,最有力的时期。高考失利或许还能补救,考研没上岸还有第二次机会,但就业前这一年,努力就是会有回报,就一定会体现在结果中,没有侥幸。最后,也是我最想聊的。十九年的学生生涯终于快要画下句号,我其实一直觉得非常梦幻。我能回忆起每一个瞬间,有小学六年级遇到的很有个性的数学老师,有考上重点中学的快乐,有中考和提前高考而大失败的难受,有本科比赛的每个通宵的焦虑,有保研出现差错的绝望,有刚读研高压之下的崩溃。但这篇长文不会再有更多的剧情了,每个故事都让我无限回味,成为了我一生中最宝贵的财富。这些瞬间组成了我。我父亲说我是一个总抓不住机会的人,确实有很多别人没有的机会摆在我面前,我都错过了。但我心中的热爱始终没有错过,我觉得这对我来说是幸运且幸福的。我非常爱打游戏,从初中开始学编程,第一个目的就是做出属于自己的游戏,做了很多小游戏发在班级群里,被人厌烦。高中自己买了unity的书,想做自己的游戏,无奈连网络的基本知识都不懂,无功而返。到了大学,我又被强化学习吸引,我想知道能不能让人工智能来帮我打游戏呢?这一整条线我没有放弃过,拿到了游戏算法offer,我真的特别特别开心。人不是一直成功的,我经历过的失败远超过成功10倍,但那让我知道成功来之不易,让我知道失败是生活常态,让我知道真正的怯懦不是不敢失败,而是不敢尝试。言尽于此,这些都“不可逃避的结束”了。追风赶月莫停留,平芜尽处是春山。
肖先生~:追风赶月莫停留,平芜尽处是春山,passion!
我的秋招日记
点赞 评论 收藏
分享
评论
5
12
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务