在上一篇文章中,我提到了 Spring 在宏观上的 IoC 执行流程,也粗略地拆解了整个流程中的核心组件,这些组件包括了:
那么在这一篇文章中,我们来看看 IoC 容器在解析并装配 Bean 之前都需要完成哪些准备工作。
整个分析流程我们参照着 IoC 容器初始化的流程走:
public class SpringClient {
public static void main(String[] args) {
// 1. 首先是创建资源
Resource resource = new ClassPathResource("applicationContext.xml");
// 2. 创建工厂(也可以理解为创建BeanDefinition注册器)
DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
// 3. 创建读取器
BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
// 4. 读取资源, 完成bean装配
beanDefinitionReader.loadBeanDefinitions(resource);
// 5. 依赖注入
Student student = defaultListableBeanFactory.getBean("student", Student.class);
}
}
复制代码
这里我们分析的是资源抽象基于 classpath 实现 —— ClassPathResource
。那么下面我们就来详细看一下,Spring 在从 classpath 获取资源对象时,底层究竟完成了那些工作。
首先我们来看一下 ClassPathResource
初始化流程涉及到的代码:
/**
* {@link Resource} Resource接口的实现类.
* 通过提供一个类加载器或者是Class实例类来加载资源
*/
public class ClassPathResource extends AbstractFileResolvingResource {
// 资源路径
private final String path;
// 用于加载资源的类加载器
@Nullable
private ClassLoader classLoader;
// 用于加载资源的类对象
@Nullable
private Class<?> clazz;
/**
* 它会调用参数为 path + classLoader 构造方法
*/
public ClassPathResource(String path) {
this(path, (ClassLoader) null);
}
/**
* 指定path、classLoader创建实例
* 需要注意的是, 若采用类加载器的方式加载资源, 那么path必须是classpath下的绝对路径
* @param 类路径中的绝对路径
* @param 用于加载资源的类加载器, 当这个参数为null时, 会默认采用线程上下文类加载器
*/
public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
// 确保path一定不为null
Assert.notNull(path, "Path must not be null");
// 这里会调用一个字符串工具类, 对path进行一些转换, 保证能够被Spring识别并可用
String pathToUse = StringUtils.cleanPath(path);
// 不允许path以 "/" 开头
if (pathToUse.startsWith("/")) {
pathToUse = pathToUse.substring(1);
}
// 将资源路径赋值给全局变量path
this.path = pathToUse;
// 当传入的类加载器不为空时, 就采用指定的类加载器
// 否则这边会通过一个工具类获取
// 感兴趣的可以看一下这个工具类的具体代码, 我贴在了下方
this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
/**
* ClassUtils.getDefaultClassLoader()
* 返回一个默认的ClassLoader供我们使用
*/
@Nullable
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
// 默认情况下, 获取的是当前线程上下文ClassLoader
// 这里解释一下什么是当前线程的上下文ClassLoader:
// 线程上下文ClassLoader能够在程序运行时动态地改变类加载的方式
// 就比如我们指定类A的加载方式是通过:扩展类加载器Ext
// 那么在改回类加载方式之前, 我们获取到的当前线程上下文ClassLoader都是扩展类加载器
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back...
}
// 当我们没有成功获取到线程上下文ClassLoader时, 会进入这个逻辑块
if (cl == null) {
// 获取ClassUtils这个类的类加载器
cl = ClassUtils.class.getClassLoader();
// 如果获取到的类加载器为null, 那么说明一定是BootStrapClassLoader
// BootStrap加载器在Java中被表示为null
// 虽然这种情况一般不太可能发生, 但Spring还是考虑在内了
if (cl == null) {
try {
// 这里会返回 BootStrapClassLoader
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
return cl;
}
/**
* 指定path、Class对象创建实例
*/
public ClassPathResource(String path, @Nullable Class<?> clazz) {
// 确保path不为空
Assert.notNull(path, "Path must not be null");
this.path = StringUtils.cleanPath(path);
this.clazz = clazz;
}
/**
* 这个方法会指定path、ClassLoader、Class对象三个属性创建实例
* 但Spring并不提倡我们使用这个构造方法, 可以看到这个方法被标记了 @Deprecated
* Spring要求我们:要么使用ClassLoader去读取Resource; 要么通过Class去读取Resource
*/
@Deprecated
protected ClassPathResource(String path, @Nullable ClassLoader classLoader, @Nullable Class<?> clazz) {
this.path = StringUtils.cleanPath(path);
this.classLoader = classLoader;
this.clazz = clazz;
}
//...
}
复制代码
总的来说,Resource 抽象需要完成的工作有如下两点:
但以上代码只涉及到了 Resource 实例的初始化,还没到真正解析元数据的过程。
创建好了资源对象,下一步就是创建默认工厂了,我们接着来看 new DefaultListableBeanFactory();
这部分涉及到的源码。
但在阅读这个类的源码之前,我们必要知道:在Spring中,任何与工厂相关的类,它的继承树最顶层都是 BeanFactory 这个接口。
原归正传,我们跟进到这个类的源码:
/**
* DefaultListableBeanFactory.DefaultListableBeanFactory()
* Create a new DefaultListableBeanFactory.
*/
public DefaultListableBeanFactory() {
// 调用了父类的构造方法, 我们继续深入到父类
super();
}
/**
* AbstractAutowireCapableBeanFactory.AbstractAutowireCapableBeanFactory()
* Create a new AbstractAutowireCapableBeanFactory.
*/
public AbstractAutowireCapableBeanFactory() {
// 依然调用了父类的构造方法, 这个方法会创建一个AbstractBeanFactory实例
// 我们就不接着向上跟进了, 但无论怎么跟进, 它们终归都是BeanFactory的实现
super();
// 忽略下面这些依赖的接口
// 这些Aware类型的接口主要的功能就是:当触发某些特定的事件时, 会调用接口中的回调方法
// 这些接口都是Spring提供, 但是并不推荐被实现的接口
// 因为Spring并不希望我们在依赖注入时, 根据这些接口来进行依赖注入
// 我们可以看一下这些接口的具体实现
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
}
/**
* 这个接口需要被BeanFactory中的实例Bean实现, 标识着这个Bean会依赖于这个Bean的名称
* note: 这是个不好的做法, 因为这代表一种脆弱的依赖关系
*/
public interface BeanNameAware extends Aware {
// 方法就会设置上Bean所依赖的Bean名称
void setBeanName(String name);
}
/**
* 这个接口需要被那些需要知道自己在哪个BeanFactory的Bean实现
* 这个接口也是不推荐被实现的
*/
public interface BeanFactoryAware extends Aware {
void setBeanFactory(BeanFactory beanFactory) throws BeansException;
}
/**
* 使得实现这个接口的bean知道它自身的回调
*/
public interface BeanClassLoaderAware extends Aware {
void setBeanClassLoader(ClassLoader classLoader);
}
复制代码
总的来说呢,DefaultListableBeanFactory 这个默认工厂会具备 BeanFactory 的特性,因为它最顶层的实现接口就是 BeanFactory,但同时它也实现了 BeanDefinitionRegistry 这个接口, 因此它也具备注册器的特性。
并且在 DefaultListableBeanFactory 被初始化时,会忽略掉一些依赖的接口,简单来说就是让属于该工厂的所有 Bean 不具备这些接口提供的特性。
按照流程来看,我们的资源、工厂都准备完毕了,之后的工作就是创建Bean定义读取器,将资源解析为Bean实例,并组装到工厂了。由于我们采用的是 xml 的形式配置元数据,因此我们主要阅读 XmlBeanDefinitionReader 相关的内容。
废话不多说,直接上源码:
// 首先是构造方法, 需要传入一个Bean定义注册器,也就是工厂类
// 我们会把解析出来的Bean实例注册到这个工厂中
public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
// 这边会调用父类的构造方法
super(registry);
}
复制代码
接下来,我们跟进 XmlBeanDefinitionReader 的父类构造方法:
// 可以看到,XmlBeanDefinitionReader的父类是一个抽象类
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable {
// 构造方法主要是交给子类继承的
protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
//断言:register不允许为空
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
// 将注册器实例赋值给全局变量
this.registry = registry;
// 如果注册器还同时具备了ResourceLoader的特性
// 也就是说如果注册器还具备资源读取的功能
if (this.registry instanceof ResourceLoader) {
// 那么将该注册器赋值到全局变量resourceLoader, 用于资源加载
this.resourceLoader = (ResourceLoader) this.registry;
} else { // 若注册器不提供资源读取的功能, 那么就会初始化一个资源加载器
// 这里底层调用的, 就是之前在 Resource 源码中分析到的 ClassUtil.getDefaultClassLoader()
// 简单来说, 就是获取到一个类加载器用于加载资源
this.resourceLoader = new PathMatchingResourcePatternResolver();
}
// 如果注册器实现了EnvironmentCapable接口
// 那么会通过注册器获取到相应的Environment实例
// 这个Environment实例很关键, 我们会单独对其进行分析
if (this.registry instanceof EnvironmentCapable) {
this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
} else { // 否则会实例化一个标准的Environment 实例
this.environment = new StandardEnvironment();
}
}
}
复制代码
/**
* 这个接口代表了当前应用正在运行的环境
*
* 主要对应用程序中两个关键的环境进行处理: profiles and properties.
*
* profile: 决定应用程序在什么环境之下运行程序, 例如开发环境、运维环境、测试环境等
* properties: 决定应用程序中各种各样的属性配置
*
* 简单来说, Environment就是对以上两个属性的封装
* 它继承了PropertyResolver, 具备处理properties的特性, 然后在此基础上增加了处理profile的特性
*/
public interface Environment extends PropertyResolver {
// 下面这些方法都是针对 profile 的相关API
String[] getActiveProfiles();
String[] getDefaultProfiles();
@Deprecated
boolean acceptsProfiles(String... profiles);
boolean acceptsProfiles(Profiles profiles);
}
复制代码
以上呢,就是读取器在创建时需要完成的工作。总结一下读取器创建时完成的工作:
在本章中,我对 IoC 容器在整个执行流程中的准备阶段进行了详细的源码分析,我们可以粗略地把完成的工作总结如下:
Resource
注册器
读取器
截止目前,所有的准备工作都已经就绪了。那么剩下的就是解析 resource 然后注册 Bean 到容器中了。虽然只有短短一行代码,但是内部的逻辑十分复杂,我会在下一章展开详细分析。