Java/JSP使用JNI技术调用本地动态链接库

来源:互联网 发布:淘宝助手上传成功错误 编辑:程序博客网 时间:2024/05/17 05:02

1.软件环境

eclipse cpp indigo

eclipse java indigo

eclipse ee indigo

ubuntu 14.04 x64

tomcat v6

jdk v1.6.0_45

2.目的

    使用Linux C实现某种硬件终端与服务器通信指令的解析,然后:

  • 提供socket服务,给PC端C/S模式软件提供协议解析服务,将解析后的结果封装成JSON格式,返回给PC端
  • 提供动态链接库,供其他PC软件调用
  • 使用JSP等B/S工具,调用动态链接库,获取到解析后的JSON结果,并展示到WEB页面上

    本文主要介绍第三种,JSP如何通过JNI技术调用本地链接库。

3.实施方案

    要实现JSP调用本地动态链接库,需要一下四步:

       <1> 获取第三方动态链接库或封装自己的动态链接库程序。

               我的程序中,该部分对应的是硬件终端协议解析程序:libppa.so。

        <2>创建JNI Java类,用来生成C头文件,并加载动态链接库。

        <3>创建C工程:

               <3.1>实现<2>中生成的头文件;

               <3.2>加载<1>中的动态链接库libppa.so,调用libppa.so中的函数;

               <3.3>将该C工程打包成动态链接库(比如叫libjniso.so),供<2>中的Java类加载和调用.

        <4>将<2>中的Java工程封装成.jar包,以便在JSP工程中调用

    实现该方案,很重要的一点在于,eclipse中各种环境变量的配置,本文中对于各种环境变量的配置也做了详细描述,希望能帮助各位读者简单、快速地搭建起来这一套开发环境。

4.开发并封装动态链接库libppa.so

    开发工具:eclipse cpp indigo

    该动态链接库的作用是,提供一个入口函数,传入指令原文,解析后,将解析结果封装成JSON格式的字符串,并通过函数返回,该入口函数如下:

       char * ppba(const char *src_cmd) ;

    如果有通过C解析JSON需求的朋友,建议使用CJSON,项目网址:http://sourceforge.net/projects/cjson/ 。

4.1创建动态链接库项目

    既然是通过eclipse开发动态链接库,首先要创建一个动态链接库项目:           

4.2配置编译环境

    开发就不讲了,主要讲,如果要成功生成动态链接库(.so)文件,需要如何配置编译环境。可能每个人的机器和系统、系统版本不同,编译环境会有差别。

4.2.1配置编译器编译选项-fPIC

    这个选项可能在你的机器上并不需要。我的PC安装的是64位Ubuntu 14.04,很不幸的是,我需要配置这个编译选项,如下:


    配置起来倒是很简单,右键你的C工程,在右键菜单中选择”Properties“,就会弹出以上页面,按照图中红色方框标注的地方设置即可。

4.2.2添加C数学函数库编译选项

    在C程序中,如果需要使用到数学函数库<math.h>,即使在程序中include了math.h头文件,也无法编译成功,需要在编译器中添加以下配置:


    此处仅记录了我遇到的两个配置问题,解决来配置问题,直接右键工程,选择”Buil Project“即可生成动态链接库文件了。

    编译成功后,在工程下的Binaries、Debug和Release下都会生成该动态链接库文件。由于是生成动态库文件,所以该程序是无法直接运行的。所以,在开发的过程中,还是建议大家先创建可以运行的普通C工程,开发调试好后,再创建一个动态链接库工程,并将代码拷贝进去,再编译成动态链接库。

5.创建JNI Java工程,生成.h头文件

    开发工具:eclipse java indigo

    这个部分比较简单,直接上代码:

<span style="font-family:Tahoma;font-size:14px;">package com.alex.ppa;public class PileProtocolAnalysizer {//将需要解析的指令原始字符串传入本地库中解析,并返回解析后的JSON结果public native String cmdParse(String cmd);static{System.loadLibrary("jniso");}/** * @param args */public static void main(String[] args) {// TODO Auto-generated method stub//String testCmd = "03411030002000B000F0103CD7E6C03000000393645373932313839363545423732433932413534394444354133333031313285841E0010270000FFFFFF7F10270000FFFFFF7FA0860100400D0300E0930400801A060020A10700C027090060AE0A0000350C00A0BB0D0030C8070018CC070000D00700E8D30700D0D70700B8DB0700A0DF070088E307006FE7070058EB07003FEF070028F307000FF70700F8FA0700DFFE0700C8020800B0060800980A0800800E08006812080050160800381A0800201E080008220800F0250800D8290800C02D0800A83108009035080078390800603D0800484108003045080018490800004D0800E850080001000002303434303430303030333030303032303030313131353038303631373132353050C30C1106080F1E00003316";//String res = new PileProtocolAnalysizer().cmdParse(testCmd);////System.out.println("PileProtocolAnalysizer.res = \n" + res);}}</span>


    创建一个Java工程,然后创建一个主类,比如上述示例中的PileProtocolAnalysizer类,这个类主要做两件事情:

    <1>定义你要在动态链接库中调用并实现的函数,如上述示例代码中的:

<span style="font-family:Tahoma;font-size:14px;">        public native String cmdParse(String cmd);</span>

   <2>加载该动态链接库:

<span style="font-family:Tahoma;font-size:14px;">static{System.loadLibrary("jniso");}</span>
   主类写好后,需要调用javah命令生成头文件,在eclipse中生成头文件的方法,请参考:http://blog.csdn.net/wangyuchun_799/article/details/46978133

   头文件生成成功后,内容如下:com_alex_ppa_PileProtocolAnalysizer.h

<span style="font-family:Tahoma;font-size:14px;">/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_alex_ppa_PileProtocolAnalysizer */#ifndef _Included_com_alex_ppa_PileProtocolAnalysizer#define _Included_com_alex_ppa_PileProtocolAnalysizer#ifdef __cplusplusextern "C" {#endif/* * Class:     com_alex_ppa_PileProtocolAnalysizer * Method:    cmdParse * Signature: (Ljava/lang/String;)Ljava/lang/String; */JNIEXPORT jstring JNICALL Java_com_alex_ppa_PileProtocolAnalysizer_cmdParse  (JNIEnv *, jobject, jstring);#ifdef __cplusplus}#endif#endif</span>
  注意,该头文件是Java自动生成的,不要做任何修改。下一步,就是创建C工程来实现这个头文件了。

6.创建并实现JNI本地库

  开发工具:eclipse cpp indigo

  注意,上一步生成头文件的Java类,加载了动态链接库,所以,我们创建的工程,依然是动态链接库工程。

6.1实现头文件

文件名称:com_alex_ppa_PileProtocolAnalysizer.c

<span style="font-family:Tahoma;font-size:14px;">/* * com_alex_ppa_PileProtocolAnalysizer.c * *  Created on: 2015-8-12 *      Author: alex */#include "com_alex_ppa_PileProtocolAnalysizer.h"#include "load_so.h"/* * Class:     com_alex_ppa_PileProtocolAnalysizer * Method:    cmdParse * Signature: (Ljava/lang/String;)Ljava/lang/String; */JNIEXPORT jstring JNICALL Java_com_alex_ppa_PileProtocolAnalysizer_cmdParse  (JNIEnv *env, jobject obj, jstring cmd){//将Java中传入的String类型指令字符串,转成C的char*类型const char *cmd_str = (*env)->GetStringUTFChars(env, cmd, 0);        //调用终端协议解析动态链接库中的ppba函数解析指令,并将解析后的char*结果转换成jstring类型,返回给Javajstring res = (*env)->NewStringUTF(env, ppa(cmd_str));return res;}</span>
   实现该头文件,只需创建一个.c文件,并将头文件中Java自动生成的方法拷进来,实现函数体即可。

   有一个细节需要注意,在Java生成的头文件中,函数的参数列表只有参数类型,没有参数名:

<span style="font-family:Tahoma;font-size:14px;">JNIEXPORT jstring JNICALL Java_com_alex_ppa_PileProtocolAnalysizer_cmdParse  (<span style="color:#990000;">JNIEnv *, jobject, jstring</span>);</span>
   但是,在.c文件中,将头文件中这些自动生成的函数拷贝进来实现时,需要给这些函数的参数列表加上参数名,不然编译时会报错。想想也是,如果没有参数名,我们该怎么使用外部传入进来的参数呢?

<span style="font-family:Tahoma;font-size:14px;">JNIEXPORT jstring JNICALL Java_com_alex_ppa_PileProtocolAnalysizer_cmdParse  (<span style="color:#CC0000;">JNIEnv *env, jobject obj, jstring cmd</span>)</span>

6.2引入JNI头文件库

    注意到没?在Java自动生成的头文件的开头,引入了如下库:

<span style="font-family:Tahoma;font-size:14px;">/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h></span>
    <jni.h>文件以及JNI其他库文件都在JDK安装目录下,所以,我们要在eclipse中配置、添加JNI本地库文件的目录。

   右键C工程,选择”Properties“,然后参考上图进行配置。说白了,就是把JDK安装目录下的include目录和include/linux目录添加进来。我本机上的jdk安装目录是:/usr/lib/jvm/jdk1.6.0_45。

6.3调用第三方动态链接库文件

    使用JNI技术的目的,就是为了能让Java调用C代码。上一步中,已经讲了如何在C工程中实现Java生成的头文件,如果所有的C代码都在该工程下编写,倒不需要这一步了。不过,在实际项目中,大家分工都很明确,C部分的代码要么由团队中其他人编写,并编译成动态链接库给你,要么就是使用第三方提供的动态链接库。所以,这一步,才是JNI技术的核心精髓作用所在。

   下面继续讲解,如何将第4节中生成的动态链接库文件libppa.so引入该C工程,并加载、调用该动态库中的函数。

6.3.1引入第三方动态链接库

    引入第三方动态链接库,有两个办法:

     <1>使用绝对路径。

            这种方法比较简单,也不需要将动态链接库放到项目工程中,直接使用绝对路径就可以了。

     <2>使用项目工程中的相对路径.

            这种方法麻烦点,首先将动态库文件拷贝到项目工程中,然后配置eclipse环境和系统环境,可以参考如下这篇文章:

            http://www.cnblogs.com/smartvessel/archive/2011/01/21/1940868.html

6.3.2加载并调用第三方动态库

<span style="font-family:Tahoma;font-size:14px;">char * ppa(const char * cmd){void *so_handler;char * (*ppba)(const char *src_cmd) ;so_handler = dlopen("/lib/ppa/libppa.so", RTLD_NOW);if(so_handler){printf("\nso_handler : 创建成功");}else{printf("\nso_handler创建失败 : %s", dlerror());}ppba = dlsym(so_handler, "ppba");char *res = ppba(cmd);//printf("load_so.c > main > ppa = \n%s", res);dlclose(so_handler);return res;}</span>
    <1>dlopen:打开动态库文件,并创建句柄

<span style="font-family:Tahoma;font-size:14px;">        void *so_handler;so_handler = dlopen("/lib/ppa/libppa.so", RTLD_NOW);</span>
        动态库打开的模式有两种,一种是RTLD_NOW,一种是RTLD_NOW。关于二者的差别,网上资料很多,不再赘述。可以参考:

        http://www.linuxidc.com/Linux/2015-03/115152.htm

    <2>dlerror:查看动态库使用过程中的错误

<span style="font-family:Tahoma;font-size:14px;">if(so_handler){printf("\nso_handler : 创建成功");}else{printf("\nso_handler创建失败 : %s", dlerror());}</span>
       动态库操作函数是不会返回错误的,需要判断或者查看错误,需要使用dlerror();

    <3>dlsym:获取动态库中的函数指针

   比如,需要调用动态库中的一个函数:

<span style="font-family:Tahoma;font-size:14px;">char * ppba(const char *src_cmd)</span>
   那么,首先需要声明指向该函数的指针变量:

<span style="font-family:Tahoma;font-size:14px;">char * (*ppba)(const char *src_cmd) ;</span>
   函数指针变量和函数本身的差别只是在函数名的名称前加了一个星号(*)。

   接着,通过dlopen获取到的动态库句柄,获取需要调用的函数,并赋给我们创建的指向该函数的指针变量,这样,在我们的代码中,该函数变量就和它指向的动态库中的函数一样了。

<span style="font-family:Tahoma;font-size:14px;">ppba = dlsym(so_handler, "ppba");</span>
    <4>调用动态库函数

<span style="font-family:Tahoma;font-size:14px;">char *res = ppba(cmd);</span>
   在上一步获取动态库函数成功后,我们就可以直接操作该函数指针了,就相当于在操作动态库中它指向的函数。

    <5>dlclose:关闭动态库句柄资源

<span style="font-family:Tahoma;font-size:14px;">dlclose(so_handler);</span>
   既然已经使用完了动态库,就需要调用dlclose函数将句柄资源释放。

6.4编译动态库

   事实上,编译动态库的方法第4节已经讲过了,可以参考第4节。

   为了方便后文讲解,我们将此处实现JNI生成的头文件、加载并调用第三方动态库的工程编译后的动态库文件叫做:libjniso.so。

7.将动态库导入Java工程,并打包

   回到第5节的Java工程。在第5节中,我们只是创建了需要生成头文件的java主类,还没有动态库,在第6节,我们根据头文件开发并编译了动态库。此时,我们需要将该动态库”libjniso.so“导入该Java项目。

<span style="font-family:Tahoma;font-size:14px;">package com.alex.ppa;public class PileProtocolAnalysizer {//将需要解析的指令原始字符串传入本地库中解析,并返回解析后的JSON结果public native String cmdParse(String cmd);static{System.loadLibrary("jniso");}/** * @param args */public static void main(String[] args) {// TODO Auto-generated method stub//String testCmd = "03411030002000B000F0103CD7E6C03000000393645373932313839363545423732433932413534394444354133333031313285841E0010270000FFFFFF7F10270000FFFFFF7FA0860100400D0300E0930400801A060020A10700C027090060AE0A0000350C00A0BB0D0030C8070018CC070000D00700E8D30700D0D70700B8DB0700A0DF070088E307006FE7070058EB07003FEF070028F307000FF70700F8FA0700DFFE0700C8020800B0060800980A0800800E08006812080050160800381A0800201E080008220800F0250800D8290800C02D0800A83108009035080078390800603D0800484108003045080018490800004D0800E850080001000002303434303430303030333030303032303030313131353038303631373132353050C30C1106080F1E00003316";//String res = new PileProtocolAnalysizer().cmdParse(testCmd);////System.out.println("PileProtocolAnalysizer.res = \n" + res);}}</span>
    在生成头文件的Java主类中,我们声明了动态库中要实现的方法,以及加载了该动态库:

<span style="font-family:Tahoma;font-size:14px;">        public native String cmdParse(String cmd);static{System.loadLibrary("jniso");}</span>
    此处注意,生成的动态库文件必须以“lib”开头,比如“libxxx.so”。而上面的System.loadLibrary("动态库名称");中,动态库名称即不能以lib开头,也不要写后缀名,因为lib前缀是Java自己加上去的,后缀名是Java判断系统是WIndows还是Linux然后自己加上去的,如果时Linux,后缀名就是so,如果是Windows,后缀名就是dll。

    所以,假如你的动态库名称为:libtestjni.so,那么System.loadLibrary("动态库名称")函数你要写成:System.loadLibrary("testjni");

    在第6节,我们已经实现了该动态库,并打包成动态库文件libjniso.so。

7.1导入动态库文件

     将动态库文件libjniso.so,直接拷贝到Java工程中任意目录。放在哪个目录不重要,因为,后面需要配置eclipse环境,告诉eclipse动态库在哪。


    比如,我在Java工程中,创建了一个lib目录,并将动态库文件libjniso.so放在该目录下。

    接下来,我们要配置动态库路径,告诉eclipse我们的动态库在哪。右键该Java工程,选择"Build Path“——”Configure Build Path...“,然后如下图操作:

    在”Libraries“Tab页中,点击”JRE System Library[java版本]"前的三角箭头,展开JRE内容,双击“Native library location”,在弹出的“Native Library Folder Configuration”对话框中,选择动态库文件所在的路径。

7.2将工程打成Jar包

    配置完动态库路径后,运行该Java工程。如果运行成功,即可打包成jar文件。右键Java工程,选择“Export...”:

    为了后文好讲述,我们将此处打包的jar文件命名为“ppa.jar”.

8.实现JSP调用动态库文件

    首先,创建一个JSP工程,然后导入jar文件,导入实现头文件的动态链接库文件libjniso.so。

8.1创建JSP工程

   下图是同通过eclipse ee indigo(3.7)版本创建的JSP工程,生成的目录文件和最新版的Mars有一些区别,但是总体上还是差不多的。此处要特别强调一下,运行eclipse的jdk版本、编译java代码的jdk版本、tomcat依赖的java版本最好要一致,最好就是你的系统中只安装一个JDK,所有的软件都下载与该JDK版本相符的版本。不然,极有可能会造成一个问题,比如你的java代码是使用JDK8编译的,但是JSP却是使用JDK6的版本,这样就会造成,你通过JDK8编写编译的java代码,在JSP工程调用的时候会报错:找不到你自己写的java类  ,无法初始化。

8.2导入jar包

右键该JSP工程,选择"Build Path“——”Configure Build Path...“,然后如下图操作:


     通过“Add JARs...”添加工程内部已经有的jar包,通过“Add External JARs...”添加工程外部的jar包。然后将我们在第7节中打包的ppa.jar文件导入到工程中。

8.3导入动态链接库文件

    首先,将实现Java头文件的动态库文件libjniso.so复制到工程目录中的“WebContent/WEB-INF/lib”下。右键该JSP工程,选择"Build Path“——”Configure Build Path...“,然后如下图操作:


     在”Libraries“Tab页中,点击”JRE System Library[java版本]"前的三角箭头,展开JRE内容,双击“Native library location”,在弹出的“Native Library Folder Configuration”对话框中,选择动态库文件所在的路径。

8.4配置JSP工程运行选项

   在编译运行JSP工程时,需要配置运行环境,告诉java我们的动态链接库libjniso.so在哪里。

   右键JSP工程,选择“Run As”——“Run Configurations...”,然后参照下图配置:

    我本机使用的时tomcat 6,服务器程序按照你本机上实际情况选择。该配置主要目的就是在服务器运行参数“Arguments”中的“VM arguments”参数中,再添加一个“-Djava.library.path”参数,告诉虚拟机,我们的动态库在哪里。上文已经讲过,动态库文件libjniso.so拷贝到工程目录中“WebContent/WEB-INF/lib”下,所以,我本机上该参数是:

Djava.library.path="/home/alex/workspace/eclipse-ee-indigo-workspace/PPA/WebContent/WEB-INF/lib"

    动态库地址,根据你本机上存放的地址填写。




        
0 0
原创粉丝点击