java.lang.UnsatisfiedLinkError: 另一个类加载器已经加载了本地库XXX.so

19

我部署了一个网络应用程序,其中包含以下代码。

System.loadLibrary(org.opencv.core.Core.NATIVE_LIBRARY_NAME);

现在,我部署了另一个具有相同代码的 Web 应用程序。当它尝试加载库时,会抛出以下错误。

Exception in thread "Thread-143" java.lang.UnsatisfiedLinkError: 
Native Library /usr/lib/jni/libopencv_java248.so already loaded in
another classloader

我希望同时运行这两个应用程序。

到目前为止,我尝试过以下方法:

  1. 在一个应用程序中加载库,并在另一个应用程序中捕获上述异常
  2. 从两个应用程序中删除jar文件,并将opencv.jar放入Tomcat的类路径中(即/usr/share/tomcat7/lib)。

但是以上方法都不起作用,您有什么建议可以让我实现这个功能吗?

编辑:对于第二个选项,

System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

这行代码可以正常工作,但当我实际使用该库时会出现异常。具体来说,当我执行以下操作时:

Mat mat = Highgui.imread("/tmp/abc.png");

然后我收到了这个异常

java.lang.UnsatisfiedLinkError: org.opencv.highgui.Highgui.imread_1(Ljava/lang/String;)J
    at org.opencv.highgui.Highgui.imread_1(Native Method)
    at org.opencv.highgui.Highgui.imread(Highgui.java:362)

第二个应该可以工作。你确定在其他地方已经删除了它,并且没有其他的jar文件在webapp中尝试加载这个库吗? - user2543253
@user2543253 请检查我编辑过的问题。 - Bhushan
你可以检查一下 Highgui 是否被同一个类加载器加载了,它是否做了 loadLibrary 的操作?否则本地方法将无法初始化。 - user2543253
嗨,@user2543253,为了简单起见,我删除了我的第二个应用程序。现在我只部署了一个应用程序,它不包含opencv jar文件。我将其放在/usr/share/tomcat7/lib/目录下。但我仍然遇到"java.lang.UnsatisfiedLinkError: org.opencv.highgui.Highgui.imread_1(Ljava/lang/String;)J"异常。 - Bhushan
我该如何检查“Highgui是否由执行loadLibrary操作的同一类加载器加载?” - Bhushan
我在答案中写了一个解释。 - user2543253
4个回答

19
问题在于OpenCV如何处理本地库的初始化。
通常使用本地库的类都会有一个静态初始化器来加载库。这样,类和本地库将始终在同一类加载器中加载。但是对于OpenCV,应用程序代码会加载本地库。
现在有一个限制,即本地库只能在一个类加载器中加载。 Web应用程序使用自己的类加载器,因此如果一个Web应用程序已经加载了本地库,则另一个Web应用程序无法执行相同操作。因此,加载本地库的代码不能放在Web应用程序目录中,而必须放在容器(Tomcat)的共享目录中。当您使用上述常规模式编写的类(在使用类的静态初始化器中使用loadLibrary)时,只需要将包含该类的jar文件放入共享目录中即可。但是,对于OpenCV和Web应用程序代码中的loadLibrary调用,本地库仍将在“错误”的类加载器中加载,从而导致UnsatisfiedLinkError。
为了使“正确”的类加载器加载本地库,您可以创建一个仅具有单个静态方法(仅执行loadLibrary)的小类。将此类放入额外的jar文件中,并将此jar文件放入共享Tomcat目录中。然后在Web应用程序中,将对System.loadLibrary的调用替换为对新静态方法的调用。这样,OpenCV类及其本地库的类加载器将匹配,本地方法可以初始化。
编辑:按评论者要求提供示例。
而是使用:
public class WebApplicationClass {
    static {
        System.loadLibrary(org.opencv.core.Core.NATIVE_LIBRARY_NAME);
    }
}

使用

public class ToolClassInSeparateJarInSharedDirectory {
    public static void loadNativeLibrary() {
        System.loadLibrary(org.opencv.core.Core.NATIVE_LIBRARY_NAME);
    }
}

public class WebApplicationClass {
    static {
        ToolClassInSeparateJarInSharedDirectory.loadNativeLibrary();
    }
}

我怎样在我的项目中使用SharedDirectory中的ToolClassInSeparateJarIn类呢?我需要创建provided依赖还是在我的项目中创建同样的类? - rackom
@rackom 我不确定我理解问题。上面的代码只是一个例子。如果你想使用它,你可以在自己的项目中创建类似这样的代码。 - user2543253
这个设置无法与opencv4.0.1一起工作。有什么想法吗?我正在使用Spring-boot,并尝试将opencv4加载到我的应用程序中。谢谢。 - delkant
不知道。最好创建一个新的问题,提供代码片段和确切的错误信息。 - user2543253
从Tomcat版本9.0.13、8.5.35和7.0.92开始,已经有了内置的解决方案。请参见下面的答案以获取详细信息。 - isapir
此外,对于已加载的本地库所属的本地代码发出的所有调用必须位于加载本地库的同一共享类加载器中。 - singidunumx

2
截至Tomcat版本9.0.13、8.5.35和7.0.92,我们已添加以下选项以解决此问题BZ-62830
1)使用JniLifecycleListener加载本地库。
例如,要加载opencv_java343库,您可以使用:
<Listener className="org.apache.catalina.core.JniLifecycleListener"
          libraryName="opencv_java343" />

2) 使用 load() 或者 loadLibrary()org.apache.tomcat.jni.Library 中调用,而不是使用 System

例如:

org.apache.tomcat.jni.Library.loadLibrary("opencv_java343");

使用这两个选项之一将使用通用类加载器来加载本机库,因此它将对所有Web应用程序可用。

1
我花了很多时间去尝试,但不幸的是对我没有用。在Tomcat(v8.5.38)的日志中,我看到“[信息] Loaded native library xxx”,但当调用已部署Web应用程序的本地方法时,出现了“java.lang.UnsatisfiedLinkError:com.package.XxxxYyyy.methodZzz(Ljava /lang /String;)Ljava /lang /String;“。我已经测试了选项1和2,分别使用,但都没有成功。我真的很希望它能工作,但事实证明并非如此。 - marioosh
@marioosh 你确定本地库放在正确的位置了吗?它能从任何类加载器中工作还是根本不能工作? - isapir
@marioosh,你解决了这个问题吗?我也遇到了同样的问题。无论我怎么做都无法避免不满足的链接错误 - 尝试将监听器放在context.xml、server.xml中并以编程方式调用它,但什么都不起作用。我看到它很好地加载了库,但无法调用它。非常令人沮丧,因为似乎没有其他人遇到这个问题,或者可能只是没有人经常尝试这样做? - user3062913
在服务器、主机和上下文级别上使用JniLifecycleListener也没有成功。分享本地库的最终解决方案是,在JNI API上打包一个包装器类,并编辑catalina.properties文件,将shared.loader="${catalina.base}/RUN/*.jar放置在包装器jar和相关依赖项(例如日志记录jar)中,放置在${catalina.base}/RUN/位置。相同的依赖项从应用程序WAR中删除(maven:provided)。包装器类具有静态{System.loadLibrary("myNativeLib");}部分,以便在catalina共享类加载器中加载本地库。像微风一样工作。 - Bernard Hauzeur
可以尝试这样做:将你的jar文件放到${catalina.base}/lib中,然后将你的maven依赖jar的范围(scope)更改为“provided”。祝你好运! - Nguyen Duc Dung

0

我卡在了这个问题上。

在Tomcat(v8.5.58)的server.xml文件中添加监听器似乎成功加载了dll文件(至少日志是这样说的),但是当调用本地方法时,它会失败并显示java.lang.UnsatisfiedLinkError。

无论是否在我的Java代码中调用“org.apache.tomcat.jni.Library.loadLibrary(“TeighaJavaCore”)”,都没有任何区别,仍然出现相同的错误。我在项目中包含了tomcat-jni依赖项以启用“org.apache.tomcat.jni.Library.loadLibrary(“TeighaJavaCore”)”调用。虽然我猜在Java代码中(在Web应用程序级别)没有必要调用“org.apache.tomcat.jni.Library.loadLibrary(“TeighaJavaCore”)”,因为TeighaJavaCore.dll将在Tomcat启动时自动加载(因为上面的监听器是为此目的在Tomcat容器级别定义的)

我还在这里检查了“org.apache.tomcat.jni.Library.loadLibrary”的源代码,它只是调用“System.loadLibrary(libname)”。

https://github.com/apache/tomcat-native/blob/master/java/org/apache/tomcat/jni/Library.java


-1

从javacpp>=1.3开始,您还可以在war部署监听器中更改缓存文件夹(由系统属性定义):

System.setProperty("org.bytedeco.javacpp.cachedir",
                   Files.createTempDirectory( "javacppnew" ).toString());

请注意,本地库始终会被解压缩并加载多次(因为被视为不同的库)。

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