-Djava.library.path
指定;用来指定非 java 类包的位置。如:dll,so。ClassLoader
的 usr_paths
中。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
}
之后就可以用 Class.forName 来加载 jar 中或 class 文件包含的 Java 类了。
其中 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 指定。