【超详细的Spring源码分析 —— 02 Spring对于Bean管理的核心组件源码分析 – 准备阶段】

在上一篇文章中,我提到了 Spring 在宏观上的 IoC 执行流程,也粗略地拆解了整个流程中的核心组件,这些组件包括了:

  • 资源抽象 – Resource
  • 注册器,也可称之为工厂 – DefaultListableBeanFactory
  • 读取器 – BeanDefinitionReader

那么在这一篇文章中,我们来看看 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);
    }
}
复制代码

一、资源抽象 Resource 的创建

这里我们分析的是资源抽象基于 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 抽象需要完成的工作有如下两点:

  • 让我们传入的 path 被 Spring 解析为可用的路径字符串。
  • 根据不同的方式(基于ClassLoader解析或是基于Class对象解析)来创建不同的解析器实例。

但以上代码只涉及到了 Resource 实例的初始化,还没到真正解析元数据的过程。

二、DefaultListableBeanFactory 的创建

创建好了资源对象,下一步就是创建默认工厂了,我们接着来看 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 不具备这些接口提供的特性。

三、BeanDefinitionReader 的创建

按照流程来看,我们的资源、工厂都准备完毕了,之后的工作就是创建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();
        }
    }
}
复制代码

我们单独对 Environment 进行分析:

/**
 * 这个接口代表了当前应用正在运行的环境
 * 
 * 主要对应用程序中两个关键的环境进行处理: 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);
}
复制代码

以上呢,就是读取器在创建时需要完成的工作。总结一下读取器创建时完成的工作:

  • 配置读取器的 ResourceLoader 用于资源加载。
  • 配置读取器的 Environment 用于处理环境相关的工作,比如程序运行时采用的属性配置、程序运行在什么环境下。

四、总结

在本章中,我对 IoC 容器在整个执行流程中的准备阶段进行了详细的源码分析,我们可以粗略地把完成的工作总结如下:

  • Resource

    • 处理配置文件 path 路径,使其能够被Spring成功识别
    • 创建解析器实例,默认会采取 classLoader 作为资源解析器
  • 注册器

    • 忽略掉一些指定的依赖接口
  • 读取器

    • 将注册器赋值到全局变量 —— BeanDefinitionRegistry(在之后会用于组装 bean)
    • 创建出资源读取器,并赋值到全局变量 —— ResourceLoader(在之后会用于读取资源并解析)
    • 创建出环境实例,并赋值到全局变量 —— Environment(在后续会用于处理 profile 和 properties 相关的内容)

截止目前,所有的准备工作都已经就绪了。那么剩下的就是解析 resource 然后注册 Bean 到容器中了。虽然只有短短一行代码,但是内部的逻辑十分复杂,我会在下一章展开详细分析

原文 

https://juejin.im/post/5eff1590e51d45346d30e56b

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 【超详细的Spring源码分析 —— 02 Spring对于Bean管理的核心组件源码分析 – 准备阶段】

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址