核心类介绍

在使用 ClassLoader 加载资源时,必须会涉及到以下几个核心组件:

  1. Launcher
    1. Launcher.AppClassLoader ,App类加载器
    2. Launcher.ExtClassLoader ,Ext类加载器
    3. Launcher.BootClassPathHolder ,Boot类加载器
  2. URLClassLoader ,在 ClassLoader 基础上,增加了资源查找能力
  3. URLClassPath ,类路径对象
    1. URLClassPath.Loader
    2. URLClassPath.JarLoader ,用来处理Jar包类型的资源
    3. URLClassPath.FileLoader ,用来处理File类型的资源

点击查看【processon】

资源查找原理

资源初始化

我们通过类加载器查找资源时,所有的资源都是来源于类路径下的。常见的类路径有以下几个:

  1. sun.boot.class.pathBootClassLoader 负责的类路径
  2. java.ext.dirsExtClassLoader 负载的类路径
  3. java.class.pathAppClassLoader 负责的类路径。这个比较常见,就是我们通过 -classpath 指定的路径

这些类路径在各个类加载器中是以 URLClassPath 的形式存在,每个 URLClassPath 可以包含多个资源路径,每个资源路径都会有一个对应的加载器( Loader )。除此之外,它还有 lmaploaderspathurl 等重要的属性:

  • lmap :保存 URL 和对应加载器 Loader 的映射
  • loaders :保存 Loader 的列表,和 urls 的下标一一对应。 urls[0]Loaderloaders[0]
  • path :保存 URL 的列表,这个列表通常不会更改
  • urls :保存 URL 的列表,这个列表记录了还没有处理的 URL ,一开始数量是和 path 一样;但是在生成 Loader 时,会一个个被弹出来,主要是为了保证一个 urls 的下标对应 loaders 的下标。

URLClassPath 是在初始化各个类加载器的时候创建的,比如拿 ExtClassLoader 的构造器来说:

  1. // ExtClassLoader 继承 URLClassLoader
  2. public ExtClassLoader(File[] dirs) throws IOException {
  3. // arg1: java.ext.dirs 属性指定的路径
  4. // arg2: 父加载器,因为Ext之上是Boot,所以这里传null
  5. // arg3: 资源加载协议处理器工厂,用来创建对应协议的Handler
  6. // 比如我要处理jar:开头的路径,就用工厂创建sun.net.www.protocol.jar.Handler
  7. // 比如我要处理file:开头的路径,就用工厂创建sun.net.www.protocol.file.Handler
  8. // 具体的可以看最后的拓展
  9. super(getExtURLs(dirs), null, factory);
  10. // 无关紧要的内容
  11. SharedSecrets.getJavaNetAccess().
  12. getURLClassPath(this).initLookupCache(this);
  13. }
  14. // 处理外部的资源路径,总之拿到的资源路径肯定都来自于 java.ext.dirs
  15. private static File[] getExtDirs() {
  16. String s = System.getProperty("java.ext.dirs");
  17. File[] dirs;
  18. if (s != null) {
  19. StringTokenizer st =
  20. new StringTokenizer(s, File.pathSeparator);
  21. int count = st.countTokens();
  22. dirs = new File[count];
  23. for (int i = 0; i < count; i++) {
  24. dirs[i] = new File(st.nextToken());
  25. }
  26. } else {
  27. dirs = new File[0];
  28. }
  29. return dirs;
  30. }

接着我们看一下 URLClassLoader 是怎么处理这些资源路径的:

  1. public URLClassLoader(URL[] urls, ClassLoader parent,
  2. URLStreamHandlerFactory factory) {
  3. // 这个和我们的主题无关
  4. super(parent);
  5. // 安全管理器,和我们的主题也没多大关系
  6. SecurityManager security = System.getSecurityManager();
  7. if (security != null) {
  8. security.checkCreateClassLoader();
  9. }
  10. acc = AccessController.getContext();
  11. // 重点来啦~~用urls、factory、acc构建了一个URLClassPath
  12. ucp = new URLClassPath(urls, factory, acc);
  13. }

URLClassPath 的加载器如下所示:

  1. public URLClassPath(URL[] urls,
  2. URLStreamHandlerFactory factory,
  3. AccessControlContext acc) {
  4. // 记录URL列表,这个列表用来保持原始数据
  5. for (int i = 0; i < urls.length; i++) {
  6. path.add(urls[i]);
  7. }
  8. // 记录URL列表,这个列表会根据处理进程发生变动
  9. push(urls);
  10. // 构建jar协议的处理器
  11. if (factory != null) {
  12. jarHandler = factory.createURLStreamHandler("jar");
  13. }
  14. // 是否启用安全管理器。和我们目前的主题无关
  15. if (DISABLE_ACC_CHECKING)
  16. this.acc = null;
  17. else
  18. this.acc = acc;
  19. }

至此, ExtClassLoader 的资源就准备好了, AppClassLoader 的处理逻辑和这个一样,唯独 BootClassLoader 有点特别,因为它是在 JVM 层被初始化的,在应用层没有具体的类,全都是用 null 表示的,所以为了也能加载到 BootClassLoader 的资源,在 Launcher 类里还有一个类用来保存 BootClassLoader 的类路径—— BootClassPathHolder

  1. // 用来保存BootClassLoader的类路径
  2. private static class BootClassPathHolder {
  3. static final URLClassPath bcp;
  4. // 在被使用的时候自动创建 bcp
  5. static {
  6. URL[] urls;
  7. if (bootClassPath != null) {
  8. urls = AccessController.doPrivileged(
  9. new PrivilegedAction<URL[]>() {
  10. public URL[] run() {
  11. File[] classPath = getClassPath(bootClassPath);
  12. int len = classPath.length;
  13. Set<File> seenDirs = new HashSet<File>();
  14. for (int i = 0; i < len; i++) {
  15. File curEntry = classPath[i];
  16. // Negative test used to properly handle
  17. // nonexistent jars on boot class path
  18. if (!curEntry.isDirectory()) {
  19. curEntry = curEntry.getParentFile();
  20. }
  21. if (curEntry != null && seenDirs.add(curEntry)) {
  22. MetaIndex.registerDirectory(curEntry);
  23. }
  24. }
  25. return pathToURLs(classPath);
  26. }
  27. }
  28. );
  29. } else {
  30. urls = new URL[0];
  31. }
  32. bcp = new URLClassPath(urls, factory, null);
  33. bcp.initLookupCache(null);
  34. }
  35. }

现在我们通过 sun.misc.Launcher#getBootstrapClassPath() 也能获取到 BootClassLoader 的资源路径

  1. // sun.misc.Launcher#getBootstrapClassPath
  2. public static URLClassPath getBootstrapClassPath() {
  3. return BootClassPathHolder.bcp;
  4. }

资源封装

基于 ClassLoader 的资源查找并不是立刻马上查找的,而是需要使用时才会真正去查找。这一阶段主要就是对资源进行封装,通过 Enumeration 把资源路径暴露给调用者。简单来说就是“懒查找”(意思类比一下懒加载)。下面是资源封装的方法调用示意图:
点击查看【processon】

  1. 当我们调用 XX.class.getClassLoader().getResources() 时,首先会通过 AppClassLoadergetResources() 去查询,所有类加载器的 getResources() 都是使用的父类 ClassLoader 的方法:

    1. // java.lang.ClassLoader#getResources()
    2. public Enumeration<URL> getResources(String name) throws IOException {
    3. // 构建一个长度为2的数组,一个用来保存父类的资源;另一个保存自己的资源,最后合并到一起返回
    4. @SuppressWarnings("unchecked")
    5. Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
    6. // 判断是否存在父加载器,获取父加载器的资源
    7. if (parent != null) {
    8. tmp[0] = parent.getResources(name);
    9. } else {
    10. // 如果父加载器为null,那就获取BootClassPath
    11. tmp[0] = getBootstrapResources(name);
    12. }
    13. // 查找自己的资源路径
    14. tmp[1] = findResources(name);
    15. // 合并到一个Enumeration里,Compound表示组合
    16. return new CompoundEnumeration<>(tmp);
    17. }
  2. AppClassLoader 会调用 ExtClassLoader#getResources() 来获取资源, ExtClassLoader#getResources() 方法和上面的一样,都是 ClassLoader#getResources() 。因为 ExtClassLoaderparentnull ,即 Boot 。所以会先通过 getBootstrapResources() 获取 Boot 的资源路径:

    1. private static Enumeration<URL> getBootstrapResources(String name)
    2. throws IOException
    3. {
    4. // getBootstrapClassPath() 用来获取Boot的URLClassPath,下称bcp
    5. // 通过bcp#getResources(),获取相关的资源,因为这里获取到的是Enumeration<Resource>
    6. // 下面还需要再封装成Enumeration<URL>才能使用
    7. final Enumeration<Resource> e =
    8. getBootstrapClassPath().getResources(name);
    9. // 将资源封装成Enumeration
    10. return new Enumeration<URL> () {
    11. public URL nextElement() {
    12. return e.nextElement().getURL();
    13. }
    14. public boolean hasMoreElements() {
    15. return e.hasMoreElements();
    16. }
    17. };
    18. }

    通过深入源码查看 bcp#getResources() ,我们可以发现它只是单纯的返回了一个 Enumeration<Resource> ,并没有做任何解析~其实 ExtClassLoaderAppClassLoader 的思路大体都是这样:

    1. // sun.misc.URLClassPath#getResources(java.lang.String, boolean)
    2. public Enumeration<Resource> getResources(final String name,
    3. final boolean check) {
    4. // 把自己的资源通过Enumeration<Resource>暴露出去
    5. // 注意这里返回的是 Enumeration<Resource> !!!!
    6. return new Enumeration<Resource>() {
    7. private int index = 0;
    8. private int[] cache = getLookupCache(name);
    9. private Resource res = null;
    10. // 判断是否存在下一个资源
    11. private boolean next() {
    12. // 如果存在该资源,立刻返回true
    13. if (res != null) {
    14. return true;
    15. } else {
    16. // 资源加载器
    17. Loader loader;
    18. // 通过缓存查找一下是否已经加载过这个资源路径;如果没有会新建一个Loader
    19. while ((loader = getNextLoader(cache, index++)) != null) {
    20. // 通过Loader尝试获取一下指定的资源
    21. res = loader.getResource(name, check);
    22. // 如果获取到了,res!=null,就能返回true
    23. if (res != null) {
    24. return true;
    25. }
    26. }
    27. return false;
    28. }
    29. }
    30. public boolean hasMoreElements() {
    31. return next();
    32. }
    33. public Resource nextElement() {
    34. if (!next()) {
    35. throw new NoSuchElementException();
    36. }
    37. // this.res 后续还要判断用,先设置给r
    38. Resource r = res;
    39. // 然后把res设置为null
    40. res = null;
    41. // 返回r
    42. return r;
    43. }
    44. };
    45. }

    疑惑:这里的 BootURLClassPath 为什么要先获取 Resource 再转成 URL ?不直接 findResources() 获取 Resource

  3. 随后 ExtClassLoader 会通过 URLClassPath#findResources() 获取自己的资源路径,这个方法和URLClassPath#getResources() 差不多,就是前者获取 URL ,后者获取 Resource

    1. public Enumeration<URL> findResources(final String name)
    2. throws IOException
    3. {
    4. // 这里调用的是URLClassPath.findResources(),Boot那边调的是getResources()
    5. // 不明白啥意思,但总之都通过这方法获取
    6. final Enumeration<URL> e = ucp.findResources(name, true);
    7. return new Enumeration<URL>() {
    8. private URL url = null;
    9. private boolean next() {
    10. if (url != null) {
    11. return true;
    12. }
    13. do {
    14. // 做了一些安全处理,可以忽略
    15. URL u = AccessController.doPrivileged(
    16. new PrivilegedAction<URL>() {
    17. public URL run() {
    18. if (!e.hasMoreElements())
    19. return null;
    20. return e.nextElement();
    21. }
    22. }, acc);
    23. if (u == null)
    24. break;
    25. url = ucp.checkURL(u);
    26. } while (url == null);
    27. return url != null;
    28. }
    29. public URL nextElement() {
    30. if (!next()) {
    31. throw new NoSuchElementException();
    32. }
    33. URL u = url;
    34. url = null;
    35. return u;
    36. }
    37. public boolean hasMoreElements() {
    38. return next();
    39. }
    40. };
    41. }
  4. 最后把父加载器的资源路径和自己的资源路径合并到一起,返回给调用者 AppClassLoader 。接着, AppClassLoader 会去获取自己的资源路径,原理和 ExtClassLoader 一样~

  5. 最后把 AppClassLoader 自己的资源路径和父加载器 ExtClassLoader 的资源路径( ExtClassLoader 的资源路径已经整合了 BootClassLoader 的资源路径)合并到一起返回回去。

获取资源

前面已经把所有的资源封装成一个 Enumeration<URL> ,这一步就是真正的去查询获取资源,先看这样一段代码:

// 资源初始化+封装
Enumeration<URL> resources = ClassLoaderTest.class.getClassLoader().getResources("cn/codeleven");
// 开始真正的查询、获取资源
while(resources.hasMoreElements()){
    URL url = resources.nextElement();
    System.out.println(url);
}
  1. 首先去查询是否存在名字为 cn/codeleven 的资源(这里的名称匹配规则后面讲)
  2. 如果有就获取对应的 URL 并打印到控制台,反之继续,直到退出 while 循环

下面是 resources 变量的数据结构:
点击查看【processon】

构造Loader

  1. 当我们通过 hasMoreElements() 方法去查询时,本质上是调用 URLClassPath#findResources() 的匿名函数对象的 hasMoreElements() 。在 next() 方法里,首先要查询获取 Loader : ```java // 正在查询的资源路径的下标 private int index = 0; // 查询指定的name是否有缓存,如果有缓存,会返回一个整数数组,元素表示 指定名字的资源 在所有资源路径里的下标 private int[] cache = getLookupCache(name); // 要返回的url private URL url = null;

public boolean hasMoreElements() { return next(); } private boolean next() { if (url != null) { return true; } else { Loader loader; // index表示当前要查找的资源路径,cache表示ClassLoader缓存下来的资源路径下标 // 如果ClassLoader缓存过 [a.jar, b.jar, c.jar, d.jar],且foo包仅存在于a.jar和c.jar // 那么getLookupCache()会返回 [0, 2] while ((loader = getNextLoader(cache, index++)) != null) { url = loader.findResource(name, check); if (url != null) { return true; } } return false; } }


2. 它首先通过 `getNextLoader()` 去查找当前 `index` 的资源路径对应的 `Loader` :
```java
private synchronized Loader getNextLoader(int[] cache, int index) {
    // URLClassLoader如果已经关闭了就没必要查了
    if (closed) {
        return null;
    }
    // 如果存在cache,就按cache给的下标去取
    if (cache != null) {
        // 这里对这个缓存的原理有些疑惑,这一块不太清楚为啥这么写,只能知道是通过缓存查Loader
        if (index < cache.length) {
            // cache已经给了缓存过 index下标的资源路径的Loader的下标地址
            // 所以我只需要通过cache[index]获取Loader的下标地址
            Loader loader = loaders.get(cache[index]);
            if (DEBUG_LOOKUP_CACHE) {
                System.out.println("HASCACHE: Loading from : " + cache[index]
                                   + " = " + loader.getBaseURL());
            }
            return loader;
        } else {
            return null; // finished iterating over cache[]
        }
    } else {
        // 如果没有缓存,就去自己创建一个
        return getLoader(index);
    }
}
  1. 如果缓存里不存在该资源的 Loader ,那么就创建一个 Loader 的原理如下所示:

    private synchronized Loader getLoader(int index) {
     if (closed) {
         return null;
     }
      // 如果加载器数量没超过我们查询的index,那么说明目前没有加载器。就需要创建一个
     while (loaders.size() < index + 1) {
         // Pop the next URL from the URL stack
         URL url;
         synchronized (urls) {
             if (urls.empty()) {
                 return null;
             } else {
                 // 从urls属性里获取一个URL
                 url = urls.pop();
             }
         }
    
         // 处理URL字符串
         String urlNoFragString = URLUtil.urlNoFragString(url);
         // lmap保存了URL和Loader的关系,判断该URL是否存在对应的Loader
         if (lmap.containsKey(urlNoFragString)) {
             continue;
         }
         // 若该URL不存在Loader,就创建一个
         Loader loader;
         try {
             // 根据url开头的协议,决定创建FileLoader还是JarLoader,还是普通的Loader
             loader = getLoader(url);
             // 这条语句,只针对JarLoader,其余的Loader返回的都是null
             // 这是因为Jar包里面还会有其他的资源路径,统统需要取出来,保存到urls里
             URL[] urls = loader.getClassPath();
             if (urls != null) {
                 push(urls);
             }
         } catch (IOException e) {
             continue;
         } catch (SecurityException se) {
             continue;
         }
         // 这个也是缓存相关的,不太清楚,C源码一直没找到
         validateLookupCache(loaders.size(), urlNoFragString);
         // 增加loader到loaders
         loaders.add(loader);
         // 建立关系
         lmap.put(urlNoFragString, loader);
     }
     if (DEBUG_LOOKUP_CACHE) {
         System.out.println("NOCACHE: Loading from : " + index );
     }
     // 获取loader,并返回
     return loaders.get(index);
    }
    

    Loader 构造完毕,就可以去通过 Loader 查询资源里的东西了。

查询资源

Loader 分为三种:

  • FileLoader ,加载文件的 Loader
  • JarLoader ,加载Jar包的 Loader
  • Loader ,普通的 Loader

我拿 FileLoader 举个例子,这里省掉了中间代码,直接放最后的代码:

Resource getResource(final String name, boolean check) {
    final URL url;
    try {
        // getBaseURL() 表示这个Loader对应的资源,这里就追加了一个 . 
        URL normalizedBase = new URL(getBaseURL(), ".");
        // 将我们查询的name,处理后拼接在 getBaseURL() 后边
        url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));
        // 通过这个url获取 filename ,查看filename是否以 normalizedBase 开头
        // 如果为true,那么没问题,就是这个url;如果为false,说明这个资源路径包含了 “../..”的字符串
        if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
            // requested resource had ../..'s in path
            return null;
        }
        // 是否要进行安全检查
        if (check)
            URLClassPath.check(url);
        final File file;
        // 判断要查找的资源名称里是否包含 “..”,若包含则特殊处理一下
        if (name.indexOf("..") != -1) {
            file = (new File(dir, name.replace('/', File.separatorChar)))
                // 获取不包含..的规范路径
                  .getCanonicalFile();
            if ( !((file.getPath()).startsWith(dir.getPath())) ) {
                /* outside of base dir */
                return null;
            }
        } else {
            // 构建File对象
            file = new File(dir, name.replace('/', File.separatorChar));
        }
        // 如果这个File对象存在,则返回对应的Resource
        if (file.exists()) {
            return new Resource() {
                public String getName() { return name; };
                public URL getURL() { return url; };
                public URL getCodeSourceURL() { return getBaseURL(); };
                public InputStream getInputStream() throws IOException
                    { return new FileInputStream(file); };
                public int getContentLength() throws IOException
                    { return (int)file.length(); };
            };
        }
    } catch (Exception e) {
        return null;
    }
    return null;
}

到此,如何查询资源也已经讲完了~其余的两个 Loader ,读者有兴趣就自己看看~

资源查找总结

因为资源查找最终都是依靠 Loader 进行的:

  • FileLoader ,基于 ClassPath + 查询的资源路径/名称 查找资源
  • JarLoader ,是通过使用 JarFile 来获取的~目前还是空着的,等用到了来补充

总而言之,所有的路径查找都是基于 ClassPath 进行的。

案例1

该方法用来获取类路径中以 ~~`path` 结尾的路径,如果出现相同路径内出现多个 `path`选择长度最长的。比如说,我查找 `org/apache` :~~

/G:/workspace/mybatis-3/target/test-classes/org/apache
/G:/workspace/mybatis-3/target/classes/org/apache
file:/E:/OfflineRepository/org/slf4j/slf4j-log4j12/1.7.29/slf4j-log4j12-1.7.29.jar!/org/apache
file:/E:/OfflineRepository/log4j/log4j/1.2.17/log4j-1.2.17.jar!/org/apache
file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache
file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-api/2.12.1/log4j-api-2.12.1.jar!/org/apache
file:/E:/OfflineRepository/commons-logging/commons-logging/1.2/commons-logging-1.2.jar!/org/apache
file:/E:/OfflineRepository/org/apache/velocity/velocity-engine-core/2.1/velocity-engine-core-2.1.jar!/org/apache
file:/E:/OfflineRepository/org/apache/commons/commons-lang3/3.8.1/commons-lang3-3.8.1.jar!/org/apache
file:/E:/OfflineRepository/org/apache/commons/commons-compress/1.19/commons-compress-1.19.jar!/org/apache

类似 ~~`file:/E:/OfflineRepository/log4j/log4j/1.2.17/log4j-1.2.17.jar!/org/apache` 就仅仅查找到最后是 `org/apache` 就结束了。
而对于 `file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache` 这种路径,包含两种满足结尾是 `org/apache` 的情况:~~

  1. ~~file:/E:/OfflineRepository/org/apache~~
  2. ~~file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache~~

但是要选一个更加“准确”(长)的,所以就是第二种情况
通过今天的学习,彻底颠覆了以往对 ClassLoader.getResources() 的理解,之所以只获取到了file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache
是因为log4j-core-2.12.1.jar 这个库是在 ClassPath 里面的,所以,去查找 /org/apache 时,能找到 file:/E:/OfflineRepository/org/apache/logging/log4j/log4j-core/2.12.1/log4j-core-2.12.1.jar!/org/apache 而不是 file:/E:/OfflineRepository/org/apache

案例2

Spring的每个模块下(比如spring-aop.jarspring-webmvc.jar等)都会包含一个文件:
META-INF/spring.handlers
它的内容如下所示:

http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler

该文件保存了该模块所属的命名空间,以及该命名空间所需的标签解析器。在解析配置文件时会用到ClassLoader来加载资源。
以下是 Spring 加载所有 META-INF/spring.handlers 的代码:

// BeanDefinitionParserDelegate#parseCustomElement(Element, BeanDefinition)
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    ...
    // 根据命名空间找到对应的NamespaceHandlerspring
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    ...
}
// DefaultNamespaceHandlerResolver#resolve()
public NamespaceHandler resolve(String namespaceUri) {
    // 获取所有已经配置好的handler映射
    Map<String, Object> handlerMappings = getHandlerMappings();
    ...
}
// DefaultNamespaceHandlerResolver#getHandlerMappings()
private Map<String, Object> getHandlerMappings() {
    ...
    // this.handlerMappingsLocation在构造函数中已经被初始化为META-INF/Spring.handlers
    Properties mappings =        PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader)    
    ...
}
// PropertiesLoaderUtils#loadAllProperties(String, ClassLoader)
public static Properties loadAllProperties(String resourceName, @Nullable ClassLoader classLoader) throws IOException {
    Enumeration<URL> urls = (classLoaderToUse != null ? classLoaderToUse.getResources(resourceName) :
            ClassLoader.getSystemResources(resourceName));
    Properties props = new Properties();
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        URLConnection con = url.openConnection();
        ResourceUtils.useCachesIfNecessary(con);
        try (InputStream is = con.getInputStream()) {
            if (resourceName.endsWith(XML_FILE_EXTENSION)) {
                props.loadFromXML(is);
            }
            else {
                props.load(is);
            }
        }
    }
    return props;
}

Loader 会加载所有类路径上的文件路径(如果是jar包也会进入分析),将满足条件的路径筛选出来,最后用Properties进行加载。

拓展

URLStreamHandlerFactory

这货就是一个接口,定义了获取资源流 URLStreamHandler 的方法:

public interface URLStreamHandlerFactory {
    URLStreamHandler createURLStreamHandler(String protocol);
}

这个具体实现也比较简单的,在 Launcher 里有一个 Factory

private static class Factory implements URLStreamHandlerFactory {
    // 包前缀,可以去查一下这个包,这个包下面有常见协议的所有处理器
    private static String PREFIX = "sun.net.www.protocol";
    // 实现了接口,根据协议获取处理器
    public URLStreamHandler createURLStreamHandler(String protocol) {
        // 根据协议,定位对应的Handler
        String name = PREFIX + "." + protocol + ".Handler";
        try {
            Class<?> c = Class.forName(name);
            return (URLStreamHandler)c.newInstance();
        } catch (ReflectiveOperationException e) {
            throw new InternalError("could not load " + protocol +
                                    "system protocol handler", e);
        }
    }
}

MetaIndex

今天学习 ClassLoader 加载资源原理时,发现了 MetaIndex 这个类,然后得知了在 jdk 的库里有这么一个文件 meta-index
image.png
这文件里面保存当前路径下( java/jre8/lib ),所有 Jar 包提供的包名信息:

% VERSION 2
% WARNING: this file is auto-generated; do not edit
% UNSUPPORTED: this file and its format may change and/or
%   may be removed in a future release
# charsets.jar
sun/nio
sun/awt
# jce.jar
javax/crypto
sun/security
META-INF/ORACLE_J.RSA
META-INF/ORACLE_J.SF
# jfr.jar
oracle/jrockit/
jdk/jfr
com/oracle/jrockit/
! jsse.jar
sun/security
com/sun/net/
! management-agent.jar
@ resources.jar
com/sun/java/util/jar/pack/
META-INF/services/sun.util.spi.XmlPropertiesProvider
META-INF/services/javax.print.PrintServiceLookup
com/sun/corba/
META-INF/services/javax.sound.midi.spi.SoundbankReader
...

如果没有接触过 ClassLoader 的,基本都不知道这货的作用。这 meta-index 文件是用来提供索引的,能帮助快速检索包。

ClassLoader的findResources和getResources的区别

getResources() 在查找资源时会结合父加载器的资源
findResources() 仅仅只查找当前类加载器的资源