手写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去做包扫描。

美团公司福利 3017人发布
查看2道真题和解析