02.Spring IOC源码深度解析之容器的基本实现

来源:互联网 发布:夏达姚非拉事件 知乎 编辑:程序博客网 时间:2024/06/03 20:28

在搭建完Spring源码阅读环境后,大家比较困惑的应该就是从哪里开始入手阅读Spring源码了。众所周知Spring的两大特性为IOC和AOP,那我们先以IOC为例跟进Spring源码。IOC(Inversion of Control):即”控制反转”,它不是什么技术而是一种设计思想,在传统的JAVA程序设计中当我们需要创建一个对象时,我们直接在对象内部通过new进行创建,而IOC是有一个专门的容器来控制对象的创建(即将对象的创建过程交由容器来完成) ,IOC也叫DI(Dependency Injection):DI即依赖注入,由容器动态的将某个依赖关系注入到组件之中,理解DI的关键是:谁依赖谁 为什么需要依赖 谁注入谁 注入了什么

容器基本用法

为了更好的理解Spring源码,后续我们会创建许多的测试案例。为了保证逻辑清晰我选择的是在spring-framework项目下新建一个独立的模块spring-roberto,该模块通过引用其他子模块来完成功能测试

在spring-framework下新增子模块spring-roberto,并修改gradle配置内容如下(添加了spring-beans的依赖)

group 'org.springframework'version '5.0.3.BUILD-SNAPSHOT'apply plugin: 'java'sourceCompatibility = 1.8repositories {    mavenCentral()}dependencies {    compile(project(":spring-beans"))    testCompile group: 'junit', name: 'junit', version: '4.12'}

Spring IOC的简单实现

新建实体类TestBean.java

package _01.ioc_simple_impl;public class TestBean {    private String str = "testStr";    public String getStr() {        return str;    }    public void setStr(String str) {        this.str = str;    }}

新建applicationContext.xml文件

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans    http://www.springframework.org/schema/beans/spring-beans.xsd">    <bean id="testBean" class="org.springframework.roberto._01_iocsimpleimpl.TestBean" /></beans>

新建测试类

package org.springframework.roberto;import org.junit.Test;import org.springframework.beans.factory.BeanFactory;import org.springframework.beans.factory.xml.XmlBeanFactory;import org.springframework.core.io.ClassPathResource;import org.springframework.roberto._01_iocsimpleimpl.TestBean;@SuppressWarnings("deprecation")public class _01_IocSimpleImplTest {    @Test    public void testSimpleLoad() {        BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("01.ioc_simple_impl.xml"));        TestBean testBean = (TestBean) beanFactory.getBean("testBean");        System.out.println(testBean.getStr());    }}

运行结果输出testStr,到此我们已经实现了IOC的功能。这个过程非常的简单但是内部实现原理却是九曲十八弯,相信你在看完后续的源码解析时,你也会对封装有一个更好的理解(特别是等研究源码半个月或者一个月发现其实你只跟进阅读2行代码时)

容器功能分析

以上测试代码主要帮我们完成如下几个功能:

1.读取applicationContext.xml文件2.根据applicationContext.xml中的配置找到对应类的配置并实例化3.调用实例化后的实例,返回结果

资源文件的加载

资源文件加载的代码为:

BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));

时序图:
资源文件的加载

通过时序图可以很清楚的看出资源加载过程是:

1.先将配置文件读取成Resource资源文件的实例对象2.然后再对XmlBeanFactory进行初始化操作

加载配置文件

new ClassPathResource(“applicationContext.xml”)过程分析

public ClassPathResource(String path, @Nullable ClassLoader classLoader) {    Assert.notNull(path, "Path must not be null");    // 对配置文件路径进行处理    String pathToUse = StringUtils.cleanPath(path);    if (pathToUse.startsWith("/")) {        pathToUse = pathToUse.substring(1);    }    // 初始化path和classLoader    this.path = pathToUse;    this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());}

Spring配置文件的加载使用ClassPathResource,打开ClassPathResource类继承结构发现它实现了Resource接口

Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等,并且Resource接口提供了几个重要的方法如:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen) 。对不同来源的资源文件Spring都内置了不同的Resource实现,如:文件(FileSystemResource)、Classpath资源(ClassPathResource)、URL资源(UrlResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等

同时Resource继承了InputStreamSource接口,InputStreamSource封装任何能返回InputStream的类,它只有一个方法定义:getInputStream(),该方法返回一个InputStream对象。在日常开发工作中资源文件的加载也是经常用到的,可以直接使用Spring提供的类,比如在加载文件时可以使用如下代码:

Resource resource = new ClassPathResource("applicationContext.xml");InputStream inputStream = resource.getInputStream();

加载XmlBeanFactory

new XmlBeanFactory(resource)过程分析

public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {    // ignoreDependencyInterface(BeanNameAware.class);    // ignoreDependencyInterface(BeanFactoryAware.class);    // ignoreDependencyInterface(BeanClassLoaderAware.class);    super(parentBeanFactory);    this.reader.loadBeanDefinitions(resource);}

在super(parentBeanFactory)方法中调用了ignoreDependencyInterface(Class<?> ifc)方法,以下是郝佳Spring源码深度解析中对ignoreDependencyInterface(Class<?>> ifc)的解释

举例来说当A中有属性B,那么当Spring在获取A的Bean的时候如果其属性B还没有初始化,那么Spring会自动初始化B,这也是Spring中提供的一个重要特性。但是某些情况下B不会被初始化,其中的一种情况就是B实现了BeanNameAware接口。Spring中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析Application上下文注册依赖,类似于BeanFactory通过BeanFactoryAware进行注入或者ApplicationContext通过ApplicationContextAware进行注入

经过尝试发现即使属性B实现了以上接口还是会被初始化,经过查看源码个人认为以上的理解并不正确。我认为此处说的忽略自动注入是指:假设A实现了BeanFactoryAware接口重写了setBeanFactory方法,那么属性beanFactory是不能通过自动注入方式进行注入

源码分析过程:ignoreDependencyInterface(Class<?> ifc)方法实现的功能其实是往ignoredDependencyInterfaces中添加Class,而通过Find Usage查找调用链发现使用ignoredDependencyInterfaces属性进行判断的方法为AbstractAutowireCapableBeanFactory中的isExcludedFromDependencyCheck,继续搜索调用链找到unsatisfiedNonSimpleProperties方法,而unsatisfiedNonSimpleProperties是在autowireByName和autowireByType中调用的。即在autowireByName和autowireByType方法中如果发现类继承了对应的以上三个接口,那么对应的属性是会被忽略注入的

注:在Spring不是所有类型都能自动装配,Object,基本数据类型及(Date CharSequence Number URI URL Class)等是不会被自动装配的,此处的自动装配指配置文件中的autowire而非注解@Autowired

加载Bean的主要逻辑是在this.reader.loadBeanDefinitions(resource);中执行的,下面为该方法的时序图
loadBeanDefinitions时序图

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {    Assert.notNull(encodedResource, "EncodedResource must not be null");    if (logger.isInfoEnabled()) {        logger.info("Loading XML bean definitions from " + encodedResource.getResource());    }    // 记录以及加载过的配置文件    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();    if (currentResources == null) {        currentResources = new HashSet<>(4);        this.resourcesCurrentlyBeingLoaded.set(currentResources);    }    // 配置文件循环引入检查(在配置文件中import自己即可进入该异常)    if (!currentResources.add(encodedResource)) {        throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");    }    try {        InputStream inputStream = encodedResource.getResource().getInputStream();        try {            InputSource inputSource = new InputSource(inputStream);            if (encodedResource.getEncoding() != null) {                inputSource.setEncoding(encodedResource.getEncoding());            }            // 加载Bean            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());        } finally {            inputStream.close();        }    } catch (IOException ex) {        throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), ex);    } finally {        currentResources.remove(encodedResource);        if (currentResources.isEmpty()) {            this.resourcesCurrentlyBeingLoaded.remove();        }    }}

该方法可拆分为数据准备和核心处理两个部分,核心部分代码为doLoadBeanDefinitions(inputSource, encodedResource.getResource());

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException {    try {        Document doc = doLoadDocument(inputSource, resource);        return registerBeanDefinitions(doc, resource);    } 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);    }}

以上为Bean加载核心处理部分代码,总共处理了两件事,一是解析配置的XML文件转为Document对象,二是根据解析出来的Document注册Bean信息

解析配置的XML文件转为Document对象过程

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {    // getEntityResolver()获取EntityResolver对象    // getValidationModeForResource(resource) 获取资源校验模式    return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, getValidationModeForResource(resource), isNamespaceAware());}

在执行loadDocument操作前做了两个准备工作,第一是获取EntiryResolver对象,第二是获取XML校验模式,最后根据这些参数解析XML配置文件转换为Document

官网对于EntityResolver的解释为:如果SAX应用程序需要实现自定义处理外部实体,则必须实现此接口。解析一个XML文件,SAX首先读取该XML文档上的说明,根据声明去寻找对应的DTD定义,以便对文档进行一个验证。默认是通过网络下载对应的声明的,但是该过程容易因为网络问题等原因导致出错,EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法(org.springframework.beans.factory.xml.ResourceEntityResolver#resolveEntity(String publicId, String systemId)),即由程序来实现寻找DTD声明的过程(该部分代码不影响理解Spring主流程 了解即可)

getValidationModeForResource(resource)执行过程:

protected int getValidationModeForResource(Resource resource) {    // 判断是否手动设置了校验模式    int validationModeToUse = getValidationMode();    if (validationModeToUse != VALIDATION_AUTO) {        return validationModeToUse;    }    // 判断配置文件中是否有DOCTYPE字段 区分是DTD还是XSD    int detectedMode = detectValidationMode(resource);    if (detectedMode != VALIDATION_AUTO) {        return detectedMode;    }    // 默认为XSD方式解析    return VALIDATION_XSD;}

loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware):

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {    DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);    if (logger.isDebugEnabled()) {        logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");    }    DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);    return builder.parse(inputSource);}

这部分代码没啥好说的,就是使用最原始的javax.xml.parsers解析配置的XML文件转为Document过程,至此我们已经将配置文件XML=>Document对象了

解析及注册BeanDefinitions

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {    // 新建BeanDefinitionDocumentReader实例对象(DefaultBeanDefinitionDocumentReader)    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();    // 记录统计前BeanDefinition加载个数    int countBefore = getRegistry().getBeanDefinitionCount();    // 加载注册Bean    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));    // 记录本次加载的BeanDefinition个数    return getRegistry().getBeanDefinitionCount() - countBefore;}

加载注册Bean执行过程:

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {    this.readerContext = readerContext;    logger.debug("Loading bean definitions");    Element root = doc.getDocumentElement();    doRegisterBeanDefinitions(root);}

经过艰难险阻磕磕碰碰,终于接触到了核心逻辑的底部doRegisterBeanDefinitions(root);如果说之前一直是XML加载解析准备阶段,那么doRegisterBeanDefinitions(root);算是真正的开始解析了

protected void doRegisterBeanDefinitions(Element root) {    BeanDefinitionParserDelegate parent = this.delegate;    // 创建Bean解析代理对象 实际上正在的解析过程是在BeanDefinitionParserDelegate这个代理中    this.delegate = createDelegate(getReaderContext(), root, parent);    // 处理Profile属性    if (this.delegate.isDefaultNamespace(root)) {        String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);        if (StringUtils.hasText(profileSpec)) {            String[] specifiedProfiles = StringUtils.tokenizeToStringArray(                    profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);            if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {                if (logger.isInfoEnabled()) {                    logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +                            "] not matching: " + getReaderContext().getResource());                }                return;            }        }    }    // 解析前操作 为了扩展交给子类实现     preProcessXml(root);    // 解析过程    parseBeanDefinitions(root, this.delegate);    // 解析后操作 为了扩展交给子类实现     postProcessXml(root);    this.delegate = parent;}

创建Bean解析代理对象过程解析:

protected BeanDefinitionParserDelegate createDelegate(XmlReaderContext readerContext, Element root, @Nullable BeanDefinitionParserDelegate parentDelegate) {    // 新建BeanDefinitionParserDelegate对象    BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);    // 参数初始化    delegate.initDefaults(root, parentDelegate);    return delegate;}

先是调用构造函数新建BeanDefinitionParserDelegate实例,然后调用initDefaults(root, parentDelegate);对参数进行初始化

public void initDefaults(Element root, @Nullable BeanDefinitionParserDelegate parent) {    // 对DocumentDefaultsDefinition属性进行初始化    populateDefaults(this.defaults, (parent != null ? parent.defaults : null), root);    // 对外开放口子 用于修改DocumentDefaultsDefinition属性的默认值    this.readerContext.fireDefaultsRegistered(this.defaults);}

在参数初始化过程中主要完成了两件事,一是通过populateDefaults方法对属性defaults:DocumentDefaultsDefinition进行初始化(涉及属性有default lazy-init, autowire, dependency check settings, init-method, destroy-method and merge settings),二是提供口子用于修改DocumentDefaultsDefinition的参数值(Spring只提供了空实现 若要使用该功能需自行拓展)

关于Spring提供的这个口子,我在网上看到一个写的很好的Demo。背景是工程单元测试希望和生产环境共用一份Spring配置文件,生产环境应用为了客户体验使用非LazyInit模式,但是单元测试下为了提高响应时间希望LazyInit

1.新建自定义事件监听器

package org.springframework.roberto._03_lazy_init;import org.springframework.beans.factory.parsing.*;import org.springframework.beans.factory.xml.DocumentDefaultsDefinition;public class CustomerEventListener implements ReaderEventListener {    @Override    public void defaultsRegistered(DefaultsDefinition defaultsDefinition) {        if (defaultsDefinition instanceof DocumentDefaultsDefinition) {            DocumentDefaultsDefinition defaults = (DocumentDefaultsDefinition) defaultsDefinition;            defaults.setLazyInit("true");        }    }    @Override    public void componentRegistered(ComponentDefinition componentDefinition) {    }    @Override    public void aliasRegistered(AliasDefinition aliasDefinition) {    }    @Override    public void importProcessed(ImportDefinition importDefinition) {    }}

新建LazyInitClasspathXmlApplicationContext类继承ClassPathXmlApplicationContext 并重写initBeanDefinitionReader方法

package org.springframework.roberto._03_lazy_init;import org.springframework.beans.BeansException;import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;import org.springframework.context.support.ClassPathXmlApplicationContext;public class LazyInitClasspathXmlApplicationContext extends ClassPathXmlApplicationContext {    public LazyInitClasspathXmlApplicationContext(String... configLocations) throws BeansException {        super(configLocations);    }    @Override    protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) {        super.initBeanDefinitionReader(reader);        reader.setEventListener(new CustomerEventListener());    }}

新建实体类TestLazyInitBean

package org.springframework.roberto._03_lazy_init;public class TestLazyInitBean {    public void init() {        try {            Thread.sleep(5000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("TestBean Init");    }}

修改配置文件如下

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans    http://www.springframework.org/schema/beans/spring-beans.xsd">    <bean id="testLazyInitBean" class="org.springframework.roberto._03_lazy_init.TestLazyInitBean" init-method="init"/></beans>

测试方法如下

package org.springframework.roberto;import org.junit.Test;import org.springframework.context.support.ClassPathXmlApplicationContext;import org.springframework.roberto._03_lazy_init.LazyInitClasspathXmlApplicationContext;public class _03_LazyInitTest {    @Test    public void testLayzyInit() {        long currentTime = System.currentTimeMillis();        new ClassPathXmlApplicationContext("03.lazy_init.xml");        System.out.println("No LazyInit Cost Time:" + (System.currentTimeMillis() - currentTime));        currentTime = System.currentTimeMillis();        new LazyInitClasspathXmlApplicationContext("03.lazy_init.xml");        System.out.println("LazyInit Cost Time:" + (System.currentTimeMillis() - currentTime));    }}

测试结果

TestBean InitNo LazyInit Cost Time:6203LazyInit Cost Time:37

测试结果表明这里确实改变了Spring的懒加载设置,其实此处原理就是修改了ReaderContext的ReaderEventListener的实现

这里使用ClassPathXmlApplicationContext方式测试是因为BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean)才对该Bean进行加载实例化,我们就不能发现一些存在的Spring配置问题,而ApplicationContext则相反它是在容器启动时一次性创建了所有的Bean,这样在容器启动时,我们就可以发现Spring中存在的错误。相对于BeanFactory而言,ApplicationContext唯一的不足就是占用内存空间,当应用程序配置Bean较多时程序启动慢

对profiles属性使用解析:
修改applicationContext.xml配置文件如下: (最外层beans的profile属性为可能出现的环境 内层的profile指向具体环境)

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans    http://www.springframework.org/schema/beans/spring-beans.xsd">    <beans profile="dev">        <bean id="testProfileBean" class="org.springframework.roberto._04_profile.TestProfileBean"></bean>    </beans>    <beans profile="test">    </beans></beans>

新建测试类

package org.springframework.roberto;import org.junit.Test;import org.springframework.beans.factory.BeanFactory;import org.springframework.beans.factory.xml.XmlBeanFactory;import org.springframework.core.io.ClassPathResource;import org.springframework.roberto._04_profile.TestProfileBean;@SuppressWarnings("all")public class _04_ProfileTest {    @Test    public void testProfile() {        System.setProperty("spring.profiles.active","dev");        BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("04.profile.xml"));        TestProfileBean testProfileBeant = (TestProfileBean) beanFactory.getBean("testProfileBean");        System.out.println(testProfileBeant);    }}

测试通过说明dev配置是生效的,若将spring.profiles.active设置成test测试不通过。在集成到Web环境中时,可以在web.xml中加入以下代码

<context-param>      <param-name>spring.profiles.active</param-name>      <param-value>dev</param-value>  </context-param>  

有了这个特性我们就可以同时在配置文件中部署两套配置来适用于生产环境和开发环境,这样可以方便的进行切换开发、部署环境,最常用的就是更换不同的数据库了(这种方式其实并不直观 现在项目大多数是Maven项目 Maven的profiles对多环境支持更强大)

转载请注明作者及出处,原文链接为:作者黄太洪 标题02.Spring IOC源码深度解析之容器的基本实现

阅读全文
0 0
原创粉丝点击