天天看点

深入Spring IOC源码之Resource

在java中,将不同来源的资源抽象成url,通过注册不同的handler(urlstreamhandler)来处理不同来源的资源的读取逻辑,一般handler的类型使用不同前缀(协议,protocol)来识别,如“file:”、“http:”、“jar:”等,然而url没有默认定义相对classpath或servletcontext等资源的handler,虽然可以注册自己的urlstreamhandler来解析特定的url前缀(协议),比如“classpath:”,然而这需要了解url的实现机制,而且url也没有提供一些基本的方法,如检查当前资源是否存在、检查当前资源是否可读等方法。因而spring对其内部使用到的资源实现了自己的抽象结构:resource接口来封装底层资源:

public interface inputstreamsource {

    inputstream getinputstream() throws ioexception;

}

public interface resource extends inputstreamsource {

    boolean exists();

    boolean isreadable();

    boolean isopen();

    url geturl() throws ioexception;

    uri geturi() throws ioexception;

    file getfile() 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等。首先,它定义了三个判断当前资源状态的方法:存在性(exists)、可读性(isreadable)、是否处于打开状态(isopen)。在c语言中,当我们拿到一个文件句柄时,我们要调用open方法打开文件才可以真正读取该文件,但是在java中并没有显示的定义open方法,一般当我们创建一个inputstream、reader时,该资源(文件)就已经处于打开状态了,因而这里的isopen方法并不是判断当前资源是否已经处于打开的可操作状态,这里是表示resource接口所抽象的底层资源是否可以多次调用getinputstream()方法,如果该方法返回true,则不可以多次调用getinputstream()方法。在spring 2.5.6的实现中,只有inputstreamresource类的isopen()方法返回true,其余都返回false。

另外,resource接口还提供了不同资源到url、uri、file类型的转换,以及获取lastmodified属性、文件名(不带路径信息的文件名,getfilename())的方法。为了便于操作,resource还提供了基于当前资源创建一个相对资源的方法:createrelative();在错误处理中需要详细的打印出错的资源文件,因而resource还提供了getdescription()方法用于在错误处理中的打印信息。

在spring 2.5.6中,所有实现resource的接口类继承关系图如下:

深入Spring IOC源码之Resource

即对不同来源的资源文件都有相应的resource实现:文件(filesystemresource)、classpath资源(classpathresource)、url资源(urlresource)、inputstream资源(inputstreamresource)、byte数组(bytearrayresource)等。

abstractresource是对resource的基本实现,所有resource实现类都继承了该类,所有继承该类的resource一般只需要实现以下方法即可:

public file getfile() throws ioexception

public url geturl() throws ioexception

public string getdescription()

public inputstream getinputstream() throws ioexception

该类默认实现中,将tostring、equals、hashcode都代理给description属性;isreadable总是返回true,而isopen总是返回false;exists方法实现中,先调用getfile返回的file对象的exists方法,如果失败,查看是否可以获得inputstream,如果可以,返回true,否则,返回false;geturl和getfile、createrelative方法抛出filenotfoundexception,而geturi则代理给geturl方法。

bytearrayresource是一个简单的resource实现,它是对二进制数组的封装,每次调用getinputstream时都会以这个二进制数组作为源创建一个bytearrayinputstream。它的exists方法总是返回true,而且重写了equals、hashcode的方法,以判断二进制数组的内容;它的description属性可以是用户自定义,也可以使用默认值:resource loaded from byte array

public final byte[] getbytearray() {

    return this.bytearray;

public boolean exists() {

    return true;

public inputstream getinputstream() throws ioexception {

    return new bytearrayinputstream(this.bytearray);

filesystemresource是对file的封装,在构建filesystemresource时可以传入file对象或路径字符串(这里的路径可以是相对路径,相对路径是相对于system.getproperty(“user.dir”)的值所在的路径,也可以是绝对路径,也可以是“file:”开头的路径值),在内部会创建相应的file对象,并且计算其path值,这里的path是计算完“.”和“..”影响的值(规格化)。

在getinputstream方法中,使用该file对象创建fileinputstream;而path值作为description属性、equals、hashcode等方法的实现;所有其他方法(exists、isreadable、geturl等)都代理给file对象;createrelative方法中使用path计算相对路径,其算法是:找到最后一个路径分隔符(/),将相对路径添加到该分隔符之后,传入的相对路径可以是以路径分割符(/)开头,也可以不以分隔符(/)开头,他们的效果是一样的,对相对路径存在的“.”和“..”会在创建filesystemresource类时处理。最后,当使用将一个目录的file对象构建filesystemresource时,调用createrelative方法,其相对路径的父目录和当前filesystemresource的父目录相同,比如使用”/home/levin/dir1”目录创建filesystemresource对象,该resource对象调用createrelative,并传入”file”,那么出现的结果为”/home/levin/file”,如果要得到”/home/levin/dir1/file”,那么构建filesystemresource时,应该传入”/home/levin/dir1/”字符串。

public boolean isreadable() {

    return (this.file.canread() && !this.file.isdirectory());

    return new fileinputstream(this.file);

public resource createrelative(string relativepath) {

    string pathtouse = stringutils.applyrelativepath(this.path, relativepath);

    return new filesystemresource(pathtouse);

public string getdescription() {

    return "file [" + this.file.getabsolutepath() + "]";

urlresource是对url和uri的封装。在构建urlresource时可以传入url、uri和path字符串(带协议字符串,如”file:”)。在urlresource内部还会创建一个cleanedurl,它是规格化(计算“.”和“..”后的值),该url将会用于equals、hashcode方法的实现。

在getinputstream方法实现中,它使用url.openconnection()方法获取urlconnection,后调用该urlconnection的getinputstream方法。对getfile()方法,只支持文件系统的资源,即url字符串的协议部分为”file:”。urlresource还支持从jar、zip、vfszip、wsjar等内部文件,以jar为例,这些文件的字符串表达为:jar:file:/<jarpath>/jarfile.jar!/<filepath>/filename,如jar:file:/e:/program%20files/eclipse-juno/plugins/org.junit_4.10.0.v4_10_0_v20120426-0900/junit.jar!/org/junit/test.class,然而这些内部文件本身并没有lastmodified的属性,因而对这些内部文件,urlresource将jar、zip等文件的lastmodified视为这些内部文件的lastmodified属性。对createrelative方法,直接使用url提供的构造函数,忽略传入的relativepath中的路径分隔符“/”。

    urlconnection con = this.url.openconnection();

    con.setusecaches(false);

    return con.getinputstream();

protected file getfileforlastmodifiedcheck() throws ioexception {

    if (resourceutils.isjarurl(this.url)) {

        url actualurl = resourceutils.extractjarfileurl(this.url);

        return resourceutils.getfile(actualurl);

    }

    else {

        return getfile();

public resource createrelative(string relativepath) throws malformedurlexception {

    if (relativepath.startswith("/")) {

        relativepath = relativepath.substring(1);

    return new urlresource(new url(this.url, relativepath));

对classpath下资源的封装,或者说是对classloader.getresource()方法或class.getresource()方法的封装。它支持在当前classpath中读取资源文件。可以传入相对classpath的文件全路径名和classloader构建classpathresource,或忽略classloader采用默认classloader(即thread context classloader),此时在getinputstream()方法实现时时会使用classloader.getresourceasstream()方法,由于使用classloader获取资源时默认相对于classpath的根目录,因而构造函数会忽略开头的“/”字符。classpathresource还可以使用文件路径和class作为参数构建,此时若文件路径以“/”开头,表示该文件为相对于classpath的绝对路径,否则为相对class实例的相对路径,在getinputstream()方法实现时使用class.getresourceasstream()方法。

getfile()方法只支持存在于文件系统中的资源;对lastmodified的属性,若是jar、zip等文件中的资源,则采用jar、zip文件本身的lastmodified属性;equals会同时判断path、classloader、clazz字段,而hashcode则只使用path。

    inputstream is = null;

    if (this.clazz != null) {

        is = this.clazz.getresourceasstream(this.path);

        is = this.classloader.getresourceasstream(this.path);

    if (is == null) {

        throw new filenotfoundexception(

                getdescription() + " cannot be opened because it does not exist");

    return is;

public url geturl() throws ioexception {

    url url = null;

        url = this.clazz.getresource(this.path);

        url = this.classloader.getresource(this.path);

    if (url == null) {

                getdescription() + " cannot be resolved to url because it does not exist");

    return url;

public file getfile() throws ioexception {

    return resourceutils.getfile(geturl(), getdescription());

    url url = geturl();

    if (resourceutils.isjarurl(url)) {

        url actualurl = resourceutils.extractjarfileurl(url);

        return resourceutils.getfile(url, getdescription());

    return new classpathresource(pathtouse, this.classloader, this.clazz);

inputstreamresource是对inputstream的封装,它接收inputstream作为构造函数参数,它的isopen总是返回true,并且只能被读取一次(即getinputstream方法只能被调用一次),exists、isreadable方法也总是返回true。由于它不能被多次读取,只有当不用多次读取的时候才使用该类,并且只有当没有其他可用resource类时才使用该类。在spring内部貌似没有使用它。它只实现了getinputstream方法:

public inputstream getinputstream() throws ioexception, illegalstateexception {

    if (this.read) {

        throw new illegalstateexception("inputstream has already been read - " +

                "do not use inputstreamresource if a stream needs to be read multiple times");

    this.read = true;

    return this.inputstream;

descriptiveresource是对非物理资源的description的封装。它实现了getdescription()方法。resource中description属性主要用于错误处理时能更加准确的打印出错位置的信息,descriptiveresource提供对那些需要提供resource接口中的description属性作为错误打印信息的方法自定义的描述信息。比如在beandefinitionreader中,在仅仅使用inputsource作为源加载beandefinition时,就可以使用descriptiveresource定义自己的description,从而在出错信息中可以方便的知道问题源在哪里。

在spring中resource可以用于非物理资源的抽,beandefinitionresource是对beandefinition的封装。beandefinitionresource类似descriptiveresource,它也只实现了getdescription()方法,用于在解析某个beandefinition出错时显示错误源信息:

    return "beandefinition defined in " + this.beandefinition.getresourcedescription();

在spring中还定义了contextresource接口,继承自resource接口,只包含一个方法:

public interface contextresource extends resource {

    string getpathwithincontext();

getpathwithcontext()方法相对于context的路径,如servletcontext、portletcontext、classpath、filesystem等,在spring core中它有两个实现类filesystemcontextresource、classpathcontextresource,他们分别是filesystemresourceloader和defaultresourceloader中的内部类,他们对getpathwithcontext()方法的实现只是简单的返回path值。

另外,在spring web模块中,有一个servletcontextresource实现类,它使用servletcontext和path作为参数构造,getinputstream、geturl、geturi、getfile等方法中将实现代理给servletcontext,其中getpathwithcontext方法依然返回path字符串:

    try {

        url url = this.servletcontext.getresource(this.path);

        return (url != null);

    catch (malformedurlexception ex) {

        return false;

    inputstream is = this.servletcontext.getresourceasstream(this.path);

        throw new filenotfoundexception("could not open " + getdescription());

    url url = this.servletcontext.getresource(this.path);

    string realpath = webutils.getrealpath(this.servletcontext, this.path);

    return new file(realpath);

    return new servletcontextresource(this.servletcontext, pathtouse);