引言

我们经常会看到ClassLoader和Class的getResource方法,它们两个存在相互调用关系,并且路径问题也是一个难点,今天我们来看一下这两个方法的联系和区别。

ClassLoader的getResource方法

方法定义

  1. /**
  2. * Finds the resource with the given name. A resource is some data
  3. * (images, audio, text, etc) that can be accessed by class code in a way
  4. * that is independent of the location of the code.
  5. *
  6. * <p> The name of a resource is a '<tt>/</tt>'-separated path name that
  7. * identifies the resource.
  8. *
  9. * <p> This method will first search the parent class loader for the
  10. * resource; if the parent is <tt>null</tt> the path of the class loader
  11. * built-in to the virtual machine is searched. That failing, this method
  12. * will invoke {@link #findResource(String)} to find the resource. </p>
  13. *
  14. * @apiNote When overriding this method it is recommended that an
  15. * implementation ensures that any delegation is consistent with the {@link
  16. * #getResources(java.lang.String) getResources(String)} method.
  17. *
  18. * @param name
  19. * The resource name
  20. *
  21. * @return A <tt>URL</tt> object for reading the resource, or
  22. * <tt>null</tt> if the resource could not be found or the invoker
  23. * doesn't have adequate privileges to get the resource.
  24. *
  25. * @since 1.1
  26. */
  27. public URL getResource(String name) {
  28. URL url;
  29. if (parent != null) {
  30. url = parent.getResource(name);
  31. } else {
  32. url = getBootstrapResource(name);
  33. }
  34. if (url == null) {
  35. url = findResource(name);
  36. }
  37. return url;
  38. }

这个方法根据指定的name来查找资源,name是一个以/分隔的路径名,这个路径用来标识这个资源。首先,它会判断当前的ClassLoader是否有parent,如果有,就让parent来加载这个资源,如果没有parent,就会让jvm内置的类加载器来加载这个资源,如果还是没找到,就会调用findResource方法来获取。我们来看findResource方法:

  1. protected URL findResource(String name) {
  2. return null;
  3. }

这个方法返回null,看来需要ClassLoader自己去实现。

路径问题

首先,如果是相对路径的话,就是相对于classpath的路径。在maven工程打包完成后,classpath就是classes文件夹这个位置。
image.png
我们来看一个例子,加入一个当前的工程结构如下:

image.png
在resources下面有一个文件TestClassLoader.txt,我们打包完成后,它就会出现在classes这个文件夹下(与application.properties一样),那么我们怎么在TestGetResource这个类中使用getResource方法拿到这个资源呢?很简单:

  1. public class TestGetResource {
  2. public static void main(String[] args) {
  3. ClassLoader classLoader = ClassLoader.getSystemClassLoader();
  4. System.out.println(classLoader);
  5. URL resource = classLoader.getResource("TestClassLoader.txt");
  6. System.out.println(resource);
  7. }
  8. }

只需要通过相对路径来,参数直接就是TestClassLoader.txt,因为打包之后的这个文件在classes文件夹下,就是classpath下,所以直接根据相对路径来就行,输出:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. file:/Users/cuihualong/develop/code/SkyWalking/consumer/target/classes/TestClassLoader.txt

那如果这种情况呢:
image.png
TestClassLoader.txt在templates文件夹下,获取也很简单,还是通过相对路径,直接加上templates这一层即可:

  1. public class TestGetResource {
  2. public static void main(String[] args) {
  3. ClassLoader classLoader = ClassLoader.getSystemClassLoader();
  4. System.out.println(classLoader);
  5. URL resource = classLoader.getResource("templates/TestClassLoader.txt");
  6. System.out.println(resource);
  7. }
  8. }

输出:
image.png
我们可以这样来看classLoader的getResource方法的路径是相对哪个路径的,也就是根路径:

  1. public static void main(String[] args) {
  2. ClassLoader classLoader = ClassLoader.getSystemClassLoader();
  3. System.out.println(classLoader);
  4. URL rootResource = classLoader.getResource("");
  5. System.out.println(rootResource);
  6. }

输出:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. file:/Users/cuihualong/develop/code/SkyWalking/consumer/target/classes/

可以看到就是classes也就是classpath。

Class的getResource方法

方法定义

  1. public java.net.URL getResource(String name) {
  2. name = resolveName(name);
  3. ClassLoader cl = getClassLoader0();
  4. if (cl==null) {
  5. // A system class.
  6. return ClassLoader.getSystemResource(name);
  7. }
  8. return cl.getResource(name);
  9. }

首先,它调用了resolveName,这个方法很重要,它修改了name的值,然后调用的是classLoader的getResource方法,根据之前对getResource方法的理解,resolveName这个方法要做的应该是把name改为ClassLoader的getResource方法可以理解的路径,我们来看它是怎样修改的:

使用resolveName方法对路径进行修改

  1. private String resolveName(String name) {
  2. if (name == null) {
  3. return name;
  4. }
  5. if (!name.startsWith("/")) {
  6. Class<?> c = this;
  7. while (c.isArray()) {
  8. c = c.getComponentType();
  9. }
  10. String baseName = c.getName();
  11. int index = baseName.lastIndexOf('.');
  12. if (index != -1) {
  13. name = baseName.substring(0, index).replace('.', '/')
  14. +"/"+name;
  15. }
  16. } else {
  17. name = name.substring(1);
  18. }
  19. return name;
  20. }

首先,如果name以/开头,就会去掉/,保留/后面的路径,这样就成为了一个相对路径,而ClassLoader中相对路径就是相对于classpath的路径,所以,如果用class的getResource方法来获取上面的TestClassLoader.txt,可以这样写:

  1. public class TestGetResource {
  2. public static void main(String[] args) {
  3. URL resource = TestGetResource.class.getResource("/templates/TestClassLoader.txt");
  4. System.out.println(resource);
  5. }
  6. }

它传给ClassLoader的getResource方法的name就是去掉/之后的template/TestClassLoader.txt,输出:

  1. file:/Users/cuihualong/develop/code/SkyWalking/consumer/target/classes/templates/TestClassLoader.txt

这是Class的getResource的name以/开头的情况。
如果不是以/开头,也就是name本来就是一个相对路径,我们看是怎么处理的:

  1. if (!name.startsWith("/")) {
  2. Class<?> c = this;
  3. while (c.isArray()) {
  4. c = c.getComponentType();
  5. }
  6. String baseName = c.getName();
  7. int index = baseName.lastIndexOf('.');
  8. if (index != -1) {
  9. name = baseName.substring(0, index).replace('.', '/')
  10. +"/"+name;
  11. }
  12. }

它是获取了当前类的完整包路径。例如我们这样写:

  1. public static void main(String[] args) {
  2. URL resource = TestGetResource.class.getResource("templates/TestClassLoader.txt");
  3. System.out.println(resource);
  4. }

注意,这样是取不到资源的,只是为了展示一下路径不包含/时的逻辑,我们在resolveName方法上打上断点,来看一些name的值:
image.png
可以看到,它会在你给到的name前面加上class所代表的类也就是TestGetResource所在的包路径。也就是说,当你name是不带/也就是相对路径时,resolveName会认为你的这个相对路径是相对于class所代表的的类的相对路径,它要把它转化为相对与classpath的路径,所以就会加上包路径。所以这样写的话,我们就找不到资源了。
从上面resolveName的两种处理方式我们可以看出,经过这个方法处理后的路径总是不带/的,也就是相对路径,并且是相对classpath的路径,这样就能传给ClassLoader的getResource方法了。
知道了resolveName方法的处理规则,我们就能轻松地写出正确的路径了。

getResourceAsStream方法

ClassLoader类还有一个方法我们也经常用到,它与getResource方法关系密切,就是getResourceAsStream方法,它的实现很简单:

  1. public InputStream getResourceAsStream(String name) {
  2. URL url = getResource(name);
  3. try {
  4. return url != null ? url.openStream() : null;
  5. } catch (IOException e) {
  6. return null;
  7. }
  8. }

首先调用了getResource方法来获取URL,然后直接打开这个URL。这里就不做过多解释了。