从类路径加载本地库

6

我有一个项目设置,遵循标准目录结构(虽然没有使用Maven):

src/main
      | java
      | resources
         | library.dll

本地DLL文件位于资源文件夹中,源文件位于Java文件夹中。资源文件夹是Java类路径的成员。

我现在想要加载一个DLL文件,而不必设置JRE -Djava.library.path选项或设置PATH变量,这样生成的jar文件可以通过简单的双击启动。

是否有可能将资源文件夹添加到库搜索路径中,而无需在运行jar文件时进行额外的配置? 例如,在清单中类似于Class-Path的设置?

3个回答

4

我在JNativeHook中做过非常类似的事情,你需要一些辅助代码来确定正确的架构和操作系统以加载代码(请参见NativeSystem类)。

// The following code covered under the GNU Lesser General Public License v3.
static {
    String libName = System.getProperty("jnativehook.lib.name", "JNativeHook");

    try {
        // Try to load the native library assuming the java.library.path was
        // set correctly at launch.
        System.loadLibrary(libName);
    }
    catch (UnsatisfiedLinkError linkError) {
        // Get the package name for the GlobalScreen.
        String basePackage = GlobalScreen.class.getPackage().getName().replace('.', '/');

        // Compile the resource path for the native lib.
        StringBuilder libResourcePath = new StringBuilder("/");
        libResourcePath.append(basePackage).append("/lib/");
        libResourcePath.append(NativeSystem.getFamily()).append('/');
        libResourcePath.append(NativeSystem.getArchitecture()).append('/');


        // Get what the system "thinks" the library name should be.
        String libNativeName = System.mapLibraryName(libName);
        // Hack for OS X JRE 1.6 and earlier.
        libNativeName = libNativeName.replaceAll("\\.jnilib$", "\\.dylib");

        // Slice up the library name.
        int i = libNativeName.lastIndexOf('.');
        String libNativePrefix = libNativeName.substring(0, i) + '-';
        String libNativeSuffix = libNativeName.substring(i);
        String libNativeVersion = null;

        // This may return null in some circumstances.
        InputStream libInputStream = GlobalScreen.class.getResourceAsStream(libResourcePath.toString().toLowerCase(Locale.English) + libNativeName);
        if (libInputStream != null) {
            try {
                // Try and load the Jar manifest as a resource stream.
                URL jarFile = GlobalScreen.class.getProtectionDomain().getCodeSource().getLocation();
                JarInputStream jarInputStream = new JarInputStream(jarFile.openStream());

                // Try and extract a version string from the Manifest.
                Manifest manifest = jarInputStream.getManifest();
                if (manifest != null) {
                    Attributes attributes = manifest.getAttributes(basePackage);

                    if (attributes != null) {
                        String version = attributes.getValue("Specification-Version");
                        String revision = attributes.getValue("Implementation-Version");

                        libNativeVersion = version + '.' + revision;
                    }
                    else {
                        Logger.getLogger(GlobalScreen.class.getPackage().getName()).warning("Invalid library manifest!\n");
                    }
                }
                else {
                    Logger.getLogger(GlobalScreen.class.getPackage().getName()).warning("Cannot find library manifest!\n");
                }
            }
            catch (IOException e) {
                Logger.getLogger(GlobalScreen.class.getPackage().getName()).severe(e.getMessage());
            }


            try {
                // The temp file for this instance of the library.
                File libFile;

                // If we were unable to extract a library version from the manifest.
                if (libNativeVersion != null) {
                    libFile = new File(System.getProperty("java.io.tmpdir"), libNativePrefix + libNativeVersion + libNativeSuffix);
                }
                else {
                    libFile = File.createTempFile(libNativePrefix, libNativeSuffix);
                }

                byte[] buffer = new byte[4 * 1024];
                int size;

                // Check and see if a copy of the native lib already exists.
                FileOutputStream libOutputStream = new FileOutputStream(libFile);

                // Setup a digest...
                MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
                DigestInputStream digestInputStream = new DigestInputStream(libInputStream, sha1);

                // Read from the digest stream and write to the file steam.
                while ((size = digestInputStream.read(buffer)) != -1) {
                    libOutputStream.write(buffer, 0, size);
                }

                // Close all the streams.
                digestInputStream.close();
                libInputStream.close();
                libOutputStream.close();

                // Convert the digest from byte[] to hex string.
                String sha1Sum = new BigInteger(1, sha1.digest()).toString(16).toUpperCase();
                if (libNativeVersion == null) {
                    // Use the sha1 sum as a version finger print.
                    libNativeVersion = sha1Sum;

                    // Better late than never.
                    File newFile = new File(System.getProperty("java.io.tmpdir"), libNativePrefix + libNativeVersion + libNativeSuffix);
                    if (libFile.renameTo(newFile)) {
                        libFile = newFile;
                    }
                }

                // Set the library version property.
                System.setProperty("jnativehook.lib.version", libNativeVersion);

                // Load the native library.
                System.load(libFile.getPath());

                Logger.getLogger(GlobalScreen.class.getPackage().getName())
                        .info("Library extracted successfully: " + libFile.getPath() + " (0x" + sha1Sum + ").\n");
            }
            catch (IOException e) {
                throw new RuntimeException(e.getMessage(), e);
            }
            catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
        else {
            Logger.getLogger(GlobalScreen.class.getPackage().getName())
                    .severe("Unable to extract the native library " + libResourcePath.toString().toLowerCase(Locale.English) + libNativeName + "!\n");

            throw new UnsatisfiedLinkError();
        }
    }
}

1
这仍然假定了一个硬编码的资源路径,其中 libResourcePath = "/org/jnativehook/lib/" - 类路径是否可以像此类动态生成?即Eclipse允许您将文件夹指定为Class-Folder,该文件夹显然已添加到搜索路径中。对于清单,类似的功能也很有趣。 - mschrimpf
1
这将是一个动态的包名,如果您遍历整个包树寻找特定的文件名,则有可能实现。该文件名将需要针对操作系统进行特定设置,并且搜索可能需要很长时间。(例如:x86_64_windows.bin)请参见:https://dev59.com/OXVC5IYBdhLWcg3w0EoD和https://dev59.com/gHI-5IYBdhLWcg3whYwr - Alex Barker
1
你的NativeSystem类有一个错误。土耳其语的问题。你的toString()方法应该使用固定的Locale调用小写字母。否则会出现“无法提取本地库/org/jnativehook/lib/wındows/x86/JNativeHook.dll!”的错误。请注意土耳其语中的小写字母"i"是"ı",而不是"i"。 - acheron55

3

我想提供一种替代方案,因为@Java42的答案不可移植(依赖于JVM),并且在Oracle/OpenJDK的Java 12及以上版本中无法使用。

你应该使用你自己的定制ClassLoader实现。在ClassLoader中有一个方法叫做findLibary(String libname)。这个方法返回要加载的库的完整路径。这可以用来在任意位置加载库。

public class MyClassLoader extends ClassLoader {
    @Override
    protected String findLibrary(String libname) {
        return "/actual/path/to/library.so";
        // or return null if unknown, then the path will be searched
    }
}

下一步是让JVM使用您自定义的ClassLoader。因此,请在代码中很快将其设置为当前线程的contextClassLoader

    public static void main(String[] args) {
        Thread.currentThread().setContextClassLoader(new StarterClassLoader());
        // ... your code
    }

3

有一种古老的技巧,即使在今天 (1.7.0_55 & 1.8.0_05) 仍然有效,允许您使用System.setProperty()进行运行时更新,并使JVM注意到更改。通常,您需要执行以下操作:

System.setProperty("java.library.path", yourPath);
Field sysPath = ClassLoader.class.getDeclaredField( "sys_paths" );
sysPath.setAccessible( true );
sysPath.set( null, null );
System.loadLibrary(libraryName);

请搜索有关此技术的文章:Google java sys_paths。

请注意处理错误/异常。 如有需要,请恢复原始路径。


不幸的是,这似乎无法在Java 1.8中工作。 - mschrimpf
2
@Mash - 你是否也测试了1.7或只有1.8?我将我的一个环境升级到1.8.0_05,使用sysPath.set(null,null)进行运行时修改java.library.path,一切正常。请再次检查你的实现。 - Java42
我实际上无法重现它 - 在生成MWE的过程中,我发现如果我创建一个新模块(使用IntelliJ)并将旧的错误模块的所有内容复制到其中,它就可以工作。尽管这些模块是相同的。 甚至不需要进行任何额外的计算,IntelliJ现在似乎已经正确处理了它。 对此我感到很抱歉,但我猜这可能是某种IDE错误。 - mschrimpf

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