-Djava.library.path=... 等价于 System.setProperty("java.library.path", ...) 吗?

55
我加载了一个放在./lib目录下的外部库。以下两种方法设置java.library.path等效吗?
  1. 在执行jar时在控制台中设置路径:

    java -Djava.library.path=./lib -jar myApplication.jar
    
  2. 在加载库之前在代码中设置路径:

  3. System.setProperty("java.library.path", "./lib");
    

如果它们是等效的,为什么第二个解决方案中Java找不到库,而第一个解决方案是可以的?

如果它们不等效,是否有一种方法在代码中设置路径?


java.library.path 指的是一个目录,而不是一个文件。 - jacktrades
1
从Java 13开始,API文档指出 --- “属性值可能在初始化期间或首次使用时被缓存。在使用getProperties()、setProperties(Properties)、setProperty(String, String)或clearProperty(String)进行初始化后设置标准属性可能无法产生预期的效果”。 - fountainhead
5个回答

59
尽管它没有很好的文档记录,但是对于System.loadLibrary()方法来说,java.library.path系统属性是一个“只读”属性。这是一个已报告的错误,但是Sun关闭了这个错误,而没有修复它。问题在于JVM的ClassLoader在启动时只读取一次这个属性,然后将其缓存起来,不允许我们在程序运行后通过编程方式更改它。除了System.getProperty()方法调用之外,System.setProperty("java.library.path", anyVal);这一行代码将没有任何效果。
幸运的是,有人在Sun论坛上发布了一个解决方法。不幸的是,那个链接已经失效了,但是我在另一个来源找到了这段代码。下面是你可以用来解决无法设置java.library.path系统属性的代码:
public static void addDir(String s) throws IOException {
    try {
        // This enables the java.library.path to be modified at runtime
        // From a Sun engineer at http://forums.sun.com/thread.jspa?threadID=707176
        //
        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 (s.equals(paths[i])) {
                return;
            }
        }
        String[] tmp = new String[paths.length+1];
        System.arraycopy(paths,0,tmp,0,paths.length);
        tmp[paths.length] = s;
        field.set(null,tmp);
        System.setProperty("java.library.path", System.getProperty("java.library.path") + File.pathSeparator + s);
    } catch (IllegalAccessException e) {
        throw new IOException("Failed to get permissions to set library path");
    } catch (NoSuchFieldException e) {
        throw new IOException("Failed to get field handle to set library path");
    }
}

警告:这可能在所有平台和/或JVM上都无法正常工作。


7
请查看 @luyifan 提供的这个新答案,它似乎以更少的代码实现了与此答案相同的功能。 - Jesse Webb
@luyifan的新答案由于以下错误在Java 11上无法工作。 java.lang.NoClassDefFoundError: Could not initialize class java.util.zip.CRC32 但是您的代码可以正常运行,没有任何错误。 - MinseongPark
这个答案真的帮了我很大的忙。我在下面发布了一个Java 17的补充说明:https://dev59.com/-m035IYBdhLWcg3wcviS#70126075 - kevinarpe

54

一般来说,两种方法的净效果是相同的,都会将系统属性java.library.path设置为值./lib

然而,某些系统属性只在特定时间点进行评估,例如JVM启动时。 如果 java.library.path属于这些属性(根据您的实验显示),那么除了在以后调用getProperty()时返回新值之外,使用第二种方法将没有明显影响。

作为经验法则,使用-D命令行属性可在所有系统属性上工作,而System.setProperty()仅适用于不仅在启动期间检查的属性。


45

您可以添加三行

 System.setProperty("java.library.path", "/path/to/libs" );
 Field fieldSysPath = ClassLoader.class.getDeclaredField( "sys_paths" );
 fieldSysPath.setAccessible( true );
 fieldSysPath.set( null, null );

还需要导入java.lang.reflect.Field。 只要解决问题就可以了。


3
这很有效(比 Jesse Webb 提出的解决方案中自己构建临时路径更简单)。 - Kevin Day
2
这个对我们来说可真是救命恩人啊。Sun/Oracle 没有提供一种更为直接的方式,从一个特定的、运行时定义的目录中加载库,看起来有些马虎了。 - birgersp
4
遗憾的是,在Java 10中,它被禁止了并将被删除("illegal reflective access operation has occurred")。 - Xdg
1
需要注意的是,这在不同的JVM之间并不可移植。特别是在IBM J9 JVM上无法工作。但我已经在Oracle和Adopt OpenJDK上成功运行过。 - Addison
@luyifan 如何处理多个库路径?例如“/path/to/lib1”,“/path/to/lib2”。 - frank jorsn

2
这是对Jesse Webb惊人答案的补充说明:https://dev59.com/-m035IYBdhLWcg3wcviS#6408467 对于Java 17:
import jdk.internal.loader.NativeLibraries;
final Class<?>[] declClassArr = NativeLibraries.class.getDeclaredClasses();
final Class<?> libraryPaths =
    Arrays.stream(declClassArr)
        .filter(klass -> klass.getSimpleName().equals("LibraryPaths"))
        .findFirst()
        .get();
final Field field = libraryPaths.getDeclaredField("USER_PATHS");
final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
final VarHandle varHandle = lookup.findVarHandle(Field.class, "modifiers", int.class);
varHandle.set(field, field.getModifiers() & ~Modifier.FINAL);

由于来自模块 java.base 的包 jdk.internal.loader 通常不可访问,因此您需要在编译器和JVM运行时参数中添加 "exports" 和 "opens"。

--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED
--add-opens=java.base/jdk.internal.loader=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED

点击以下链接了解更多信息:


1
在Temurin JDK设置中,将sys_paths设为null是无效的。这会导致NullPointerException的发生。
以下方法在Temurin中有效:
Method initLibraryPaths = ClassLoader.class.getDeclaredMethod("initLibraryPaths");
initLibraryPaths.setAccessible(true);
initLibraryPaths.invoke(null);

1
原因不是因为版本,而是因为JVM实现不同。 你介绍的方式似乎是Temurin JDK的实现。 - Heedo Lee
1
@HeedoLee 你说得对。谢谢你指出来。我已经修正了我的回答。 - Joe23

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接