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设置为空?

屏幕快照 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结果如下图所示:


屏幕快照 2016-11-21 下午2.56.22.png

屏幕快照 2016-11-21 下午2.57.15.png

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

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

屏幕快照 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));
 }
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,516评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • 2017-8-11学经汇报: 一、学经日期:2017年8月11日 农历润六月二十 多云转雨 星期五 宝贝...
    b0a4ca4b06a4阅读 918评论 0 0
  • 文/浆桥 闺蜜群炸开了,M要结婚了,还是读研期间。一群死党在下面排队形:呀,别!没有份子钱。 小绮却冷不丁的冒出一...
    浆桥阅读 364评论 2 5