Spring(三)IoC之核心组件装配解析

建议先看最后的总结再细读全文

由上一篇文章得知,从 bean 的装配到获取,分别用到了三大组件:

  • 资源抽象 Resource
  • 工厂 DefaultListableBeanFactory
  • 配置信息读取器 BeanDefinitionReader
public class SpringDemo {
    public static void main(String[] args) {
        // 1.指定加载的资源文件
        Resource resource = new ClassPathResource("spring.xml");
        // 2.创建管理bean的工厂
        DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
        // 3.资源读取器,把读取的到信息装配到 defaultListableBeanFactory 里面,工厂再对 bean 进行管理
        BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
        // 4.读取装配文件 xml 里面的信息
        beanDefinitionReader.loadBeanDefinitions(resource);
        // 5.获取 bean
        Student student = defaultListableBeanFactory.getBean("student", Student.class);
        System.out.println(student.getName());
        System.out.println(student.getAge());
    }
}

1. ClassPathResource

接下来谈谈spring是如何通过 ClassPathResource 把资源加载进来。

首先来看看源码:

    private final String path;

    @Nullable
    private ClassLoader classLoader;

    @Nullable
    private Class<?> clazz;

    public ClassPathResource(String path) {
        // 继续调用来下面的方法
        this(path, (ClassLoader) null);
    }
    
    public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
        // 断言
        Assert.notNull(path, "Path must not be null");
        // 判断这个路径是否为一个合法的路径,并解析成spring能识别的标准路径
        String pathToUse = StringUtils.cleanPath(path);
        if (pathToUse.startsWith("/")) {
            pathToUse = pathToUse.substring(1);
        }
        // 已经是一个带解析并且能识别的path
        this.path = pathToUse;
        this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
    }
    
    public ClassPathResource(String path, @Nullable Class<?> clazz) {
        Assert.notNull(path, "Path must not be null");
        this.path = StringUtils.cleanPath(path);
        this.clazz = clazz;
    }
    

从源码可以看出,ClassPathResource 可以通过给定 class 或者给定的 classloader来进行资源的加载。ClassPathResource 的 构造方法主要是判断加载资源的路径是否有误,然后由默认的线程上下文类加载器(在运行期间,可以动态地去改变类加载器加载地方式)加载资源。如果传过来的classloader是空的,则:

    public static ClassLoader getDefaultClassLoader() {
        ClassLoader cl = null;
        try {
            cl = Thread.currentThread().getContextClassLoader();
        }
        catch (Throwable ex) {
            // Cannot access thread context ClassLoader - falling back...
        }
        if (cl == null) {
            // No thread context class loader -> use class loader of this class.
            cl = ClassUtils.class.getClassLoader();
            if (cl == null) {
                // getClassLoader() returning null indicates the bootstrap ClassLoader
                try {
                    cl = ClassLoader.getSystemClassLoader();
                }
                catch (Throwable ex) {
                    // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
                }
            }
        }
        return cl;
    }

从作者的源码和注释中可以看出,首先判断当前线程的上下文类加载器是否存在,若不存在,则使用当前 ClassUtils 的 classloader,cl还是为空则说明当前的 cl 是用到了 bootstrap classloader,如果一个类是通过bootstrap classloader 载入的,那我们通过这个类去获得classloader的话,有些jdk的实现是会返回一个null的。所以,如果以上都获取不到 classloader,最终会由 SystemClassLoader 系统类加载器 来进行加载。

2. DefaultListableBeanFactory

DefaultListableBeanFactory 是 BeanFactory 的一个默认实现类,它继承了AbstractAutowireCapableBeanFactory,实现了ConfigurableListableBeanFactory, BeanDefinitionRegistry。

defaultlistablebeanfactory.png

DefaultListableBeanFactory构造函数源码分析

public DefaultListableBeanFactory() {
        super();
    }

这里的无参构造函数调用了父类的方法,继续分析:

    public AbstractAutowireCapableBeanFactory() {
        super();
        ignoreDependencyInterface(BeanNameAware.class);
        ignoreDependencyInterface(BeanFactoryAware.class);
        ignoreDependencyInterface(BeanClassLoaderAware.class);
    }

可以看到创建了一个 AbstractAutowireCapableBeanFactory ,它 的无参构造方法除了继续调用了父类的方法之外,忽略掉了3个class文件,为了在依赖注入的时候不应该用这三种类型来进行依赖的注入。继续往上跟:

    public AbstractBeanFactory() {
    }

只是创建了一个 AbstractBeanFactory 实例。

3. BeanDefinitionReader 实现类 XmlBeanDefinitionReader

实现类XmlBeanDefinitionReader构造方法源码分析:

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        super(registry);
    }

把工厂一起传给了父类,继续往上跟:

    protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        this.registry = registry;

        // Determine ResourceLoader to use.
        if (this.registry instanceof ResourceLoader) {
            this.resourceLoader = (ResourceLoader) this.registry;
        }
        else {
            this.resourceLoader = new PathMatchingResourcePatternResolver();
        }

        // Inherit Environment if possible
        if (this.registry instanceof EnvironmentCapable) {
            this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
        }
        else {
            this.environment = new StandardEnvironment();
        }
    }

这里可以看到通过 ResourceLoader 去加载资源文件并继承它的环境,判断是否实现了ResourceLoader,若没有,则:

    public PathMatchingResourcePatternResolver() {
        this.resourceLoader = new DefaultResourceLoader();
    }
    @Override
    @Nullable
    public ClassLoader getClassLoader() {
        return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
    }

又回到了上面所说获取类加载器的方法,是一种典型的回退思想。这算是为工厂装配bean准备环境的一步。

  • EnvironmentCapable:如果可以的话 registry 把环境也继承了,如果没有则创建一个StandardEnvironment。

4. XmlBeanDefinitionReader.loadBeanDefinitions()

对 xml 文件信息整体的解析,将解析出来的xml信息装配成一个bean,并且把bean存放到工厂当中。看看 loadBeanDefinitions 做了哪些操作:

    public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
        return loadBeanDefinitions(new EncodedResource(resource));
    }

首先把 resource 传给 EncodedResource,让 EncodedResource 封装成一个真正带编码或者字符集的资源。且encoding 和 charset 是互斥关系:

    public EncodedResource(Resource resource) {
        this(resource, null, null);
    }

    public EncodedResource(Resource resource, @Nullable String encoding) {
        this(resource, encoding, null);
    }

    public EncodedResource(Resource resource, @Nullable Charset charset) {
        this(resource, null, charset);
    }

    private EncodedResource(Resource resource, @Nullable String encoding, @Nullable Charset charset) {
        super();
        Assert.notNull(resource, "Resource must not be null");
        this.resource = resource;
        this.encoding = encoding;
        this.charset = charset;
    }

然后再调用重载的 loadBeanDefinitions

    public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        Assert.notNull(encodedResource, "EncodedResource must not be null");
        if (logger.isTraceEnabled()) {
            logger.trace("Loading XML bean definitions from " + encodedResource);
        }

        Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();

        // 防止循环依赖,因为set集合不可重复的原因,若集合中存在 encodedResource A,再次add A 的时候则会抛出异常
        if (!currentResources.add(encodedResource)) {
            throw new BeanDefinitionStoreException(
                    "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
        }

        // 获取 encodedResource 里面 resource 的输入流
        try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
            // InputSource 用字节流创建一个新的输入源,是 org.xml.sax 提供的一个对象,并不是 spring 内部的
            InputSource inputSource = new InputSource(inputStream);
            // 这里我们传入的是 null
            if (encodedResource.getEncoding() != null) {
                inputSource.setEncoding(encodedResource.getEncoding());
            }
            // doLoadBeanDefinitions 方法在下文做出分析,实际是从指定的XML文件加载bean定义的方法
            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "IOException parsing XML document from " + encodedResource.getResource(), ex);
        }
        finally {
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                // resourcesCurrentlyBeingLoaded 是 threadlocal 变量,remove 防止内存泄漏
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }
    }
  • doLoadBeanDefinitions
    // 实际从指定的XML文件加载 bean 定义的方法
    protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {

        try {
            // 通过doLoadDocument加载inputSource资源,返回一个 doc 对象
            Document doc = doLoadDocument(inputSource, resource);
            // 注册给定DOM文档中包含的bean定义,完成对 xml 的解析
            int count = registerBeanDefinitions(doc, resource);
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded " + count + " bean definitions from " + resource);
            }
            return count;
        }
        catch (BeanDefinitionStoreException ex) {
            throw ex;
        }
        catch (SAXParseException ex) {
            throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                    "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
        }
        catch (SAXException ex) {
            throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                    "XML document from " + resource + " is invalid", ex);
        }
        catch (ParserConfigurationException ex) {
            throw new BeanDefinitionStoreException(resource.getDescription(),
                    "Parser configuration exception parsing XML from " + resource, ex);
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(resource.getDescription(),
                    "IOException parsing XML document from " + resource, ex);
        }
        catch (Throwable ex) {
            throw new BeanDefinitionStoreException(resource.getDescription(),
                    "Unexpected exception parsing XML document from " + resource, ex);
        }
    }

可以看出 doLoadBeanDefinitions 是对 spring 装配 bean 的主要实现。下一章看看Spring是如何装配bean的。

5. 总结

通过上面的分析得知,spring 想要把 bean 注册到工厂,然后再从工厂中获取(IoC容器思想)具体需要以下操作(文中采取的是xml方式注入,和注解方式注入实质上原理一样):

  • 通过ClassPathResource 把 xml 配置文件加载进来,加载方式有两种:
    1. 通过默认的线程上下文类加载器(SystemClassLoader)
    2. 通过给定的 class 对象
  • 定义一个Bean工厂 DefaultListableBeanFactory用来装配和管理Bean的所有信息,指定注入的相关方式,但这时和资源文件还没有任何交互
  • 定义一个资源读取器BeanDefinitionReader,调用其实现类XmlBeanDefinitionReader把读取到的beans信息装配到DefaultListableBeanFactory,这时已经准备好了装配bean的加载器和环境并和bean工厂关联起来。
  • 通过调用XmlBeanDefinitionReader.loadBeanDefinitions()方法把加载进来xml资源解析并装配成bean,一同注册到DefaultListableBeanFactory。此过程中,底层还有很多操作,可以概括为先把 xml 转成 inputSource 流资源,再转化成相应的 doc 对象,最终在DefaultBeanDefinitionDocumentReader类中通过doRegisterBeanDefinitions 方法,递归遍历doc对象的所有节点和元素从而完成bean定义的注册。

至此操作已全部完毕,下一章将会继续分析spring装配bean的详细过程。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,548评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,069评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,985评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,305评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,324评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,030评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,639评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,552评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,081评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,194评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,327评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,004评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,688评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,188评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,307评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,667评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,337评论 2 358

推荐阅读更多精彩内容