简介
Java中获取资源的最常用的2中方式就是使用Class的getResource和使用ClassLoader的getResource方法,当然还有它们相关的方法。这里就介绍一下使用这2中方式的区别,和它们搜索使用的路径。 这里先说结论(hotspot): ClassLoader的getResource(name)方法会依次查找:
- 在"sun.boot.class.path"指定的路径问根目录下查找name资源
- 在"java.ext.dirs"指定的路径为根目录下查找name资源
- 在"java.class.path"指定的路径为根目录下查找name资源
- 利用ClassLoader(自定义的,重写了findResource)的findResource(name)获取URL
Class的getResource(name)方法是调用的ClassLoader的getResource(name)方法,但是它做了2点处理:
- 如果name以"/"开头,就把name中开头的"/"去掉,然后调优ClassLoader的getResource(name)方法。然后在ClassLoader的getResource(name)方法搜索方式搜索。
- 如果name不以"/"开头,那么就用Class的包名+name作为新的name来调用ClassLoader的getResource(name)方法。cn.freemethod.start.BaseName.class.getResource("config.properties")最终调用的就是ClassLoader的getResource("cn/freemethod/start/config.properties")可能ClassLoader的类型不同,但是不影响ClassLoader的getResource(name)搜索套路。(前提是没有破坏父类委托机制)
注意:这些路径包括了jar包中的资源,例如,你的classpath中包含了springmvc的jar包,你就可以通过下面的方式来加载DispatcherServlet.properties文件。
import java.net.URL;import org.junit.Test;public class ResourceTest { private static final String DISPATCHER_SERVLET_PROPERTIES_PKG = "org/springframework/web/servlet/DispatcherServlet.properties"; @Test public void testResource() { URL url = null; ClassLoader loader = ResourceTest.class.getClassLoader(); url = loader.getResource(DISPATCHER_SERVLET_PROPERTIES_PKG); System.out.println(url); url = ClassLoader.getSystemResource(DISPATCHER_SERVLET_PROPERTIES_PKG); System.out.println(url); }}
另一点值得注意的是在IDE中,比如Eclipse中运行的时候Eclipse常常是添加了新的classpath路径的,比如,如果是Java project工程,Eclipse就会把项目根目录下的bin目录添加到classpath中,如果是maven工程,Eclipse就会把项目根目录下的target目录下的classes和pom中的jar包加到classpath中,运行JUnit,Eclipse会把test-classes和pom中的jar包加到classpath中。 要知道指定的路径有没有在搜索路径中,可以输出3个系统属性看一下就可以了:
System.out.println(System.getProperty("java.class.path"));System.out.println(System.getProperty("sun.boot.class.path"));System.out.println(System.getProperty("java.ext.dirs"));
其实用的最多的还是"java.class.path",一般注意一下"java.class.path"这个就可以了,知道了上面的内容基本上遇到getResource相关的内容的问题基本都能解决了。接下来我们就通过代码来看一些细节的东西。
ClassLoader#getResource(String name)
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; }
上面就是ClassLoader的getResource方法了,看上去感觉逻辑很简单,其实还是比较绕的。如果没有破坏父类委托机制,那么调用栈中应该先通过getBootstrapResource(name)这个方法查找,为了专注与ClassLoader的getResource方法的逻辑,我们先不细说getBootstrapResource(name),我们先看一下它干了什么,getBootstrapResource(name)方法做的工作就是在"sun.boot.class.path"指定的路径和其他通过接口添加到启动类加载器搜索路径中查找name。一般我们把资源放在classpath路径下,所以,一般是查找不到的。那么接下来执行到的将是getBootstrapResource(name)方法,层层委托先执行启动类加载器的getBootstrapResource(name),然后扩展类加载器的getBootstrapResource(name),然后系统类加载器的getBootstrapResource(name),最后自定义类加载器的getBootstrapResource(name)(这里假设的使用的自定义或者系统类加载器,如果不是,就不搜索下层的加载路径)。扩展类加载器的getBootstrapResource(name)干的事情就是在"java.ext.dirs"指定的路径和其他通过接口添加到扩展类加载器搜索路径中查找name。系统类加载器的getBootstrapResource(name)干的事情就是在"java.class.path"指定的路径和其他通过接口添加到系统类加载器搜索路径中查找name。 如果有兴趣的朋友可以看一下Launcher,URLClassLoader,URLClassPath这3个类的源码,如果深入一点还可以看一下MetaIndex缓存、URLStreamHandler相关的类等。
public EnumerationgetResources(String name) throws IOException { Enumeration[] tmp = new Enumeration[2]; if (parent != null) { tmp[0] = parent.getResources(name); } else { tmp[0] = getBootstrapResources(name); } tmp[1] = findResources(name); return new CompoundEnumeration<>(tmp); }
ClassLoader的getResource是找到一个就不找了,而getResources把所有搜索路径中能找到的name资源都找出来。
ClassLoader提供的一下静态的查找资源方法
public static URL getSystemResource(String name) { ClassLoader system = getSystemClassLoader(); if (system == null) { return getBootstrapResource(name); } return system.getResource(name); }
这个方法其实还是调用的还是按ClassLoader的套路来查找,不过调用的加载器已经确定了是系统类加载器(AppclassLoader)而已。
private static URL getBootstrapResource(String name) { URLClassPath ucp = getBootstrapClassPath(); Resource res = ucp.getResource(name); return res != null ? res.getURL() : null; }
只在"sun.boot.class.path"路径下查找name资源。
static URLClassPath getBootstrapClassPath() { return sun.misc.Launcher.getBootstrapClassPath(); }
public static EnumerationgetSystemResources(String name) throws IOException { ClassLoader system = getSystemClassLoader(); if (system == null) { return getBootstrapResources(name); } return system.getResources(name); }
和getSystemResource(String name)差不多,只是查找搜索路径下所有的name资源而已。
private static EnumerationgetBootstrapResources(String name) throws IOException { final Enumeration e = getBootstrapClassPath().getResources(name); return new Enumeration () { public URL nextElement() { return e.nextElement().getURL(); } public boolean hasMoreElements() { return e.hasMoreElements(); } }; }
和getBootstrapResource(String name),只是查找"sun.boot.class.path"下所有的name资源而已。
Class#getResource(String name)
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); }
我们一般使用Class的获取资源的时候都是使用的我们自定义的类,所以ClassLoader一般都是系统类加载器或者我们自定义的类加载器,所以getResource(String name)基本上就是按照ClassLoader的基本的套路来搜索的。最常见的用法就是加载和Class在同一个包下的资源,这样就不用拼接路径了。Class#getResource("/xxx"),加上"/"这个样的方式基本上就和ClassLoader#getSystemResource(String name)这个静态方法差不多了。
Class#resolveName(String 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 name = baseName.substring(0, index).replace('.', '/') +"/"+name; } } else { name = name.substring(1); } return name; }