Java web 项目中使用JNI技术(如何在程序运行期间改变 java.library.path并生效)

记录结构:
JNI技术入门详解,参照刚哥的手记:http://www.jianshu.com/p/fe42aa3150a0
注意:刚哥手记与接下来要记录的web项目中使用JNI技术是无缝连接的。
应用场景:当我们根据不同的平台生成不同的JNI libaray时,例如:linux .so、mac jnilib、windows .dll。我们想在打包web 应用时让程序动态调用c,或者c++对Java Native Inteface 的具体底层实现时,有一种方法是借助配置在idea中的vm option中设置库文件所在的路径,即-Djava.path.library,刚哥手记最后一部分有说明。
精准定位问题:

1.那么有没有另外一种方式使得Java 程序在调用native inteface 中抽象本地方法自动加载所需要的代码呢?也就是说应用程序自动加载.so || (或).jnilib** || **.dll?。
2.我们知道Java 应用程序在调用底层代码生成的库文件时,需要指定库文件所在的path。那么我们的问题就清晰了,问题的痛点在于如何让应用程序在程序运行期间动态加载库文件所在的路径,进而加载所需的库文件。
网上的一种说法是:在使用System.loadLibrary("具体库文件所在的路径的相对路径"),之前使用System.load("具体库文件所在的根目录的全路径"),本人试了一下,发现并不起作用。

继续找解决方案,无意中发现了一篇博客,博客地址是:http://ju.outofmemory.cn/entry/150717
这篇文章讲述的是如何在运行时改变 java.library.path并生效。
我想这正是我要的答案,无奈是英文的,还是硬着头皮看吧
首先开篇很简明扼要说明问题:

The java.library.path
system property instructs the JVM where to search for native libraries. You have to specify it as a JVM argument using -Djava.library.path=/path/to/lib
and then when you try to load a library using System.loadLibrary("foo")
, the JVM will search the library path for the specified library. If it cannot be found you will get an exception which looks like:

大致的意思是:

系统属性- java.library.path指引JVM去寻找底层的库文件,你必须为JVM声明一个属性,类似于Djava.library.path=/path/to/lib ,当你需要使用System.loadLibrary("foo")加载底层foo库文件的时候,jvm会按照你声明的path去加载这个库文件,如果你不声明的话,会出现下面错误:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no foo in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1734)
    at java.lang.Runtime.loadLibrary0(Runtime.java:823)
    at java.lang.System.loadLibrary(System.java:1028)

这个错告诉我们foo库并不在我们所要加载的路径下面。
接下来说明原因:

The java.library.path
is read only once when the JVM starts up. If you change this property usingSystem.setProperty
, it won’t make any difference.

意思是:java.library.path 只会在JVM启动的时候被都到,如果你直接使用
System.setProperty("java.path.libarary","库所在路径")这样是不起作用的,因为JVM已经启动了。所以这个JVM后期不能找到这个库文件所在的路径,所以就报如上错误。
源码中ClassLoader.loadLibrary有这样一句代码:

if (sys_paths == null) {
    usr_paths = initializePath("java.library.path");
    sys_paths = initializePath("sun.boot.library.path");
}

为什么就定位问题到上述几行代码,我们得从源码的角度来分析,看下源码:
首先是System.loadLibaray(),借助idea看下源码:

  /**
     * Loads the native library specified by the <code>libname</code>
     * argument.  The <code>libname</code> argument must not contain any platform
     * specific prefix, file extension or path. If a native library
     * called <code>libname</code> is statically linked with the VM, then the
     * JNI_OnLoad_<code>libname</code> function exported by the library is invoked.
     * See the JNI Specification for more details.
     *
     * Otherwise, the libname argument is loaded from a system library
     * location and mapped to a native library image in an implementation-
     * dependent manner.
     * <p>
     * The call <code>System.loadLibrary(name)</code> is effectively
     * equivalent to the call
     * <blockquote><pre>
     * Runtime.getRuntime().loadLibrary(name)
     * </pre></blockquote>
     *
     * @param      libname   the name of the library.
     * @exception  SecurityException  if a security manager exists and its
     *             <code>checkLink</code> method doesn't allow
     *             loading of the specified dynamic library
     * @exception  UnsatisfiedLinkError if either the libname argument
     *             contains a file path, the native library is not statically
     *             linked with the VM,  or the library cannot be mapped to a
     *             native library image by the host system.
     * @exception  NullPointerException if <code>libname</code> is
     *             <code>null</code>
     * @see        java.lang.Runtime#loadLibrary(java.lang.String)
     * @see        java.lang.SecurityManager#checkLink(java.lang.String)
     */
    @CallerSensitive
    public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
    }

可以看到 方法调用中出现Runtime.getRuntime().loadLibrary0(), 从这行代码我们知道库文件是在运行时被加载起作用的。
我们继续看loadLibrary0()

  /**
     * Loads the native library specified by the <code>libname</code>
     * argument.  The <code>libname</code> argument must not contain any platform
     * specific prefix, file extension or path. If a native library
     * called <code>libname</code> is statically linked with the VM, then the
     * JNI_OnLoad_<code>libname</code> function exported by the library is invoked.
     * See the JNI Specification for more details.
     *
     * Otherwise, the libname argument is loaded from a system library
     * location and mapped to a native library image in an implementation-
     * dependent manner.
     * <p>
     * First, if there is a security manager, its <code>checkLink</code>
     * method is called with the <code>libname</code> as its argument.
     * This may result in a security exception.
     * <p>
     * The method {@link System#loadLibrary(String)} is the conventional
     * and convenient means of invoking this method. If native
     * methods are to be used in the implementation of a class, a standard
     * strategy is to put the native code in a library file (call it
     * <code>LibFile</code>) and then to put a static initializer:
     * <blockquote><pre>
     * static { System.loadLibrary("LibFile"); }
     * </pre></blockquote>
     * within the class declaration. When the class is loaded and
     * initialized, the necessary native code implementation for the native
     * methods will then be loaded as well.
     * <p>
     * If this method is called more than once with the same library
     * name, the second and subsequent calls are ignored.
     *
     * @param      libname   the name of the library.
     * @exception  SecurityException  if a security manager exists and its
     *             <code>checkLink</code> method doesn't allow
     *             loading of the specified dynamic library
     * @exception  UnsatisfiedLinkError if either the libname argument
     *             contains a file path, the native library is not statically
     *             linked with the VM,  or the library cannot be mapped to a
     *             native library image by the host system.
     * @exception  NullPointerException if <code>libname</code> is
     *             <code>null</code>
     * @see        java.lang.SecurityException
     * @see        java.lang.SecurityManager#checkLink(java.lang.String)
     */
    @CallerSensitive
    public void loadLibrary(String libname) {
        loadLibrary0(Reflection.getCallerClass(), libname);
    }

    synchronized void loadLibrary0(Class<?> fromClass, String libname) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkLink(libname);
        }
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        ClassLoader.loadLibrary(fromClass, libname, false);
    }

题外话:loadLibrary(),loadLibrary0()这两个方法的命名还是挺不符合规范的,历史遗留问题吧。
在loadLibrary中我们看到了ClassLoader.loadLibrary(fromClass, libname, false);方法
继续追溯

 // 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");
        }
        if (isAbsolute) {
            if (loadLibrary0(fromClass, new File(name))) {
                return;
            }
            throw new UnsatisfiedLinkError("Can't load library: " + name);
        }
        if (loader != null) {
            String libfilename = loader.findLibrary(name);
            if (libfilename != null) {
                File libfile = new File(libfilename);
                if (!libfile.isAbsolute()) {
                    throw new UnsatisfiedLinkError(
    "ClassLoader.findLibrary failed to return an absolute path: " + libfilename);
                }
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                throw new UnsatisfiedLinkError("Can't load " + libfilename);
            }
        }
        for (int i = 0 ; i < sys_paths.length ; i++) {
            File libfile = new File(sys_paths[i], System.mapLibraryName(name));
            if (loadLibrary0(fromClass, libfile)) {
                return;
            }
            libfile = ClassLoaderHelper.mapAlternativeName(libfile);
            if (libfile != null && loadLibrary0(fromClass, libfile)) {
                return;
            }
        }
        if (loader != null) {
            for (int i = 0 ; i < usr_paths.length ; i++) {
                File libfile = new File(usr_paths[i],
                                        System.mapLibraryName(name));
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                libfile = ClassLoaderHelper.mapAlternativeName(libfile);
                if (libfile != null && loadLibrary0(fromClass, libfile)) {
                    return;
                }
            }
        }
        // Oops, it failed
        throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
    }

这其中有段代码很重要:

   if (sys_paths == null) {
            usr_paths = initializePath("java.library.path");
            sys_paths = initializePath("sun.boot.library.path");
        }

对于上述代码的解释我们可以从这篇博客中获取到答案:

if you set sys_paths to null, the library path will be re-initialised when you try to load a library.

意思是说,如果我们通过代码将sys_paths,设置为null,那么java.library.path将被重新加载一次。
那么问题来了,通过刚才的源代码追溯,我们知道System.loadLibray()调用ClassLoader.loadLibrary()方法,
我们应该如何将sys_paths设置为空?

Java web 项目中使用JNI技术(如何在程序运行期间改变 java.library.path并生效)
屏幕快照 2016-11-21 下午2.15.02.png

通过上述情景描述,我们要更改sys_paths的值为null,只能在sys_paths初始化之前做手脚(反射在程序动态运行期间更改程序中的属性值)。
代码如下:

 /**
  * Sets the java library path to the specified path
  *
  * @param path the new library path
  * @throws Exception
  */
 public static void setLibraryPath(String path) throws Exception {
  System.setProperty("java.library.path", path);
  //set sys_paths to null
  final Field sysPathsField =   ClassLoader.class.getDeclaredField("sys_paths");
  sysPathsField.setAccessible(true);
  sysPathsField.set(null, null);
 }

追溯上述代码,debug结果如下图所示:


Java web 项目中使用JNI技术(如何在程序运行期间改变 java.library.path并生效)
屏幕快照 2016-11-21 下午2.56.22.png

Java web 项目中使用JNI技术(如何在程序运行期间改变 java.library.path并生效)
屏幕快照 2016-11-21 下午2.57.15.png

上图红色注释为java.library.path注释有误特此说明。
最终程序的正常运行。

在程序中实现了程序运行时动态更改java.library.path并生效的效果。
我在web项目中的应用是这样的:
程序封装,对JNI的使用封装成jniutil工具类:

Java web 项目中使用JNI技术(如何在程序运行期间改变 java.library.path并生效)
屏幕快照 2016-11-21 下午3.16.18.png

代码如下:
GetDownloadID.java 声明本地方法,依赖底层实现。

package com.fxmms.common.jniutil;
public class GetDownloadID{
   public native String getDownloadID(String mac);
}

GetDownloadIDUtil.java,工具类,调用上述GetDownloadID类的实例方法getDownloadID()

package com.fxmms.common.jniutil;

import org.apache.http.util.Asserts;

import java.lang.reflect.Field;

/**
 * @usage JNI调用底层c算法将mac地址转化为downloadid
 */
public class GetDownloadIDUtil {
 static{
   try{
    setLibraryPath("/Users/mark/mms/src/main/java/com/fxmms/common/jniutil");
    System.loadLibrary("GetDownloadID");
   }catch(Exception e){
    System.err.println("Native code library failed to load.\n" + e);
    System.exit(1);
   }
  }

 public static String getDownLoadId(String mac){
  GetDownloadID test = new GetDownloadID();
  String downLoadId = test.getDownloadID(mac);
  return downLoadId;
 }

 /**
  * Sets the java library path to the specified path
  * @usage 动态更改sys_paths,使得usr_paths 重新初始化
  * @param path the new library path
  * @throws Exception
  */
 public static void setLibraryPath(String path) throws Exception {
  System.setProperty("java.library.path", path);
  //set sys_paths to null
  final Field sysPathsField = ClassLoader.class.getDeclaredField("sys_paths");
  sysPathsField.setAccessible(true);
  sysPathsField.set(null, null);
 }

 public static void main(String[] args){
  //-Djava.library.path="/Users/mark/mms/src/main/java/com/fxmms/common/jniutil"
  ///Users/mark/mms/src/main/java/com/fxmms/common/jniutil
  System.out.println(System.getProperty("java.library.path"));
  String mac = "CC:81:DA:86:42:E7";
  Asserts.check(mac!=null,"mac  null");
  GetDownloadID test = new GetDownloadID();
  System.out.println(test.getDownloadID(mac));
 }
}

注意:对库文件的加载放置在静态代码块中。
记录完毕。

上一篇:如何使用Javascript格式化日期显示


下一篇:EasyCVR批量导入通道信息提示框消失问题排查