在上一篇文章中,我提到了 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 到容器中了。虽然只有短短一行代码,但是内部的逻辑十分复杂,我会在下一章展开详细分析。