手写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

相关推荐

4 10 评论
分享
牛客网
牛客企业服务