一般在查找资源时,可根据本地路径和网络路径来定位具体的资源文件。其中本地路径又可分为绝对路径和相对路径。Java在查找资源时的最佳实践是通过相对路径来确定资源,java的类装载器ClassLoader内部包装了相对路径到绝对路径的转化实现。对于任意一个自定义类,可通过其Class类的getResource()方法获取到URL形式的资源绝对路径。

        在具体的实践过程中,碰到一个小问题。测试用例如下:

工程结构:

Java-learning

       |---- src/main/java

       |---- src/test/java

       |---- src/main/resources/META-INF/spring

       |---- src/main/ resources

       |---- 引用的jar包和maven的pom.xml等

测试代码包路径:

src/main/java

       |---- com.lee.java.learning

              |---- compiler

                     |---- Test.java

              |---- AnClass.java

测试代码:

AnClass.java

package com.lee.java.learning;

public class AnClass {
	public static void main(String[] args) {
	}
}

Test.java

package com.lee.java.learning.compiler;

import java.net.URL;

public class Test {

	public static void main(String[] args) {
		Test test = new Test();
		test.test();
	}

	public void test() {
		URL root = this.getClass().getResource("/");
		URL current1 = this.getClass().getResource("");
		URL current2 = this.getClass().getResource(".");
		URL parent = this.getClass().getResource("..");
		URL self1 = this.getClass().getResource("Test.class");
		URL self2 = this.getClass().getResource("./Test.class");
		URL brother = this.getClass().getResource("../AnClass.class");

		System.out.println("root = "+root);
		System.out.println("current1 = "+current1);
		System.out.println("current2 = "+current2);
		System.out.println("parent = "+parent);
		System.out.println("self1 = "+self1);
		System.out.println("self2 = "+self2);
		System.out.println("brother = "+brother);
	}

}

测试输出结果:

root = file:/D:/Java/workspace_1/java-learning/target/test-classes/
current1 = file:/D:/Java/workspace_1/java-learning/target/test-classes/com/lee/java/learning/compiler/
current2 = file:/D:/Java/workspace_1/java-learning/target/test-classes/com/lee/java/learning/compiler/
parent = file:/D:/Java/workspace_1/java-learning/target/test-classes/com/lee/java/learning/
self1 = file:/D:/Java/workspace_1/java-learning/target/classes/com/lee/java/learning/compiler/Test.class
self2 = file:/D:/Java/workspace_1/java-learning/target/classes/com/lee/java/learning/compiler/Test.class
brother = file:/D:/Java/workspace_1/java-learning/target/classes/com/lee/java/learning/AnClass.class

分析应该是classpath的问题。eclipse下调试程序,观察命令行参数:

E:\java6\bin\javaw.exe -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:4584 -Dfile.encoding=UTF-8 -classpath D:\java\eclipse\my_workspace\java-learning\target\test-classes;D:\java\eclipse\my_workspace\java-learning\target\classes;E:\working\maven_lib\commons-logging\commons-logging\1.1.1\commons-logging-1.1.1.jar;E:\working\maven_lib\junit\junit\4.7\junit-4.7.jar;E:\working\maven_lib\org\springframework\spring\2.5.6\spring-2.5.6.jar com.sdo.lee.java.learning.compiler.Test

再查看了一下工程的.classpath文件:

<?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry kind="output" path="target/classes"/>
	<classpathentry including="**/*.java" kind="src" path="src/main/java"/>
	<classpathentry including="**/*.java" kind="src" output="target/test-classes" path="src/test/java"/>
	<classpathentry excluding="**/*.java" kind="src" path="src/main/resources/META-INF/spring"/>
	<classpathentry excluding="META-INF/spring/|**/*.java" kind="src" path="src/main/resources"/>
	<classpathentry kind="var" path="M2_REPO/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1.jar"/>
	<classpathentry kind="var" path="M2_REPO/junit/junit/4.7/junit-4.7.jar"/>
	<classpathentry kind="var" path="M2_REPO/org/springframework/spring/2.5.6/spring-2.5.6.jar"/>
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
</classpath>

发现更改上面的其中一条<classpathentry>如下,即可得到预期结果。

<classpathentry including="**/*.java" kind="src" output="target/classes"

同时,更改后的eclipse命令行参数如下:

E:\java6\bin\javaw.exe -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:4785 -Dfile.encoding=UTF-8 -classpath D:\java\eclipse\my_workspace\java-learning\target\classes;D:\java\eclipse\my_workspace\java-learning\target\test-classes;E:\working\maven_lib\commons-logging\commons-logging\1.1.1\commons-logging-1.1.1.jar;E:\working\maven_lib\junit\junit\4.7\junit-4.7.jar;E:\working\maven_lib\org\springframework\spring\2.5.6\spring-2.5.6.jar com.sdo.lee.java.learning.compiler.Test

eclipse在运行时是如何根据.classpath文件来添加命令行参数的-classpath属性不得而知。

(初步实践发现:eclipse可能是逐条读取classpathentry,一旦发现指定output属性,即将其path属性指定的路径添加到命令行参数的-classpath属性,并在最后将kind="output"指定的的默认path属性路径也添加到-classpath属性中。至于是否正确,有待验证。)

对于相对路径的解析('/','.','..',''等),java的源代码如下:

Class.class

public java.net.URL getResource(String name) {
        name = resolveName(name);
        ClassLoader cl = getClassLoader0();
        if (cl==null) {
            // A system class.
            return ClassLoader.getSystemResource(name);
        }
        return cl.getResource(name);
    }
private String resolveName(String name) {
        if (name == null) {
            return name;
        }
        if (!name.startsWith("/")) {
            Class c = this;
            while (c.isArray()) {
                c = c.getComponentType();
            }
            String baseName = c.getName();
            int index = baseName.lastIndexOf('.');
            if (index != -1) {
                name = baseName.substring(0, index).replace('.', '/')
                    +"/"+name;
            }
        } else {
            name = name.substring(1);
        }
        return name;
    }

ClassLoader.class

public URL getResource(String name) {
	URL url;
	if (parent != null) {
	    url = parent.getResource(name);
	} else {
	    url = getBootstrapResource(name);
	}
	if (url == null) {
	    url = findResource(name);
	}
	return url;
    }
public static URL getSystemResource(String name) {
	ClassLoader system = getSystemClassLoader();
	if (system == null) {
	    return getBootstrapResource(name);
	}
	return system.getResource(name);
    }
protected URL findResource(String name) {
	return null;
    }
private static URL getBootstrapResource(String name) {
        try {
            // If this is a known JRE resource, ensure that its bundle is 
            // downloaded.  If it isn't known, we just ignore the download
            // failure and check to see if we can find the resource anyway
            // (which is possible if the boot class path has been modified).
            sun.jkernel.DownloadManager.getBootClassPathEntryForResource(name);
        } catch (NoClassDefFoundError e) {
            // This happens while Java itself is being compiled; DownloadManager
            // isn't accessible when this code is first invoked.  It isn't an
            // issue, as if we can't find DownloadManager, we can safely assume
            // that additional code is not available for download.
        }
	URLClassPath ucp = getBootstrapClassPath();
	Resource res = ucp.getResource(name);
	return res != null ? res.getURL() : null;
    }

从上面的源代码可以看到,Class类的getResource方法先是进行一个简单的路径解析,然后将其委托给其装载器去处理,装载器则是委托其父装载器处理,一直到bootstrap装载器。bootstrap装载器是非java实现的,sun没有开源,具体如何实现不得可知,但有一点可以肯定的是根据classpath来进行装载。java的官方文档说明中讲到:classloader装载类时的优先级(-jar命令参数 > -classpath命令参数 > CLASSPATH环境变量 > 默认的'.'指定的当前路径)。详细可参考官方文档How Classes are Found一文。

在相对路径解析时,我们发现所有的相对路径是以'/'指定的.class文件根目录来进行解析。观察上面的eclipse命令行参数猜想:类装载器默认将-classpath参数指定的第一个目录作为'/'指定的根目录,即getResource('/')返回的结果,而一旦/后面跟有具体的文件时(如'./Test.class'),则会顺序遍历classpath参数指定的所有路径,找到相应的Test.class文件,返回其绝对路径的URL,如有多个,则返回第一个找到的Test.class文件的路径。

为求证,在纯命令行环境下进行实践,实践结果与猜想的一致。

总结:对于这种采用构建工具进行构建的项目,往往指定有多个.class文件输出目录(对于只有一个.class文件输出目录时,不存在本文开始处描述的问题),因此在采用getResource(String name)之类的涉及到相对路径的方法调用,要稍加注意,有时候结果可能与预期出现偏差。