关于 java.library.path、classpath、path 以及 java 程序一些依赖相关的理解

介绍

  • java.library.path:Java 的属性。启动时通过 -Djava.library.path 指定;用来指定非 java 类包的位置。如:dll,so。
  • classpath:Java 启动时指定的类路径。如 .class 文件,.jar 文件。
  • path:一个系统环境变量,声明命令的搜索路径,让操作系统找到指定的工具程序。(jvm 也能直接读取 path 里面的动态链接库)

关于 java 执行时需要的依赖

  • class 依赖:通过 classpath 指定。
  • native 依赖:直接调用动态链接库(dll,so文件)。可以通过 System.loadLibrary(xxx) 加载指定的动态链接库。但是动态链接库搜索路径必须通过以下方式指定:
    • 可以通过 java.library.path 指定,会读取到 ClassLoaderusr_paths 中。
    • 也能够直接读取 path 变量里面的动态链接库。

如何动态指定java依赖

Java类由于需要加载和编译字节码,虽然动态加载 class 文件较为麻烦,不像C加载动态链接库只要一个文件名就可以搞定,但 JDK 仍提供了一整套方法来动态加载 jar 文件和 class 文件。

		/**
     * 加载 jar 文件
     */
    public static void loadJar(String jarPath) {
        // 系统类库路径
        File file = new File(jarPath);
        if (!file.exists()) {
            return;
        }

        if (file.isDirectory()) {
            Optional.ofNullable(file.listFiles((dir, name) -> name.endsWith(".jar") || name.endsWith(".zip")))
                    .ifPresent(files -> Stream.of(files).forEach(ClassUtils::addToLoader));
        } else {
            addToLoader(file);
        }
    }

    /**
     * 加载 class 文件
     *
     * @param classPath 类文件的根路径。不含包路径
     */
    public static void loadClass(String classPath) {
        File classFile = new File(classPath);
        if (classFile.exists() && classFile.isDirectory()) {
            addToLoader(classFile);
            // 获取路径长度
            int clazzPathLen = classFile.getAbsolutePath().length() + 1;
            Deque deque = new LinkedList<>();
            deque.push(classFile);
            // 遍历类路径
            while (!deque.isEmpty()) {
                Optional.ofNullable(deque.pop().listFiles(f -> f.isDirectory() || f.getName().endsWith(".class"))).ifPresent(files -> Stream.of(files).forEach(subFile -> {
                    if (!subFile.isDirectory()) {
                        try {
                            // 文件名称
                            String className = subFile.getAbsolutePath();
                            className = className.substring(clazzPathLen, className.length() - 6).replace(File.separatorChar, '.');
                            Class.forName(className);
                            LoggerUtils.debug(ClassUtils.class, MessageFormat.format("读取到类文件[class={0}]", className));
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }
                    } else {
                        deque.push(subFile);
                    }
                }));
            }
        }
    }

    @SuppressWarnings("java:S3011")
    private static void addToLoader(File file) {
        // region 将当前类路径加入到类加载器中
        Method method = null;
        boolean accessible = false;
        try {
            method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            accessible = method.isAccessible();
            method.setAccessible(true);
            method.invoke(ClassLoader.getSystemClassLoader(), file.toURI().toURL());
        } catch (NoSuchMethodException | MalformedURLException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        } finally {
            if (method != null) {
                method.setAccessible(accessible);
            }
        }
        // endregion
    }
  • 如果是 jar 文件,则通过上面的 loadJar(jarPath) 加载。
  • 如果是 class 文件夹,则通过上面的 loadClass(lassPath) 加载。

之后就可以用 Class.forName 来加载 jar 中或 class 文件包含的 Java 类了。

如何动态指定native依赖

其中 java.library.path 的读取和使用都在 ClassLoader 中:

// The paths searched for libraries
private static String usr_paths[];
private static String sys_paths[];

// Invoked in the java.lang.Runtime class to implement load and loadLibrary.
static void loadLibrary(Class fromClass, String name,
                        boolean isAbsolute) {
    ClassLoader loader =
        (fromClass == null) ? null : fromClass.getClassLoader();
    if (sys_paths == null) {
        usr_paths = initializePath("java.library.path");
        sys_paths = initializePath("sun.boot.library.path");
    }
    /*....*/
}

当第一次读取后 sys_paths 不为空,后面就不会再读取 java.library.path 属性了。

那么,程序启动后,如何修改 java.library.path 并使其生效呢?或者说如何动态加载一个不在启动时指定的动态链接库搜索路径下面呢?

其实根据这里加载的代码,可以通过反射的方式来实现需求(虽然不建议使用)。

public static void addLibraryDir(String libraryPath) throws IOException {  
    try {  
        Field field = ClassLoader.class.getDeclaredField("usr_paths");  
        field.setAccessible(true);  
        String[] paths = (String[]) field.get(null);  
        for (int i = 0; i < paths.length; i++) {  
            if (libraryPath.equals(paths[i])) {  
                return;  
            }  
        }  

        String[] tmp = new String[paths.length + 1];  
        System.arraycopy(paths, 0, tmp, 0, paths.length);  
        tmp[paths.length] = libraryPath;  
        field.set(null, tmp);  
    } catch (IllegalAccessException e) {  
        throw new IOException(  
                "Failedto get permissions to set library path");  
    } catch (NoSuchFieldException e) {  
        throw new IOException(  
                "Failedto get field handle to set library path");  
    }  

// 		System.loadLibrary(xxx);
}

或者可以用反射将 sys_paths 修改为 null,然后修改 java.library.path。再用 System.loadLibrary(xxx) 加载。

需要注意的是,如果,使用 -Djava.library.path 指定了 xxx.dll 后,用 System.loadLibrary(xxx) 加载动态链接库时,虽然能找到动态链接库 xxx.dll,但是如果 xxx.dll 有依赖的动态链接库找不到,则会报错:Can’t find dependent libraries xxx。这种情况,最好使用 path 指定。

你可能感兴趣的:(jar,java)