手写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
去做增强,这可以看做是装饰器模式。
关于ApplicationContext
和BeanFactory
它们之间最常见的区别点:
- 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) { } }
这个组件中主要会使用到两个很关键的组件,ConfigurationClassBeanDefinitionReader
和ConfigurationClassParser
。
- 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
去做包扫描。