前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >踩坑集锦之你真的明白Java类路径的含义吗?

踩坑集锦之你真的明白Java类路径的含义吗?

作者头像
大忽悠爱学习
发布2023-05-23 10:11:11
1.2K0
发布2023-05-23 10:11:11
举报
文章被收录于专栏:c++与qt学习

踩坑集锦之你真的明白Java类路径的含义吗?

引言

本文基于JDK 1.8进行讲解!!!

Dubbo源码篇02—从泛化调用探究Wrapper机制的原理一文中,我们写过compileJava2Class这个方法,来编译,加载,实例化我们的代理对象的java文件:

代码语言:javascript
复制
    private static Object compileJava2Class(String filePath, String proxyClassName) throws Exception {
        // 编译 Java 文件
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> compilationUnits =
                fileManager.getJavaFileObjects(new File(filePath + File.separator + proxyClassName + ".java"));
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        task.call();
        fileManager.close();
        // 加载 class 文件
        URL[] urls = new URL[]{new URL("file:" + filePath)};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class<?> clazz = urlClassLoader.loadClass(CustomInvokerProxyUtils.WRAPPER_PACKAGE_NAME + "." + proxyClassName);
        // 反射创建对象,并且实例化对象
        Constructor<?> constructor = clazz.getConstructor();
        return constructor.newInstance();
    }

在该方法中,我们使用URL从指定路径下加载我们的class文件,那么URLClassLoader究竟是如何定位资源的呢?

还有,我们经常会使用类加载器的getResource等方法加载类路径下的资源,那么这其中的细节你又知道多少呢?


前置知识补充

  • classpath
    • Java中有两个classpath,一个是bootstrap classpath,另一个是classpath。
    • classpath有如下两种形式 :
      • JAR files(JAR文件的全路径)
      • Paths to the top of package hierarchies.(顶级目录路径)
  • bootrap classpath
    • bootstrap classpath对应于启动类加载器,根据类加载的双亲委派模型,Java程序运行时首先会由启动类加载器加载 bootstrap classpath下的类和Jar包中的类。
    • bootstrap classpath可以通过-Xbootclasspath JVM参数来指定。
    • 在Java代码中,我们可以通过System.getProperty("sun.boot.class.path")来获取bootstrap classpath

Java中的getResource等资源加载方法也遵循双亲委派模型,首先会委托给父类加载器加载资源。委托到启动类加载器时,启动类加载器会从bootstrap classpath对应的jar包或目录中加载资源。因此放在bootstrap classpath中的资源也能够被加载。

  • classpath
    • classpath用于告诉Java程序从哪里加载用户类以及用户资源。(JRE、JDK本身的类,以及扩展类应该通过其他方式来定位,例如bootstrap class path 或 扩展目录。)
    • 所以说,classpath是用来定位用户自定义的类和资源的。
    • 在Java代码中,可以通过System.getProperty("java.class.path")来获取当前的classpath。
  • extention directory
    • extention directory也就是扩展目录,是Java程序加载扩展类的目录,可以通过System.getProperty("java.ext.dirs")来获取。

故事还要从程序启动讲起…

我们知道JVM中有两种类型的类加载器,由C++编写的及由Java编写的。除了启动类加载器(Bootstrap Class Loader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承自类java.lang.ClassLoader。

各种类加载器之间存在着逻辑上的父子关系:

在这里插入图片描述
在这里插入图片描述

启动类加载器是Java虚拟机中内置的一个特殊类加载器,主要用于加载Java平台核心库中的类。它是由Java虚拟机自身实现的,并且是用C++语言编写的。它的主要作用是在Java虚拟机启动时,负责加载Java核心库(如rt.jar等)中的类,以及其他一些需要在Java虚拟机启动时就可用的类和资源。

  • 启动类加载器的核心逻辑是在java.c文件中的LoadMainClass函数中实现的。该函数主要调用了checkAndLoadMain函数和GetLauncherHelperClass函数。
  • GetLauncherHelperClass函数的作用是查找并返回名为"sun.launcher.LauncherHelper"的类。
  • checkAndLoadMain函数则是在LauncherHelper类中实现的,主要负责加载包含main方法的主类,并在加载该类时完成扩展类加载器和应用类加载器的初始化工作。

总的来说,启动类加载器的主要作用是在Java虚拟机启动时,加载核心类库以及其他必要的类和资源,以便Java程序能够正常运行。启动类加载器是Java虚拟机中最早启动的类加载器,因此它的实现非常简单、高效。

核心源码如下:

代码语言:javascript
复制
int JNICALL
JavaMain(void * _args)
{
   ...
    mainClass = LoadMainClass(env, mode, what);
   ...
}


static jclass
LoadMainClass(JNIEnv *env, int mode, char *name)
{
	jclass cls = GetLauncherHelperClass(env);
		...
    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
                "checkAndLoadMain",
                "(ZILjava/lang/String;)Ljava/lang/Class;"));
    ...
}

jclass
GetLauncherHelperClass(JNIEnv *env)
{
    if (helperClass == NULL) {
        NULL_CHECK0(helperClass = FindBootStrapClass(env,
                "sun/launcher/LauncherHelper"));
    }
    return helperClass;
}

总结:这套逻辑做的事情就是通过启动类加载器加载sun.launcher.LauncherHelper类,执行该类的方法checkAndLoadMain,最终完成加载main函数所在的类


C++和Java的桥接类LauncherHelper

  • LauncherHelper的checkAndLoadMain方法作为桥接C++和Java语言的关键方法,主要做了以下几件事情:
代码语言:javascript
复制
    public static Class<?> checkAndLoadMain(boolean printToStderr,
                                            int mode,
                                            String what) {
        //根据参数决定将ostream初始化为标准输出流还是标准错误流,从而保证程序的正常输出。                                    
        initOutput(printToStderr);
        // mode 变量是一个枚举类型,表示不同的启动模式
        // what参数指定要运行的主类名或要运行的JAR文件路径。根据mode参数的不同,what的含义也有所不同。
        String cn = null;
        switch (mode) {
             //当mode为LM_CLASS时,what为要运行的主类名
            case LM_CLASS:
                cn = what;
                break;
            //当mode为LM_JAR时,what为要运行的JAR文件路径。    
            case LM_JAR:
                cn = getMainClassFromJar(what);
                break;
            default:
                // should never happen
                throw new InternalError("" + mode + ": Unknown launch mode");
        }
        //将路径符替换为包分割符号
        cn = cn.replace('/', '.');
        Class<?> mainClass = null;
        try {
            //加载启动类
            mainClass = scloader.loadClass(cn);
        } catch (NoClassDefFoundError | ClassNotFoundException cnfe) {
            //检查操作系统是否为 OS X,如果是,则可能存在字符串规范化的问题,需要对类名进行重新规范化处理,然后再次尝试加载
            ...
        }
        // 设置启动类
        appClass = mainClass;
         // JavaFX是一组用于创建富客户端应用程序的工具和库,可以帮助开发人员轻松构建跨平台的桌面和移动应用程序。
        //在 Java 8 及之前版本中,JavaFX 应用程序和普通 Java 应用程序启动方式不同。JavaFX 应用程序需要通过特定的启动类来启动,而不是通过 main 方法。
        ...
       //校验启动类的psvm方法是否符合规范
       //1.存在方法名为main的方法
       //2.方法修饰符是否为public
       //3.方法是否为静态方法
       //4.方法是否携带一个String[]数组类型的入参
       //5.方法的返回值是否为void
        validateMainClass(mainClass);
        return mainClass;
    }
代码语言:javascript
复制
    static void validateMainClass(Class<?> mainClass) {
        Method mainMethod;
        try {
            mainMethod = mainClass.getMethod("main", String[].class);
        } catch (NoSuchMethodException nsme) {
            // invalid main or not FX application, abort with an error
            abort(null, "java.launcher.cls.error4", mainClass.getName(),
                  FXHelper.JAVAFX_APPLICATION_CLASS_NAME);
            return; // Avoid compiler issues
        }

        /*
         * getMethod (above) will choose the correct method, based
         * on its name and parameter type, however, we still have to
         * ensure that the method is static and returns a void.
         */
        int mod = mainMethod.getModifiers();
        if (!Modifier.isStatic(mod)) {
            abort(null, "java.launcher.cls.error2", "static",
                  mainMethod.getDeclaringClass().getName());
        }
        if (mainMethod.getReturnType() != java.lang.Void.TYPE) {
            abort(null, "java.launcher.cls.error3",
                  mainMethod.getDeclaringClass().getName());
        }
    }

这里注意几点:

  • mode 变量是一个枚举类型,表示不同的启动模式。LM_CLASS 和 LM_JAR 分别代表两种不同的启动模式。
    • LM_CLASS 表示通过指定一个类名来启动程序,这个类名可以是任意一个带有 main() 方法的类。
    • LM_JAR 则表示通过指定一个 jar 文件路径来启动程序。在这种模式下,需要在 jar 文件的 META-INF/MANIFEST.MF 文件中指定 Main-Class 属性,该属性值为带有 main() 方法的类的全限定名。这个属性会被解析出来,然后作为启动类。
    • 在代码中,根据传入的 mode 值来决定是使用类名还是 jar 文件路径来获取启动类。如果是 jar 文件,则需要通过解析 META-INF/MANIFEST.MF 文件来获取启动类。

主类是如何被加载的

我们的主类是通过scloader类加载器加载的,scloader类加载器在LauncherHelper桥接类进行类初始化操作时被初始化:

在这里插入图片描述
在这里插入图片描述

系统类加载器别名应用程序上下文类加载器,说到这里大家应该就不陌生了,下面看看系统类加载器是如何被初始化的吧:

代码语言:javascript
复制
    public static ClassLoader getSystemClassLoader() {
        //尝试初始化系统类加载器---单例初始化操作
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        ...
        return scl;
    }

initSystemClassLoader方法的关键是调用Launcher的getLauncher完成Java 程序的启动器:

代码语言:javascript
复制
    private static synchronized void initSystemClassLoader() {
        //sclSet变量控制系统类加载器只被初始化调用一次
        if (!sclSet) {
             //scl就是系统类加载器
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            //
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                //拿到系统类加载器
                scl = l.getClassLoader();
                //SystemClassLoaderAction就比较有意思了--我们可以在此处替换系统类加载器为我们自定义的类加载器
                scl = AccessController.doPrivileged(new SystemClassLoaderAction(scl));
            }
            sclSet = true;
        }
    }

加餐: 如何利用jdk预留的口子,替换系统类加载器为我们自定义的类加载器

上面说过,在initSystemClassLoader方法中,在创建完java启动器后,会获取java启动器在初始化阶段创建好的appClassLoader,但是在SystemClassLoaderAction的run方法中,jdk给我们预留了替换默认系统类加载器的口子:

代码语言:javascript
复制
class SystemClassLoaderAction
    implements PrivilegedExceptionAction<ClassLoader> {
    private ClassLoader parent;
    //parent为系统类加载器 
    SystemClassLoaderAction(ClassLoader parent) {
        this.parent = parent;
    }

    public ClassLoader run() throws Exception {
        //我们可以在系统上下文中设置我们自定义的系统类加载器全类名
        String cls = System.getProperty("java.system.class.loader");
        if (cls == null) {
            return parent;
        }
        //此处利用系统类加载器来加载我们的自定义系统类加载器
        Constructor<?> ctor = Class.forName(cls, true, parent) 
             //需要提供一个有一个参数的构造函数,参数类型为ClassLoader
            .getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
        //实例化我们的自定义系统类加载器
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[] { parent });
        //设置我们的自定义的系统类加载器为线程上下文类加载器    
        Thread.currentThread().setContextClassLoader(sys);
        //返回替换默认的系统类加载器
        return sys;
    }
}

这里我们介绍完了jdk预留的口子SystemClassLoaderAction,但是还没介绍Launcher对象初始化的时候,是如何把ExtClassLoader和AppClassLoader创建出来的,下面一起来看看。


Launcher启动类的初始化

代码语言:javascript
复制
public class Launcher {
    //Launcher类进行类初始化操作时,会创建一个单例的Launcher对象--饿汉式单例
    private static Launcher launcher = new Launcher();
    //系统类加载器
    private ClassLoader loader;
    //从系统上下文中获取BootStrapClassLoader启动类加载器加载的类路径
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    //返回实例化好的单例Launcher对象 
    public static Launcher getLauncher() {
        return launcher;
    }

注意我们无法通过断点断到Launcher构造函数被调用的过程,具体原因参考这篇文章:

代码语言:javascript
复制
// java虚拟机启动的时候调用
public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        //初始化扩展类加载器
        extcl = ExtClassLoader.getExtClassLoader();
        // 初始化线程上下文类加载器
        loader = AppClassLoader.getAppClassLoader(extcl)
        //设置线程上下文类加载器默认为appClassLoader
        //上面提到,我们可以在后续的jdk预留的口子中替换默认的系统类加载器为我们自定义的类加载器
        Thread.currentThread().setContextClassLoader(loader);
        ...
    }

启动类加载器类路径如何确定的

Launcher内部提供的一个静态内部类BootClassPathHolder,用于持有启动类加载器的类路径:

代码语言:javascript
复制
//BootClassPathHolder类会在JVM启动时被类加载器初始化    
private static class BootClassPathHolder {
        static final URLClassPath bcp;
        static {
            URL[] urls;
            //从系统上下文中拿到启动类加载器加载的类路径
            if (bootClassPath != null) {
                    //对类路径按照分隔符进行分割,并封装为一组File对象返回
                    File[] classPath = getClassPath(bootClassPath);
                    int len = classPath.length;
                    //过滤掉重复的目录
                    Set<File> seenDirs = new HashSet<File>();
                    for (int i = 0; i < len; i++) {
                        File curEntry = classPath[i];
                        //这段代码的作用是为了正确地处理启动类路径中不存在的JAR文件。
                        //如果curEntry代表的是一个JAR文件,!curEntry.isDirectory()将返回true,
                        //此时代码将curEntry设置为JAR文件的父文件夹。
                        //这是因为JAR文件是一个文件而不是一个目录,如果直接将JAR文件添加到类路径中可能会导致ClassNotFoundException。
                        //因此,将JAR文件所在的文件夹添加到类路径中可以避免这个问题。
                        if (!curEntry.isDirectory()) {
                            curEntry = curEntry.getParentFile();
                        }
                       //将JAR文件中的元数据信息注册到内存中的元数据索引中,以便在需要查找该JAR文件时进行快速查找
                       //前提是jar包提供了meta-index文件
                        if (curEntry != null && seenDirs.add(curEntry)) {
                           MetaIndex.registerDirectory(curEntry);
                         }
                    } 
                  //将File转换为URL
                  urls=pathToURLs(classPath);
            } else {
                urls = new URL[0];
            }
            //初始化启动类加载器对应URLClassPath--类路径集合操作的具体表示
            bcp = new URLClassPath(urls, factory, null);
            bcp.initLookupCache(null);
        }
    }

我们可以Launcher提供的getBootstrapClassPath方法,获取启动类加载器对应的URLClassPath:

代码语言:javascript
复制
    public static URLClassPath getBootstrapClassPath() {
        return BootClassPathHolder.bcp;
    }

注意:

  • MetaIndex.registerDirectory(curEntry)方法用于将JAR文件中的元数据信息注册到内存中的元数据索引中,以便在需要查找该JAR文件时进行快速查找,这在加载类和资源时非常有用。
  • 在Java 9及更高版本中,JAR文件中的元数据信息已被放置在META-INF目录下,包括module-info.class文件和module-info文件。
  • 当执行该方法时,会扫描指定的目录下的所有JAR文件,将这些JAR文件中的元数据信息读取到内存中,以便在后续的类加载和资源查找中使用。
  • MetaIndex.getJarMap()方法返回一个包含所有元索引记录的Map对象,其中的键是JAR文件名,值是该JAR文件的元数据记录。元数据记录是包含JAR文件中所有类和资源名称的列表,以及这些名称对应的SHA-1散列的字符串数组。这个Map对象被用于构建Java运行时的类路径索引,用于快速查找类和资源。

MetaIndex类的registerDirectory方法的作用是解析meta-index文件,提取其中的类和资源信息,并将其保存到JarMap中,以便在加载类时加快查找速度。如果某个Jar包没有提供meta-index文件,那么该方法啥也不做,直接返回。

在这里插入图片描述
在这里插入图片描述
  • 启动类加载器类路径状态
在这里插入图片描述
在这里插入图片描述

扩展类加载器初始化时机

ExtClassLoader会调用getExtClassLoader来创建一个单例的扩展类加载器实例:

代码语言:javascript
复制
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            if (instance == null) {
                synchronized(ExtClassLoader.class) {
                    if (instance == null) {
                        instance = createExtClassLoader();
                    }
                }
            }
            return instance;
        }
代码语言:javascript
复制
private static ExtClassLoader createExtClassLoader() throws IOException {
               //获取扩展类加载器负责加载的目录---也就是我们说的扩展类加载器能够加载的类路径  
               final File[] dirs = getExtDirs();
               int len = dirs.length;
                //将JAR文件中的元数据信息注册到内存中的元数据索引中,以便在需要查找该JAR文件时进行快速查找
                //前提是jar包提供了meta-index文件
                for (int i = 0; i < len; i++) {
                     MetaIndex.registerDirectory(dirs[i]);
                }
               return new ExtClassLoader(dirs);
}               
代码语言:javascript
复制
        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            if (s != null) {
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                for (int i = 0; i < count; i++) {
                    dirs[i] = new File(st.nextToken());
                }
            } else {
                dirs = new File[0];
            }
            return dirs;
        }

扩展类加载器默认能够加载的路径:

在这里插入图片描述
在这里插入图片描述

ExtClassLoader的构造方法最终会调用父类URLClassLoader的构造方法:

代码语言:javascript
复制
        //URLStreamHandlerFactory是Java中用于自定义URL协议处理程序的接口
        private static URLStreamHandlerFactory factory = new Factory();
        
        public ExtClassLoader(File[] dirs) throws IOException {
            //将file包装为URL,
            super(getExtURLs(dirs), null, factory);
            ...
        }
  
        private static URL[] getExtURLs(File[] dirs) throws IOException {
            // 将扩展类加载器所能加载的两个类路径下所有文件搜寻出来
            //把每一个文件都封装为一个file:协议URL
            Vector<URL> urls = new Vector<URL>();
            for (int i = 0; i < dirs.length; i++) {
                String[] files = dirs[i].list();
                if (files != null) {
                    for (int j = 0; j < files.length; j++) {
                        if (!files[j].equals("meta-index")) {
                            File f = new File(dirs[i], files[j]);
                            urls.add(getFileURL(f));
                        }
                    }
                }
            }
            URL[] ua = new URL[urls.size()];
            urls.copyInto(ua);
            return ua;
        }        
在这里插入图片描述
在这里插入图片描述

可以看到最终是调用到了URLClassLoader父类的构造方法:

在这里插入图片描述
在这里插入图片描述

应用程序类加载器初始化时机

AppClassLoader会调用getAppClassLoader来创建一个单例的扩展类加载器实例:

代码语言:javascript
复制
       //传入的类加载器是上面已经实例化好的扩展类加载器,这里作为应用程序上下文类加载器的双亲类加载器
       public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException {
            //获取环境上下文中定义的类路径
            final String s = System.getProperty("java.class.path");
            //getClassPath会利用分隔符切分类路径--和扩展类路径处理一个套路
            final File[] path = (s == null) ? new File[0] : getClassPath(s);
            //将path转换为url返回
            URL[] urls = (s == null) ? new URL[0] : pathToURLs(path);
           return new AppClassLoader(urls, extcl);
        }
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

AppClassLoader不同之处在于他自己内部有一个单独的URLClassPath,独立于其父类URLClassLoader提供的ucp:

代码语言:javascript
复制
        final URLClassPath ucp;
        AppClassLoader(URL[] urls, ClassLoader parent) {
            //调用到父类URLClassLoader的构造函数
            super(urls, parent, factory);
            //初始化URLClassPath--单独提供的
            ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
            ucp.initLookupCache(this);
        }
在这里插入图片描述
在这里插入图片描述

前置知识补充

  • 资源的访问方式
    • 资源(resource) 就是我们的程序需要访问的数据,例如图片、文本、视频、音频等等。
  • 访问资源有两种方式:
    • Location dependent
    • Location Independent
  • Location dependent
    • 所谓Location Dependent,就是我们对资源的访问方式受程序所在位置的影响。
    • 例如,在Java中,使用本机绝对路径访问文件时,就是一种Location Dependent的访问方法,代码如下:
代码语言:javascript
复制
File file = new File("/root/project/resource/config.xml")

如果项目中使用上述方式读取文件,当项目在其他目录或其他机器上部署和运行时,就需要修改上述代码中的文件路径,因此上述用法是LocationDependent的。

注意:

  • 并不是说通过File类来访问资源一定是Location Dependent的,我们借助File也可以实现Location Independent的访问,例如我们可以给File构造器传入相对路径,这里的相对路径是相对于当前工作目录(System.getProperty("user.dir"))的,所以如果要访问的资源是项目的一部分,File类搭配相对路径也可以实现Location Independent的访问。
  • Location Independent
    • 实现Location Independent的资源读取最常用的就是Class或ClassLoader类中的如下方法:
代码语言:javascript
复制
URL getResource(String name)
InputStream getResourceAsStream(String name)
Enumeration<URL> getResources(String name)
getSystemResource, getSystemResources, getSystemResourceAsStream
  • 其中,前两个方法是Class和ClassLoader类都有的,后面的方法只有ClassLoader类有。
  • 借助这些方法,可以实现从classpath下读取资源,或者相对于当前class文件所在的目录读取资源。

借助Class和ClassLoader都可以获取资源,并且后面分析源码可以看到,Class类获取资源的方法最终会调用ClassLoader类中对应的方法,那么,这两个类中获取资源的方法的区别在哪里呢?

  • 区别在于ClassLoader类中的这两个方法仅支持相对于classpath的路径(开头不能加/,加了就获取不到classpath下的文件了),而Class类中的这两个方法除了支持相对于classpath的路径外(以/开头),还支持相对于当前class文件所在目录的路径(开头不加/)。
在这里插入图片描述
在这里插入图片描述

这里以一个实际的例子为例进行说明:

  • 项目结构
在这里插入图片描述
在这里插入图片描述

当前工作目录可以打开IDEA进行调整,默认为当前项目的根路径:

在这里插入图片描述
在这里插入图片描述

注意:

  • 在 IDEA 中,默认只会把 src/main/resources 和 src/test/resources 下的资源文件编译存放到类路径下。这意味着在编译后,这些资源文件会被打包到 JAR 或者 WAR 中,并且可以在运行时被访问到。这些资源文件包括配置文件、图片、XML 文件、JSON 文件等等。
  • 对于其他的文件,如源代码、Markdown 文档、Git 忽略文件等等,它们不会被编译和打包到 JAR 或者 WAR 中。这些文件通常只是在开发过程中使用,而不需要在生产环境中使用。
  • 如果您希望将其他的文件也打包到 JAR 或者 WAR 中,可以在 build.gradle 或者 pom.xml 中的构建配置中添加相应的配置。

注意:

  • src/main/resources目录下的资源文件是主代码的资源文件,会被编译到项目的classpath路径下,最终打包进入生成的jar包或war包中。
  • src/test/resources目录下的资源文件是测试代码的资源文件,不会被编译到项目的classpath路径下,只有在执行测试时才会将这些资源文件添加到测试类路径下,用于测试代码中的资源读取或者加载。
  • 测试代码
代码语言:javascript
复制
/**
 * 类路径资源定位测试
 */
public class ClassPathTest {
    @Test
    public void classPathTest(){
        //情况1: 资源寻找路径=加载ClassPathTest的类加载器的类路径作为basePath+ClassPathTest的包名('.'替换为'/'后的路径)+资源文件名
        URL resource0 = ClassPathTest.class.getResource("a.txt");
        URL resource1 = ClassPathTest.class.getResource("b.txt");
        //情况2: 资源寻找路径=加载ClassPathTest的类加载器的类路径作为basePath+资源文件名
        URL resource2 = ClassPathTest.class.getResource("/a.txt");
        URL resource3 = ClassPathTest.class.getResource("/test.txt");
        //情况3: 资源寻找路径=加载ClassPathTest的类加载器的类路径作为basePath+资源文件名
        URL resource4 = ClassPathTest.class.getClassLoader().getResource("a.txt");
        URL resource5 = ClassPathTest.class.getClassLoader().getResource("test.txt");
        URL resource6 = ClassPathTest.class.getClassLoader().getResource("./a.txt");
        URL resource7 = ClassPathTest.class.getClassLoader().getResource("./test.txt");
        //情况4: 获取不到,无法被解析为相对于classpath的路径
        URL resource8 = ClassPathTest.class.getClassLoader().getResource("/a.txt");
        URL resource9 = ClassPathTest.class.getClassLoader().getResource("/test.txt");

        System.out.println(resource0);
        System.out.println(resource1);
        System.out.println(resource2);
        System.out.println(resource3);
        System.out.println(resource4);
        System.out.println(resource5);
        System.out.println(resource6);
        System.out.println(resource7);
        System.out.println(resource8);
        System.out.println(resource9);
    }
}

上面所说的类路径并非只有一个路径,而是一类URL路径的集合,类加载器会挨个尝试将每个url path作为base path,去下面寻找资源,哪个路径下找到了,就直接返回。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  • 测试结果
在这里插入图片描述
在这里插入图片描述

URLClassLoader

在这里插入图片描述
在这里插入图片描述

URLClassLoader作为应用程序上下文类加载器,扩展类加载器和其他一些类加载器的公共父类,主要负责承载资源定位的公共逻辑,下面是java api文档对该类的定义(省略权限保护部分内容介绍):

在这里插入图片描述
在这里插入图片描述

URLClassLoader类装载器用于从引用 JAR 文件和目录的 URL 的搜索路径装入类和资源。任何以"/"结尾的 URL 都假定引用目录。否则,假定 URL 引用将根据需要打开的 JAR 文件。

对于URLClassLoader来说,最重要的概念就是searchPath:

代码语言:javascript
复制
public class URLClassLoader extends SecureClassLoader implements Closeable {
    // 用于搜索class类资源和resource资源的urls集合
    private final URLClassPath ucp;

URLClassPath也就是我们常说的类路径,类路径并非只有一个路径,而是一类URLS的集合,每个URL可以代表一个目录,一个jar,或者其他形式的资源。

我们来看一下用的最多的URLClassLoader的构造函数:

代码语言:javascript
复制
    public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        //设置双亲类加载器                   
        super(parent);
        ...
        //初始化类路径集合
        ucp = new URLClassPath(urls, factory, acc);
    }

URLClassLoader初始化时机

代码语言:javascript
复制
    public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        //设置双亲类加载器
        super(parent);
        ...
        //初始化类路径管理器
        ucp = new URLClassPath(urls, factory, acc);
    }

从findClass看起

URLClassLoader重写了父类ClassLoader的findClass方法,用于根据要加载的class文件全类名,借助URLClassPath定位class文件所在地址:

代码语言:javascript
复制
    protected Class<?> findClass(final String name)
            throws ClassNotFoundException {
        final Class<?> result;
        //将全类名的.替换为/ ,最后加上.class
        String path = name.replace('.', '/').concat(".class");
        //拿着包路径加文件名组成的相对路径,去当前类加载器管理的类路径下匹配查找
        Resource res = ucp.getResource(path, false);
        //资源存在,则执行后续类加载步骤: 装载,链接,初始化
        if (res != null) {
            result = defineClass(name, res);
        } else {
            return null;
        }
        //资源不存在
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

我们重点关注上面方法中通过URLClassPath的getResource方法完成class资源文件的定位。


从Class类提供的getResource和getResourceAsStream方法看起

代码语言:javascript
复制
    public java.net.URL getResource(String name) {
        //判断是否需要对资源路径进行标准化处理--什么是标准化处理,下面会讲
        name = resolveName(name);
        //获取加载当前类的类加载器
        ClassLoader cl = getClassLoader0();
        //如果为空,使用系统类加载器 --- Bootstrap ClassLoader加载的系统类,它们的类加载器都为null
        if (cl==null) {
            // A system class. ---> 加载系统资源,怎么实现的,一会看看便知
            return ClassLoader.getSystemResource(name);
        }
        //调用类加载的getResource方法获取资源
        return cl.getResource(name);
    }
代码语言:javascript
复制
     public InputStream getResourceAsStream(String name) {
        name = resolveName(name);
        ClassLoader cl = getClassLoader0();
        if (cl==null) {
            // A system class.
            return ClassLoader.getSystemResourceAsStream(name);
        }
        return cl.getResourceAsStream(name);
    }

resolveName方法说明:

  • 首先检查名称是否以’/'开头。
  • 如果是,它将去掉开头的’/'。
  • 如果不是,它将使用当前的Class类的包名来作为基础名称,然后将其与传入的名称组合起来来得到相对路径。
  • 这里,需要注意的是,如果Class是数组类型,则它的组件类型将的包名将被作为基础名称。
  • 最后,将相对路径转换为标准格式,并返回结果。
代码语言:javascript
复制
    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的getResource方法

类加载加载资源的方式遵循双亲委派机制,类文件的加载是资源文件加载的一种特殊情况:

代码语言:javascript
复制
    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的findResource方法是空实现,最终会调用到子类URLClassLoader的getResource方法
    protected URL findResource(String name) {
        return null;
    }    

URLClassLoader的findResource方法
代码语言:javascript
复制
    public URL findResource(final String name) {
        URL url =  ucp.findResource(name, true);
        return url != null ? ucp.checkURL(url) : null;
    }

可以看到最终的最终我们的资源查找逻辑都是交给URLClassPath负责完成的,所以下面跟随我的视角,一起来看看URLClassPath是怎么实现的吧。


类路径的实际表示–>URLClassPath

每个类加载器都有与之对应的 URLClassPath:

  • 应用(系统)类加载器 AppClassLoader 和 扩展类加载器 ExtClassLoader 都继承自 URLClassLoader,URLClassLoader有一个URLClassPath字段:
在这里插入图片描述
在这里插入图片描述
  • 启动类加载器对应的是null,它对应的URLClassPath是通过getBootstrapClassPath()方法获取的,参考ClassLoader.getBootstrapClassPath方法
代码语言:javascript
复制
private static URL getBootstrapResource(String name) {
    URLClassPath ucp = getBootstrapClassPath();
    Resource res = ucp.getResource(name);
    return res != null ? res.getURL() : null;
}

getBootstrapClassPath()最终获取的是sun.misc.Launcher.BootClassPathHolder.bcp字段,该字段的赋值过程,上面已经讲过了。

下面是java api文档对该类的介绍:

  • 此类用于维护 URL 的搜索路径,以便从 JAR 文件和目录加载类和资源。

URLClassPath用于维护从JAR包或目录中加载类或资源的查找路径,这个路径由若干个URL组成(URL封装在了Loader里面,一个Loader对应一个URL)。

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
    ArrayList<Loader> loaders = new ArrayList<Loader>();

URLClassPath包括一个ArrayList<Loader> loaders字段,LoaderURLClassPath的内部类,顾名思义,Loader是用来从JAR包或目录中加载类或资源的,它用于加载资源的方法是findResourcegetResource

注意,查看JarLoader和FileLoader的源码可以发现,findResource最终也会调用getResource:

在这里插入图片描述
在这里插入图片描述

Loader有两个实现类JarLoader和FileLoader,负责实际的资源加载任务,分别负责从JAR包和目录中加载资源。

每个Loader对应一个base URL,表示对应的JAR包或目录的URL。

在这里插入图片描述
在这里插入图片描述

FileLoader的getResource方法

Loader不管是用findResource还是getResource获取资源,最终都是调用getResource,这里以较为简单的FileLoader的getResource方法进行分析:

代码语言:javascript
复制
        Resource getResource(final String name, boolean check) {
            final URL url;
            try {
                //使用当前Loader管理的类路径作为baseUrl
                URL normalizedBase = new URL(getBaseURL(), ".");
                //baseUrl拼接资源相对路径得到完整的资源路径
                url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));
                //判断请求资源是否在类路径之内。
                //如果请求资源的路径中包含了 ../ 等导航符,则需要先进行归一化处理,再比较是否在类路径之内。
                //如果请求的资源不在类路径之内,则返回 null,表示未找到该资源。  
                if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
                    // requested resource had ../..'s in path
                    return null;
                }
                ...
                final File file;
                //判断资源相对路径中是否包含".."符号
                if (name.indexOf("..") != -1) {
                    //如果包含,那么将FileLoader的dir路径作为basePath--这里的dir路径也就是FileLoader能够处理的类路径
                    file = (new File(dir, name.replace('/', File.separatorChar)))
                          .getCanonicalFile();
                    //判断所请求的资源是否越过了类路径的范围
                    if ( !((file.getPath()).startsWith(dir.getPath())) ) {
                        /* outside of base dir */
                        return null;
                    }
                } else {
                    //资源相对路径中没有包含".."符号
                    //正常将当前FileLoader的类路径作为basePath
                    //拼接资源相对路径得到资源完整路径
                    file = new File(dir, name.replace('/', File.separatorChar));
                }
                
                //文件存在,那么构建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;
        }
在这里插入图片描述
在这里插入图片描述

URLClassPath的getResource/findResource方法

三种类加载器对资源的加载最终都是靠URLClassPath的getResource或findResoource方法完成的,而这两个方法又是借助loaders列表中的每一个loader来分别对指定的JAR包或目录进行资源加载的。

其中,应用类加载器和扩展类加载器会调用URLClassPath的findResource方法:

代码语言:javascript
复制
public URL findResource(String name, boolean check) {
    Loader loader;
    // 遍历loaders列表中的每一个loader,看看哪个Loader能够加载当前资源,如果能够加载,就直接返回
    for (int i = 0; (loader = getLoader(i)) != null; i++) {
        URL url = loader.findResource(name, check);
        if (url != null) { return url; }
    }
    return null;
}

启动类加载器会调用URLClassPath的getResource方法,该方法的逻辑和findResource几乎一样:

代码语言:javascript
复制
public Resource getResource(String name, boolean check) {
    if (DEBUG) {
        System.err.println("URLClassPath.getResource(\"" + name + "\")");
    }

    Loader loader;
    for (int i = 0; (loader = getLoader(i)) != null; i++) {
        Resource res = loader.getResource(name, check);
        if (res != null) { return res;}
    }
    return null;
}

前面介绍过,Loader负责从Jar包或目录下加载资源,每个Loader对应一个base URL。

这个base URL其实来源于bootstrap classpath或classpath中的每一个条目对应的URL,以及扩展目录下的每一个jar包对应的URL。

以AppClassLoader的URLClassPath对象为例,假设程序的classpath有3个条目,记为a;b;c,则URLClassPath对象有3个Loader,这3个Loader的base URL分别为a,b,c对应的URL,分别负责从这三个地方加载资源。

URLClassPath的loaders集合初始化采用的是懒加载策略,只有当第一次调用其findResource或者getResource方法时,才会进行初始化,这里以findResource方法为例进行讲解:

代码语言:javascript
复制
   //遍历当前URLClassPath内部所有Loader,挨个尝试加载资源,哪个先成功,就直接返回
    public URL findResource(String name, boolean check) {
        Loader loader;
         ...
        //重点在getNextLoader第一次被调用的时,会对loaders集合进行初始化
        for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
            URL url = loader.findResource(name, check);
            if (url != null) {
                return url;
            }
        }
        return null;
    }
代码语言:javascript
复制
    private synchronized Loader getNextLoader(int[] cache, int index) {
        if (closed) {
            return null;
        }
        //...
        return getLoader(index);
    }

getLoader方法负责根据下标返回一个Loader,具体逻辑如下:

代码语言:javascript
复制
private synchronized Loader getLoader(int index) {
        if (closed) {
            return null;
        }
         //按需创建Loader
        while (loaders.size() < index + 1) {
            // Pop the next URL from the URL stack
            URL url;
            //当urls栈为空时,说明所有Loaders都完成了初始化
            //也说明此时没有Loa
            synchronized (urls) {
                if (urls.empty()) {
                    return null;
                } else {
                    url = urls.pop();
                }
            }
            //检查给定的URL是否已经被处理过,如果已经被处理过,则跳过该URL,
            //URLClassPath内部维护了一个lmap,用来保存已经处理过的URL和对应的Loader;
            //给定的URL在lmap中已经存在,则说明该URL已经被处理过,可以直接跳过; 
            //如果给定的URL在lmap中不存在,则说明该URL尚未被处理过,需要创建对应的Loader,并加入到lmap和loaders列表中。
            String urlNoFragString = URLUtil.urlNoFragString(url);
            if (lmap.containsKey(urlNoFragString)) {
                continue;
            }
            Loader loader;
            try {
                //通过给定的URL创建并返回一个新的Loader对象
                //根据URL的不同,可以创建不同类型的Loader。
                //如果URL指向一个本地文件,就会创建FileLoader;
                //URL指向一个Jar文件,就会创建JarLoader。
                loader = getLoader(url);
                // If the loader defines a local class path then add the
                // URLs to the list of URLs to be opened.
                //在对于新的URL进行处理时,会获取这个URL对应的Loader,并查看Loader是否定义了本地的类路径。
                //如果定义了本地类路径,那么就会将本地类路径中的URL加入到URL栈中,
                //这样在后续的查找资源的过程中就可以继续遍历这些URL。
                URL[] urls = loader.getClassPath();
                if (urls != null) {
                    push(urls);
                }
            } ...
            // Finally, add the Loader to the search path.
            ...
            //将新创建的Loader,加入URLClasspath的loader集合汇总
            loaders.add(loader);
            //记录已经加载的 URL 对应的 Loader 对象
            //URL 在集合中不存在时,将会创建一个新的 Loader 对象,并添加到集合中,以便进行缓存,避免重复加载相同的 URL。
           // URL 在集合中已经存在时,则跳过该 URL,不再进行加载操作。这样可以有效地避免重复加载相同的 URL,提高了加载的效率。
            lmap.put(urlNoFragString, loader);
        }
        ...
        return loaders.get(index);
    }

getLoader方法的逻辑设计的十分巧妙,其中的一段while循环完成了对Loaders集合的按需加载,具体思路如下图所示:

在这里插入图片描述
在这里插入图片描述
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-05-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 踩坑集锦之你真的明白Java类路径的含义吗?
  • 前置知识补充
  • 故事还要从程序启动讲起…
    • C++和Java的桥接类LauncherHelper
      • 主类是如何被加载的
        • 加餐: 如何利用jdk预留的口子,替换系统类加载器为我们自定义的类加载器
      • Launcher启动类的初始化
        • 启动类加载器类路径如何确定的
        • 扩展类加载器初始化时机
        • 应用程序类加载器初始化时机
    • 前置知识补充
    • URLClassLoader
      • URLClassLoader初始化时机
        • 从findClass看起
          • 从Class类提供的getResource和getResourceAsStream方法看起
            • 类路径的实际表示–>URLClassPath
              • FileLoader的getResource方法
              • URLClassPath的getResource/findResource方法
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档