最近写了一个网站模拟点击的项目,究其原因是合作方提供不了接口。利用httpclient模拟浏览器访问,深深的感到这事的蛋疼性。然后蛋疼的事还不止这个,做的是一个远在千里之外内网的一个环境,远程调试又卡的要命。经过漫长的搜集数据和尝试,这个接口项目总算是结束了。 公司考虑到提供出去的接口一反编译就暴露在阳光下了,所有才有了JNI。
废话不说了,进入正题。
Jni程序开发的一般操作步骤如下:
l 编写java中的调用类
l 用javah生成c/c++原生函数的头文件
l c/c++中调用需要的其他函数功能,实现原生函数(原则上可以调用任何资源)
l 将项目依赖的所有原生库和资源加入到java项目的java.library.path
l 生成java程序
l 发布java应用和dll或者so
首先,建立一个测试的类
package com.ist.manage.ess;
public class BssSupport {
static {
System.loadLibrary("BssSupport");
}
public static void main(String[] args) {
BssSupport bs = new BssSupport();
bs.test("hello,java");
}
public void imi(String s)
{
System.out.println(s);
}
public native void test(String instring); }
打开cmd进入到工程的目录下
javah -encoding UTF-8 -jni com.ist.manage.ess.TestHello //因为项目本来是utf8编码的缘故所有加上utf8,产生一个.h文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#ifndef _Included_com_ist_manage_ess_TestHello
#define _Included_com_ist_manage_ess_TestHello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_ist_manage_ess_TestHello
* Method: test
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_ist_manage_ess_BssSupport_test(JNIEnv *, jobject,jstring );
#ifdef __cplusplus
}
#endif
#endif
观察一下就能得出函数是Java +报名 +函数名组成,中间中下划线分割,如果函数名中包含下划线就会被特殊处理,所以还是不要用那么蛋疼的命名规则,其实也很简单,再说java的函数名,一般不加下划线的,注意一下。
还看到了生僻的类型,这些类型是jni特有的和C++有一一对应的关系。
Java类型 | 本地类型 | JNI中定义的别名 |
int | long | jint |
long | _int64 | jlong |
byte | signed char | jbyte |
boolean | unsigned char | jboolean |
char | unsigned short | jchar |
short | short | jshort |
float | float | jfloat |
double | double | jdouble |
Object | _jobject* | jobject |
需要在jdk目录下
在%JAVA_HOME%/include/下找到 jni.h,
在%JAVA_HOME%/include/win32/下找到jni_md.h,
可以复制到vs/vc的include/”下,这样就不用费力的在工程中导入这两文件了。
vc不能编译出64位的dll,果断用了vs2010,发现不支持C99这也是一件坑爹的地方。
vs建工程很简单,新建一个空的dll工程即可,省的输入一大堆命令编译。
建一个.cpp文件,这里代码很简单,先包含头文件,然后通过jni提供的函数调用java函数imi()
#include "com_ist_manage_ess_TestHello.h"
JNIEXPORT void JNICALL Java_com_ist_manage_ess_BssSupport_test(JNIEnv *env, jobject obj,jstring in_url)
{
jclass class_Test =env->GetObjectClass(obj);
jmethodID mid = env->GetMethodID(class_Test, "imi","(Ljava/lang/String;)V" );
env->CallObjectMethod(obj, mid,in_url);
}
这里比较蛋疼的问题是jni签名,为什么会有这个东西,以为java支持函数重载,编译器必须知道函数的形参类型才能到找到具体的函数。
然后,得说明一下这些函数参数和签名的对应关系才好下手。
类型标示 | Java类型 | 类型标示 | Java类型 |
Z | boolean | F | float |
B | byte | D | double |
C | char | Ljava/lang/String | String |
S | short | [I | Int[] |
I | int | [Ljava/lang/object | Object[] |
J | long |
上面表格中列出了常用的类型标示,如果java类型是数组,则标示中会出现一个“[”,另外引用类型(除基本类型的数组外)的标识最后都有一个”;”分号
函数签名一开始最好自己手动输入理解一下熟练后可以利用java自带的工具自动生成
javap -s -p xxx
Xxx为编译后的class文件,s标识输出内部数据类型的签名信息,p表示打印所有的函数和成员的签名信息,默认只会打印public成员和函数的签名信息
类的签名规则是:“L+全限定类名+;” 三部分组成,例如:
- long fun (int n, String str, int[] arr);
- (ILjava/lang/String;[I)J
括号里面的内容分成三部分,之间没有空格,
即“I”、“Ljava/lang/String;”和“[I”,
分别代表 int、String和int[]。括号外面是返回值类型签名,J代表long型。
这里还有必要解释一下参数1和参数2 ,参数1就是jni环境了,可以用一张图表示。参数2就是调用JNI的那个对象了,如果是静态方法变对应的类。写一个静态的函数,javah一下就看到了参数名就不叫object了,但是类型都是一样的。
相信代码一看就懂,编译一下把生产的.dll扔到 c:/window/systm32目录下(path自己加也太麻烦了,就干脆用这个目录了)
然后运行java程序就看到了效果。