欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

spring源码深度解析— IOC 之 容器的基本实现

程序员文章站 2024-01-07 13:36:58
概述 上一篇我们搭建完Spring源码阅读环境,spring源码深度解析—Spring的整体架构和环境搭建 这篇我们开始真正的阅读Spring的源码,分析spring的源码之前我们先来简单回顾下spring核心功能的简单使用 容器的基本用法 bean是spring最核心的东西,spring就像是一个 ......

概述

上一篇我们搭建完spring源码阅读环境,spring源码深度解析—spring的整体架构和环境搭建 这篇我们开始真正的阅读spring的源码,分析spring的源码之前我们先来简单回顾下spring核心功能的简单使用

容器的基本用法

bean是spring最核心的东西,spring就像是一个大水桶,而bean就是水桶中的水,水桶脱离了水也就没有什么用处了,我们简单看下bean的定义,代码如下:

package com.chenhao.spring;

/**
 * @author: chenhao
 * @description:
 * @date: created in 10:35 2019/6/19
 * @modified by:
 */
public class mytestbean {
    private string name = "chenhao";
public string getname() {
        return name;
    }

    public void setname(string name) {
        this.name = name;
    }
}

源码很简单,bean没有特别之处,spring的的目的就是让我们的bean成为一个纯粹的的pojo,这就是spring追求的,接下来就是在配置文件中定义这个bean,配置文件如下:

<?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="mytestbean" class="com.chenhao.spring.mytestbean"/>

</beans>

在上面的配置中我们可以看到bean的声明方式,在spring中的bean定义有n种属性,但是我们只要像上面这样简单的声明就可以使用了。 
具体测试代码如下:

import com.chenhao.spring.mytestbean;
import org.junit.test;
import org.springframework.beans.factory.beanfactory;
import org.springframework.beans.factory.xml.xmlbeanfactory;
import org.springframework.core.io.classpathresource;

/**
 * @author: chenhao
 * @description:
 * @date: created in 10:36 2019/6/19
 * @modified by:
 */
public class apptest {
    @test
    public void mytestbeantest() {
        beanfactory bf = new xmlbeanfactory( new classpathresource("spring-config.xml"));
        mytestbean mytestbean = (mytestbean) bf.getbean("mytestbean");
        system.out.println(mytestbean.getname());
    }
}

运行上述测试代码就可以看到输出结果如下图: 

spring源码深度解析— IOC 之 容器的基本实现

其实直接使用beanfactory作为容器对于spring的使用并不多见,因为企业级应用项目中大多会使用的是applicationcontext(后面我们会讲两者的区别,这里只是测试)

功能分析

接下来我们分析2中代码完成的功能;
- 读取配置文件spring-config.xml。
- 根据spring-config.xml中的配置找到对应的类的配置,并实例化。
- 调用实例化后的实例
下图是一个最简单spring功能架构,如果想完成我们预想的功能,至少需要3个类: 

spring源码深度解析— IOC 之 容器的基本实现

configreader :用于读取及验证自己直文件 我们妥用配直文件里面的东西,当然首先 要做的就是读取,然后放直在内存中.

reflectionutil :用于根据配置文件中的自己直进行反射实例化,比如在上例中 spring-config.xml 出现的<bean id="mytestbean" class="com.chenhao.spring.mytestbean"/>,我们就可以根据 com.chenhao.spring.mytestbean 进行实例化。

app :用于完成整个逻辑的串联。

 工程搭建

spring的源码中用于实现上面功能的是spring-bean这个工程,所以我们接下来看这个工程,当然spring-core是必须的。

beans包的层级结构

阅读源码最好的方式是跟着示例操作一遍,我们先看看beans工程的源码结构,如下图所示: 

 spring源码深度解析— IOC 之 容器的基本实现

- src/main/java 用于展现spring的主要逻辑 
- src/main/resources 用于存放系统的配置文件 
- src/test/java 用于对主要逻辑进行单元测试 
- src/test/resources 用于存放测试用的配置文件

核心类介绍

接下来我们先了解下spring-bean最核心的两个类:defaultlistablebeanfactory和xmlbeandefinitionreader

defaultlistablebeanfactory

xmlbeanfactory继承自defaultlistablebeanfactory,而defaultlistablebeanfactory是整个bean加载的核心部分,是spring注册及加载bean的默认实现,而对于xmlbeanfactory与defaultlistablebeanfactory不同的地方其实是在xmlbeanfactory中使用了自定义的xml读取器xmlbeandefinitionreader,实现了个性化的beandefinitionreader读取,defaultlistablebeanfactory继承了abstractautowirecapablebeanfactory并实现了configurablelistablebeanfactory以及beandefinitionregistry接口。以下是configurablelistablebeanfactory的层次结构图以下相关类图

spring源码深度解析— IOC 之 容器的基本实现

上面类图中各个类及接口的作用如下:
- aliasregistry:定义对alias的简单增删改等操作
- simplealiasregistry:主要使用map作为alias的缓存,并对接口aliasregistry进行实现
- singletonbeanregistry:定义对单例的注册及获取
- beanfactory:定义获取bean及bean的各种属性
- defaultsingletonbeanregistry:默认对接口singletonbeanregistry各函数的实现
- hierarchicalbeanfactory:继承beanfactory,也就是在beanfactory定义的功能的基础上增加了对parentfactory的支持
- beandefinitionregistry:定义对beandefinition的各种增删改操作
- factorybeanregistrysupport:在defaultsingletonbeanregistry基础上增加了对factorybean的特殊处理功能
- configurablebeanfactory:提供配置factory的各种方法
- listablebeanfactory:根据各种条件获取bean的配置清单
- abstractbeanfactory:综合factorybeanregistrysupport和configurationbeanfactory的功能
- autowirecapablebeanfactory:提供创建bean、自动注入、初始化以及应用bean的后处理器
- abstractautowirecapablebeanfactory:综合abstractbeanfactory并对接口autowirecapablebeanfactory进行实现
- configurablelistablebeanfactory:beanfactory配置清单,指定忽略类型及接口等
- defaultlistablebeanfactory:综合上面所有功能,主要是对bean注册后的处理
xmlbeanfactory对defaultlistablebeanfactory类进行了扩展,主要用于从xml文档中读取beandefinition,对于注册及获取bean都是使用从父类defaultlistablebeanfactory继承的方法去实现,而唯独与父类不同的个性化实现就是增加了xmlbeandefinitionreader类型的reader属性。在xmlbeanfactory中主要使用reader属性对资源文件进行读取和注册

xmlbeandefinitionreader

xml配置文件的读取是spring中重要的功能,因为spring的大部分功能都是以配置作为切入点的,可以从xmlbeandefinitionreader中梳理一下资源文件读取、解析及注册的大致脉络,首先看看各个类的功能

resourceloader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的resource
beandefinitionreader:主要定义资源文件读取并转换为beandefinition的各个功能
environmentcapable:定义获取environment方法
documentloader:定义从资源文件加载到转换为document的功能
abstractbeandefinitionreader:对environmentcapable、beandefinitionreader类定义的功能进行实现
beandefinitiondocumentreader:定义读取document并注册beandefinition功能
beandefinitionparserdelegate:定义解析element的各种方法
整个xml配置文件读取的大致流程,在xmlbeandefinitionreader中主要包含以下几步处理 

spring源码深度解析— IOC 之 容器的基本实现

(1)通过继承自abstractbeandefinitionreader中的方法,来使用resourceloader将资源文件路径转换为对应的resource文件
(2)通过documentloader对resource文件进行转换,将resource文件转换为document文件
(3)通过实现接口beandefinitiondocumentreader的defaultbeandefinitiondocumentreader类对document进行解析,并使用beandefinitionparserdelegate对element进行解析

容器的基础xmlbeanfactory

 通过上面的内容我们对spring的容器已经有了大致的了解,接下来我们详细探索每个步骤的详细实现,接下来要分析的功能都是基于如下代码:

beanfactory bf = new xmlbeanfactory( new classpathresource("spring-config.xml"));

首先调用classpathresource的构造函数来构造resource资源文件的实例对象,这样后续的资源处理就可以用resource提供的各种服务来操作了。有了resource后就可以对beanfactory进行初始化操作,那配置文件是如何封装的呢? 

配置文件的封装 

 spring的配置文件读取是通过classpathresource进行封装的,spring对其内部使用到的资源实现了自己的抽象结构:resource接口来封装底层资源,如下源码:

public interface inputstreamsource {
    inputstream getinputstream() throws ioexception;
}
public interface resource extends inputstreamsource {
    boolean exists();
    default boolean isreadable() {
        return true;
    }
    default boolean isopen() {
        return false;
    }
    default boolean isfile() {
        return false;
    }
    url geturl() throws ioexception;
    uri geturi() throws ioexception;
    file getfile() throws ioexception;
    default readablebytechannel readablechannel() throws ioexception {
        return channels.newchannel(getinputstream());
    }
    long contentlength() throws ioexception;
    long lastmodified() throws ioexception;
    resource createrelative(string relativepath) throws ioexception;
    string getfilename();
    string getdescription();
}

 inputstreamsource封装任何能返回inputstream的类,比如file、classpath下的资源和byte array等, 它只有一个方法定义:getinputstream(),该方法返回一个新的inputstream对象 。

resource接口抽象了所有spring内部使用到的底层资源:file、url、classpath等。首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isreadable)、是否处于打开状态(isopen)。另外,resource接口还提供了不同资源到url、uri、file类型的转换,以及获取lastmodified属性、文件名(不带路径信息的文件名,getfilename())的方法,为了便于操作,resource还提供了基于当前资源创建一个相对资源的方法:createrelative(),还提供了getdescription()方法用于在错误处理中的打印信息。
对不同来源的资源文件都有相应的resource实现:文件(filesystemresource)、classpath资源(classpathresource)、url资源(urlresource)、inputstream资源(inputstreamresource)、byte数组(bytearrayresource)等,相关类图如下所示: 

spring源码深度解析— IOC 之 容器的基本实现

在日常开发中我们可以直接使用spring提供的类来加载资源文件,比如在希望加载资源文件时可以使用下面的代码:

resource resource = new classpathresource("spring-config.xml");
inputstream is = resource.getinputstream();

有了 resource 接口便可以对所有资源文件进行统一处理 至于实现,其实是非常简单的,以 getlnputstream 为例,classpathresource 中的实现方式便是通 class 或者 classloader 提供的底层方法进行调用,而对于 filesystemresource 其实更简单,直接使用 fileinputstream 对文件进行实例化。

classpathresource.java

inputstream is;
if (this.clazz != null) {
    is = this.clazz.getresourceasstream(this.path);
}
else if (this.classloader != null) {
    is = this.classloader.getresourceasstream(this.path);
}
else {
    is = classloader.getsystemresourceasstream(this.path);
}

filesystemresource.java

public inputstream getinputstream () throws ioexception {
    return new filelnputstream(this file) ; 
}

当通过resource相关类完成了对配置文件进行封装后,配置文件的读取工作就全权交给xmlbeandefinitionreader来处理了。
接下来就进入到xmlbeanfactory的初始化过程了,xmlbeanfactory的初始化有若干办法,spring提供了很多的构造函数,在这里分析的是使用resource实例作为构造函数参数的办法,代码如下:

xmlbeanfactory.java

public xmlbeanfactory(resource resource) throws beansexception {
    this(resource, null);
}
public xmlbeanfactory(resource resource, beanfactory parentbeanfactory) throws beansexception {
    super(parentbeanfactory);
    this.reader.loadbeandefinitions(resource);
}

上面函数中的代码this.reader.loadbeandefinitions(resource)才是资源加载的真正实现,但是在xmlbeandefinitionreader加载数据前还有一个调用父类构造函数初始化的过程:super(parentbeanfactory),我们按照代码层级进行跟踪,首先跟踪到如下父类代码:

public defaultlistablebeanfactory(@nullable beanfactory parentbeanfactory) {
    super(parentbeanfactory);
}

然后继续跟踪,跟踪代码到父类abstractautowirecapablebeanfactory的构造函数中:

public abstractautowirecapablebeanfactory(@nullable beanfactory parentbeanfactory) {
    this();
    setparentbeanfactory(parentbeanfactory);
}
public abstractautowirecapablebeanfactory() {
    super();
    ignoredependencyinterface(beannameaware.class);
    ignoredependencyinterface(beanfactoryaware.class);
    ignoredependencyinterface(beanclassloaderaware.class);
}

这里有必要提及 ignoredependencylnterface方法,ignoredependencylnterface  的主要功能是 忽略给定接口的向动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢?

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

调用ignoredependencyinterface方法后,被忽略的接口会存储在beanfactory的名为ignoreddependencyinterfaces的set集合中:

public abstract class abstractautowirecapablebeanfactory extends abstractbeanfactory
        implements autowirecapablebeanfactory {

    private final set<class<?>> ignoreddependencyinterfaces = new hashset<>();
    
    public void ignoredependencyinterface(class<?> ifc) {
        this.ignoreddependencyinterfaces.add(ifc);
    }
...
}

ignoreddependencyinterfaces集合在同类中被使用仅在一处——isexcludedfromdependencycheck方法中:

protected boolean isexcludedfromdependencycheck(propertydescriptor pd) {
    return (autowireutils.isexcludedfromdependencycheck(pd) || this.ignoreddependencytypes.contains(pd.getpropertytype()) || autowireutils.issetterdefinedininterface(pd, this.ignoreddependencyinterfaces));
}

而ignoreddependencyinterface的真正作用还得看autowireutils类的issetterdefinedininterface方法。

public static boolean issetterdefinedininterface(propertydescriptor pd, set<class<?>> interfaces) {
    //获取bean中某个属性对象在bean类中的setter方法
    method setter = pd.getwritemethod();
    if (setter != null) {
        // 获取bean的类型
        class<?> targetclass = setter.getdeclaringclass();
        for (class<?> ifc : interfaces) {
            if (ifc.isassignablefrom(targetclass) && // bean类型是否接口的实现类
                classutils.hasmethod(ifc, setter.getname(), setter.getparametertypes())) { // 接口是否有入参和bean类型完全相同的setter方法
                return true;
            }
        }
    }
    return false;
}

ignoreddependencyinterface方法并不是让我们在自动装配时直接忽略实现了该接口的依赖。这个方法的真正意思是忽略该接口的实现类中和接口setter方法入参类型相同的依赖。
举个例子。首先定义一个要被忽略的接口。

public interface ignoreinterface {

    void setlist(list<string> list);

    void setset(set<string> set);
}

然后需要实现该接口,在实现类中注意要有setter方法入参相同类型的域对象,在例子中就是list<string>和set<string>。

public class ignoreinterfaceimpl implements ignoreinterface {

    private list<string> list;
    private set<string> set;

    @override
    public void setlist(list<string> list) {
        this.list = list;
    }

    @override
    public void setset(set<string> set) {
        this.set = set;
    }

    public list<string> getlist() {
        return list;
    }

    public set<string> getset() {
        return set;
    }
}

定义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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemalocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd"
default-autowire="bytype">


    <bean id="list" class="java.util.arraylist">
        <constructor-arg>
            <list>
                <value>foo</value>
                <value>bar</value>
            </list>
        </constructor-arg>
    </bean>

    <bean id="set" class="java.util.hashset">
        <constructor-arg>
            <list>
                <value>foo</value>
                <value>bar</value>
            </list>
        </constructor-arg>
    </bean>

    <bean id="ii" class="com.chenhao.ignoredependency.ignoreinterfaceimpl"/>
    <bean class="com.chenhao.autowire.ignoreautowiringprocessor"/>
</beans>

最后调用ignoredependencyinterface:

beanfactory.ignoredependencyinterface(ignoreinterface.class);

运行结果:
null
null
而如果不调用ignoredependencyinterface,则是:
[foo, bar]
[bar, foo]

我们最初理解是在自动装配时忽略该接口的实现,实际上是在自动装配时忽略该接口实现类中和setter方法入参相同的类型,也就是忽略该接口实现类中存在依赖外部的bean属性注入。

典型应用就是beanfactoryaware和applicationcontextaware接口。
首先看该两个接口的源码:

public interface beanfactoryaware extends aware {
    void setbeanfactory(beanfactory beanfactory) throws beansexception;
}

public interface applicationcontextaware extends aware {
    void setapplicationcontext(applicationcontext applicationcontext) throws beansexception;
}

在spring源码中在不同的地方忽略了该两个接口:

beanfactory.ignoredependencyinterface(applicationcontextaware.class);
ignoredependencyinterface(beanfactoryaware.class);

使得我们的beanfactoryaware接口实现类在自动装配时不能被注入beanfactory对象的依赖:

public class mybeanfactoryaware implements beanfactoryaware {
    private beanfactory beanfactory; // 自动装配时忽略注入

    @override
    public void setbeanfactory(beanfactory beanfactory) throws beansexception {
        this.beanfactory = beanfactory;
    }

    public beanfactory getbeanfactory() {
        return beanfactory;
    }
}

applicationcontextaware接口实现类中的applicationcontext对象的依赖同理:

public class myapplicationcontextaware implements applicationcontextaware {
    private applicationcontext applicationcontext; // 自动装配时被忽略注入

    @override
    public void setapplicationcontext(applicationcontext applicationcontext) throws beansexception {
        this.applicationcontext = applicationcontext;
    }

    public applicationcontext getapplicationcontext() {
        return applicationcontext;
    }
}
这样的做法使得applicationcontextaware和beanfactoryaware中的applicationcontext或beanfactory依赖在自动装配时被忽略,而统一由框架设置依赖,如applicationcontextaware接口的设置会在applicationcontextawareprocessor类中完成:
private void invokeawareinterfaces(object bean) {
    if (bean instanceof aware) {
        if (bean instanceof environmentaware) {
            ((environmentaware) bean).setenvironment(this.applicationcontext.getenvironment());
        }
        if (bean instanceof embeddedvalueresolveraware) {
            ((embeddedvalueresolveraware) bean).setembeddedvalueresolver(this.embeddedvalueresolver);
        }
        if (bean instanceof resourceloaderaware) {
            ((resourceloaderaware) bean).setresourceloader(this.applicationcontext);
        }
        if (bean instanceof applicationeventpublisheraware) {
            ((applicationeventpublisheraware) bean).setapplicationeventpublisher(this.applicationcontext);
        }
        if (bean instanceof messagesourceaware) {
            ((messagesourceaware) bean).setmessagesource(this.applicationcontext);
        }
        if (bean instanceof applicationcontextaware) {
            ((applicationcontextaware) bean).setapplicationcontext(this.applicationcontext);
        }
    }
}

通过这种方式保证了applicationcontextaware和beanfactoryaware中的容器保证是生成该bean的容器。

bean加载

在之前xmlbeanfactory构造函数中调用了xmlbeandefinitionreader类型的reader属性提供的方法this.reader.loadbeandefinitions(resource),而这句代码则是整个资源加载的切入点,这个方法的时序图如下: 

spring源码深度解析— IOC 之 容器的基本实现

我们来梳理下上述时序图的处理过程:
(1)封装资源文件。当进入xmlbeandefinitionreader后首先对参数resource使用encodedresource类进行封装
(2)获取输入流。从resource中获取对应的inputstream并构造inputsource
(3)通过构造的inputsource实例和resource实例继续调用函数doloadbeandefinitions,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();
    if (currentresources == null) {
        currentresources = new hashset<>(4);
        this.resourcescurrentlybeingloaded.set(currentresources);
    }
    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());
            }
            return doloadbeandefinitions(inputsource, encodedresource.getresource());
        }
        finally {
            inputstream.close();
        }
    }
    ...
}

encodedresource的作用是对资源文件的编码进行处理的,其中的主要逻辑体现在getreader()方法中,当设置了编码属性的时候spring会使用相应的编码作为输入流的编码,在构造好了encoderesource对象后,再次转入了可复用方法loadbeandefinitions(new encodedresource(resource)),这个方法内部才是真正的数据准备阶段,代码如下:

protected int doloadbeandefinitions(inputsource inputsource, resource resource)
        throws beandefinitionstoreexception {
    try {
        // 获取 document 实例
        document doc = doloaddocument(inputsource, resource);
        // 根据 document 实例****注册 bean信息
        return registerbeandefinitions(doc, resource);
    }
    ...
}

核心部分就是 try 块的两行代码。

  1. 调用 doloaddocument() 方法,根据 xml 文件获取 document 实例。
  2. 根据获取的 document 实例注册 bean 信息

其实在doloaddocument()方法内部还获取了 xml 文件的验证模式。如下:

protected document doloaddocument(inputsource inputsource, resource resource) throws exception {
    return this.documentloader.loaddocument(inputsource, getentityresolver(), this.errorhandler,
            getvalidationmodeforresource(resource), isnamespaceaware());
}

调用 getvalidationmodeforresource() 获取指定资源(xml)的验证模式。所以 doloadbeandefinitions()主要就是做了三件事情。

1. 调用 getvalidationmodeforresource() 获取 xml 文件的验证模式
2. 调用 loaddocument() 根据 xml 文件获取相应的 document 实例。
3. 调用 registerbeandefinitions() 注册 bean 实例。

获取xml的验证模式 

dtd和xsd区别 

dtd(document type definition)即文档类型定义,是一种xml约束模式语言,是xml文件的验证机制,属于xml文件组成的一部分。dtd是一种保证xml文档格式正确的有效方法,可以通过比较xml文档和dtd文件来看文档是否符合规范,元素和标签使用是否正确。一个dtd文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符合规则。
使用dtd验证模式的时候需要在xml文件的头部声明,以下是在spring中使用dtd声明方式的代码:

<?xml version="1.0" encoding="utf-8"?>
<!doctype beans public "-//spring//dtd bean 2.0//en" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">

xml schema语言就是xsd(xml schemas definition)。xml schema描述了xml文档的结构,可以用一个指定的xml schema来验证某个xml文档,以检查该xml文档是否符合其要求,文档设计者可以通过xml schema指定一个xml文档所允许的结构和内容,并可据此检查一个xml文档是否是有效的。

在使用xml schema文档对xml实例文档进行检验,除了要声明名称空间外(xmlns=http://www.springframework.org/schema/beans),还必须指定该名称空间所对应的xml schema文档的存储位置,通过schemalocation属性来指定名称空间所对应的xml schema文档的存储位置,它包含两个部分,一部分是名称空间的uri,另一部分就该名称空间所标识的xml schema文件位置或url地址(xsi:schemalocation=”http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd“),代码如下:

<?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="mytestbean" class="com.chenhao.spring.mytestbean"/>

</beans>

验证模式的读取 

在spring中,是通过getvalidationmodeforresource方法来获取对应资源的验证模式,其源码如下:

protected int getvalidationmodeforresource(resource resource) {
    int validationmodetouse = getvalidationmode();
    if (validationmodetouse != validation_auto) {
        return validationmodetouse;
    }
    int detectedmode = detectvalidationmode(resource);
    if (detectedmode != validation_auto) {
        return detectedmode;
    }
    // hmm, we didn't get a clear indication... let's assume xsd,
    // since apparently no dtd declaration has been found up until
    // detection stopped (before finding the document's root tag).
    return validation_xsd;
}

方法的实现还是很简单的,如果设定了验证模式则使用设定的验证模式(可以通过使用xmlbeandefinitionreader中的setvalidationmode方法进行设定),否则使用自动检测的方式。而自动检测验证模式的功能是在函数detectvalidationmode方法中,而在此方法中又将自动检测验证模式的工作委托给了专门处理类xmlvalidationmodedetector的validationmodedetector方法,具体代码如下:

public int detectvalidationmode(inputstream inputstream) throws ioexception {
    // peek into the file to look for doctype.
    bufferedreader reader = new bufferedreader(new inputstreamreader(inputstream));
    try {
        boolean isdtdvalidated = false;
        string content;
        while ((content = reader.readline()) != null) {
            content = consumecommenttokens(content);
            if (this.incomment || !stringutils.hastext(content)) {
                continue;
            }
            if (hasdoctype(content)) {
                isdtdvalidated = true;
                break;
            }
            if (hasopeningtag(content)) {
                // end of meaningful data...
                break;
            }
        }
        return (isdtdvalidated ? validation_dtd : validation_xsd);
    }
    catch (charconversionexception ex) {
        // choked on some character encoding...
        // leave the decision up to the caller.
        return validation_auto;
    }
    finally {
        reader.close();
    }
}

从代码中看,主要是通过读取 xml 文件的内容,判断内容中是否包含有 doctype ,如果是 则为 dtd,否则为 xsd,当然只会读取到 第一个 “<” 处,因为 验证模式一定会在第一个 “<” 之前。如果当中出现了 charconversionexception 异常,则为 xsd模式。

获取document

经过了验证模式准备的步骤就可以进行document加载了,对于文档的读取委托给了documentloader去执行,这里的documentloader是个接口,而真正调用的是defaultdocumentloader,解析代码如下:

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);
}

分析代码,首选创建documentbuildfactory,再通过documentbuilderfactory创建documentbuilder,进而解析inputsource来返回document对象。对于参数entityresolver,传入的是通过getentityresolver()函数获取的返回值,代码如下:

protected entityresolver getentityresolver() {
    if (this.entityresolver == null) {
        // determine default entityresolver to use.
        resourceloader resourceloader = getresourceloader();
        if (resourceloader != null) {
            this.entityresolver = new resourceentityresolver(resourceloader);
        }
        else {
            this.entityresolver = new delegatingentityresolver(getbeanclassloader());
        }
    }
    return this.entityresolver;
}

这个entityresolver是做什么用的呢,接下来我们详细分析下。 

entityresolver 的用法 

对于解析一个xml,sax首先读取该xml文档上的声明,根据声明去寻找相应的dtd定义,以便对文档进行一个验证,默认的寻找规则,即通过网络(实现上就是声明dtd的uri地址)来下载相应的dtd声明,并进行认证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的dtd声明没有被找到的原因.

entityresolver的作用是项目本身就可以提供一个如何寻找dtd声明的方法,即由程序来实现寻找dtd声明的过程,比如将dtd文件放到项目中某处,在实现时直接将此文档读取并返回给sax即可,在entityresolver的接口只有一个方法声明:

public abstract inputsource resolveentity (string publicid, string systemid)
    throws saxexception, ioexception;

它接收两个参数publicid和systemid,并返回一个inputsource对象,以特定配置文件来进行讲解 
(1)如果在解析验证模式为xsd的配置文件,代码如下:

<?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>

则会读取到以下两个参数 
- publicid:null 
- systemid:http://www.springframework.org/schema/beans/spring-beans.xsd 

spring源码深度解析— IOC 之 容器的基本实现

(2)如果解析验证模式为dtd的配置文件,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<!doctype beans public "-//spring//dtd bean 2.0//en" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
....
</beans>

读取到以下两个参数
- publicid:-//spring//dtd bean 2.0//en
- systemid:http://www.springframework.org/dtd/spring-beans-2.0.dtd

spring源码深度解析— IOC 之 容器的基本实现

一般都会把验证文件放置在自己的工程里,如果把url转换为自己工程里对应的地址文件呢?以加载dtd文件为例来看看spring是如何实现的。根据之前spring中通过getentityresolver()方法对entityresolver的获取,我们知道,spring中使用delegatingentityresolver类为entityresolver的实现类,resolveentity实现方法如下:

@override
@nullable
public inputsource resolveentity(string publicid, @nullable string systemid) throws saxexception, ioexception {
    if (systemid != null) {
        if (systemid.endswith(dtd_suffix)) {
            return this.dtdresolver.resolveentity(publicid, systemid);
        }
        else if (systemid.endswith(xsd_suffix)) {
            return this.schemaresolver.resolveentity(publicid, systemid);
        }
    }
    return null;
}

不同的验证模式使用不同的解析器解析,比如加载dtd类型的beansdtdresolver的resolveentity是直接截取systemid最后的xx.dtd然后去当前路径下寻找,而加载xsd类型的pluggableschemaresolver类的resolveentity是默认到meta-inf/spring.schemas文件中找到systemid所对应的xsd文件并加载。 beansdtdresolver 的解析过程如下:

public inputsource resolveentity(string publicid, @nullable string systemid) throws ioexception {
    if (logger.istraceenabled()) {
        logger.trace("trying to resolve xml entity with public id [" + publicid +
                "] and system id [" + systemid + "]");
    }
    if (systemid != null && systemid.endswith(dtd_extension)) {
        int lastpathseparator = systemid.lastindexof('/');
        int dtdnamestart = systemid.indexof(dtd_name, lastpathseparator);
        if (dtdnamestart != -1) {
            string dtdfile = dtd_name + dtd_extension;
            if (logger.istraceenabled()) {
                logger.trace("trying to locate [" + dtdfile + "] in spring jar on classpath");
            }
            try {
                resource resource = new classpathresource(dtdfile, getclass());
                inputsource source = new inputsource(resource.getinputstream());
                source.setpublicid(publicid);
                source.setsystemid(systemid);
                if (logger.isdebugenabled()) {
                    logger.debug("found beans dtd [" + systemid + "] in classpath: " + dtdfile);
                }
                return source;
            }
            catch (ioexception ex) {
                if (logger.isdebugenabled()) {
                    logger.debug("could not resolve beans dtd [" + systemid + "]: not found in classpath", ex);
                }
            }
        }
    }
    return null;
}

从上面的代码中我们可以看到加载 dtd 类型的 beansdtdresolver.resolveentity() 只是对 systemid 进行了简单的校验(从最后一个 / 开始,内容中是否包含 spring-beans),然后构造一个 inputsource 并设置 publicid、systemid,然后返回。 pluggableschemaresolver 的解析过程如下:

public inputsource resolveentity(string publicid, @nullable string systemid) throws ioexception {
    if (logger.istraceenabled()) {
        logger.trace("trying to resolve xml entity with public id [" + publicid +
                "] and system id [" + systemid + "]");
    }

    if (systemid != null) {
        string resourcelocation = getschemamappings().get(systemid);
        if (resourcelocation != null) {
            resource resource = new classpathresource(resourcelocation, this.classloader);
            try {
                inputsource source = new inputsource(resource.getinputstream());
                source.setpublicid(publicid);
                source.setsystemid(systemid);
                if (logger.isdebugenabled()) {
                    logger.debug("found xml schema [" + systemid + "] in classpath: " + resourcelocation);
                }
                return source;
            }
            catch (filenotfoundexception ex) {
                if (logger.isdebugenabled()) {
                    logger.debug("couldn't find xml schema [" + systemid + "]: " + resource, ex);
                }
            }
        }
    }
    return null;
}

首先调用 getschemamappings() 获取一个映射表(systemid 与其在本地的对照关系),然后根据传入的 systemid 获取该 systemid 在本地的路径 resourcelocation,最后根据 resourcelocation 构造 inputsource 对象。 映射表如下(部分):

 spring源码深度解析— IOC 之 容器的基本实现

解析及注册beandefinitions

当把文件转换成document后,接下来就是对bean的提取及注册,当程序已经拥有了xml文档文件的document实例对象时,就会被引入到xmlbeandefinitionreader.registerbeandefinitions这个方法:

public int registerbeandefinitions(document doc, resource resource) throws beandefinitionstoreexception {
    beandefinitiondocumentreader documentreader = createbeandefinitiondocumentreader();
    int countbefore = getregistry().getbeandefinitioncount();
    documentreader.registerbeandefinitions(doc, createreadercontext(resource));
    return getregistry().getbeandefinitioncount() - countbefore;
}

其中的doc参数即为上节读取的document,而beandefinitiondocumentreader是一个接口,而实例化的工作是在createbeandefinitiondocumentreader()中完成的,而通过此方法,beandefinitiondocumentreader真正的类型其实已经是defaultbeandefinitiondocumentreader了,进入defaultbeandefinitiondocumentreader后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续beandefinition的注册,如下代码:

public void registerbeandefinitions(document doc, xmlreadercontext readercontext) {
    this.readercontext = readercontext;
    logger.debug("loading bean definitions");
    element root = doc.getdocumentelement();
    doregisterbeandefinitions(root);
}

通过这里我们看到终于到了解析逻辑的核心方法doregisterbeandefinitions,接着跟踪源码如下:

protected void doregisterbeandefinitions(element root) {
    beandefinitionparserdelegate parent = this.delegate;
    this.delegate = createdelegate(getreadercontext(), root, parent);
    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;
}

我们看到首先要解析profile属性,然后才开始xml的读取,具体的代码如下:

protected void parsebeandefinitions(element root, beandefinitionparserdelegate delegate) {
    if (delegate.isdefaultnamespace(root)) {
        nodelist nl = root.getchildnodes();
        for (int i = 0; i < nl.getlength(); i++) {
            node node = nl.item(i);
            if (node instanceof element) {
                element ele = (element) node;
                if (delegate.isdefaultnamespace(ele)) {
                    parsedefaultelement(ele, delegate);
                }
                else {
                    delegate.parsecustomelement(ele);
                }
            }
        }
    }
    else {
        delegate.parsecustomelement(root);
    }
}

最终解析动作落地在两个方法处:parsedefaultelement(ele, delegate) 和 delegate.parsecustomelement(root)。我们知道在 spring 有两种 bean 声明方式:

  • 配置文件式声明:<bean id="mytestbean" class="com.chenhao.spring.mytestbean"/>
  • 自定义注解方式:<tx:annotation-driven>

两种方式的读取和解析都存在较大的差异,所以采用不同的解析方法,如果根节点或者子节点采用默认命名空间的话,则调用 parsedefaultelement() 进行解析,否则调用 delegate.parsecustomelement() 方法进行自定义解析。

而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getnamespaceuri()获取命名空间,并与spring中固定的命名空间http://www.springframework.org/schema/beans进行对比,如果一致则认为是默认,否则就认为是自定义。 

推荐博客

  

profile的用法 

通过profile标记不同的环境,可以通过设置spring.profiles.active和spring.profiles.default激活指定profile环境。如果设置了active,default便失去了作用。如果两个都没有设置,那么带有profiles的bean都不会生成。 

配置spring配置文件最下面配置如下beans

<!-- 开发环境配置文件 -->
<beans profile="development">
    <context:property-placeholder
            location="classpath*:config_common/*.properties, classpath*:config_development/*.properties"/>
</beans>

<!-- 测试环境配置文件 -->
<beans profile="test">
    <context:property-placeholder
            location="classpath*:config_common/*.properties, classpath*:config_test/*.properties"/>
</beans>

<!-- 生产环境配置文件 -->
<beans profile="production">
    <context:property-placeholder
            location="classpath*:config_common/*.properties, classpath*:config_production/*.properties"/>
</beans>

配置web.xml

<!-- 多环境配置 在上下文context-param中设置profile.default的默认值 -->
<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>production</param-value>
</context-param>

<!-- 多环境配置 在上下文context-param中设置profile.active的默认值 -->
<!-- 设置active后default失效,web启动时会加载对应的环境信息 -->
<context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>test</param-value>
</context-param>

这样启动的时候就可以按照切换spring.profiles.active的属性值来进行切换了。

上一篇:

下一篇: