使用JNI进行C调用java jni c++调用java

admin2024-06-04  22


文章目录

  • 前言
  • 一、说明
  • 二、使用步骤
  • 1.加载jvm动态库
  • 2.创建虚拟机
  • 3.加载java类
  • 4.加载java类方法或静态成员
  • 5.创建对象和方法调用
  • 6.辅助通用函数
  • 6.1.C++ List -> java List
  • 6.2. java List -> C++ List
  • 6.3. Java jstring -> C++ string
  • 6.3. C++ utf-8转gbk函数
  • 7.多线程开发注意事项
  • 7.1. Jvm共享
  • 7.2. 线程获取env
  • 8. Jvm destroy问题



前言

前段时间公司因项目需要,需要使用C++调用jar包,但是网上的资料零零散散,完整的可借鉴的开发资料少之又少,于是就有了这篇文章, 把这段时间的开发的经验总结一下,以后,自己也可以回过头来温习一下,各位读者如果对调用流程不是很熟悉,只要细心的阅读,相信一定会有收获的。


一、说明

C++调用class="superseo">java的是通过加载java虚拟机来解释java字节码,然后再通过虚拟机来调用对应的java代码,
首先加载jvm库,只要安装了JDK都会有jvm库(动态库和静态库都有),加载流程如下
本文以动态库为实例

二、使用步骤

1.加载jvm动态库

代码如下(示例):

char szJVMpath[MAX_PATH] = {0};

//通过环境变量javahome直接获取jvm.dll的路径,这里说明一下这个路径可以自己设置,不一定非要使用已经安装的jdk。
sprintf(szJVMpath, "%s\jre\bin\server\jvm.dll", getenv("JAVA_HOME"));

HINSTANCE hInstance = LoadLibrary(szJVMpath);
	if (hInstance == NULL)
	{
		return false;
	}

	pFnCreateJavaVM m_CreateJavaVM = (pFnCreateJavaVM)GetProcAddress(hInstance, "JNI_CreateJavaVM");

	pFnGetCreatedJavaVMs m_GetCreatedJavaVMs = (pFnGetCreatedJavaVMs)GetProcAddress(hInstance, "JNI_GetCreatedJavaVMs");

	if (NULL == m_CreateJavaVM || NULL == m_GetCreatedJavaVMs)
	{
		return false;
	}

2.创建虚拟机

代码如下(示例):

int ret = 0;

	JavaVM* Tempjvm = NULL;
	jsize count;
	
/*先获取当前进程中是否有已存在的jvm虚拟机, 如果存在则直接获取该虚拟机句柄, 如果不存在则创建新的虚拟机,

 因为较新的jdk中一个进程中只允许存在创建一个虚拟机,如果当前进程中已存在虚拟机,重新调用m_CreateJavaVM接口创建虚拟机会失败。

如果你需要多线程使用虚拟机,则需要考虑这种情况, 因为虚拟机中  JavaVM虚拟机句柄是线程共享的    环境上下文env是非线程共享的
*/
	ret = m_GetCreatedJavaVMs(&Tempjvm, sizeof(JavaVM) * 10, &count);
	
	JNIEnv* env;
	JavaVM* jvm;

		
	if (0 == ret  &&  NULL != Tempjvm && count > 0)
	{
		jvm = Tempjvm;
	}
	else
	{
		JavaVMInitArgs vm_args;
		const int OPTION_COUNT = 5;
		vm_args.nOptions = OPTION_COUNT;
		JavaVMOption options[OPTION_COUNT] = { 0 };
	
		char Currpath[256] = {0};
		int nCurrpathlen = 255;
		getcwd(Currpath, nCurrpathlen);
	
		char JarPath[256] = {0};
		char optionStringBuffer[1024] = {0};
	
		strcpy(JarPath, Currpath);
		strcat(JarPath, "\icbc-api-sdk-cop.zip");
	
		char JarPath_io[256] = {0};
		strcpy(JarPath_io, Currpath);
		strcat(JarPath_io, "\icbc-api-sdk-cop-io.zip");
	
		char JarPath_hsm[256] = {0};
		strcpy(JarPath_hsm, Currpath);
		strcat(JarPath_hsm, "\hsm-software-share-1.0.3.jar");

		sprintf(optionStringBuffer, "-Djava.class.path=%s;%s;%s", JarPath, JarPath_io, JarPath_hsm);
	
		//这里是设置需要加载的java文件,或者jar包,不同的源文件用;隔开即可。
		options[0].optionString = optionStringBuffer;
	
		/*这里是设置虚拟机的内存空间大小,vm虚拟机的内存 主要由三块构成(年轻代+老年代+持久代)
		年轻代:所有新生成的对象都是放在年轻代中,年轻代的目的就是快速回收那些短生命周期的对象,年轻代分三个区,一个Edge区, 两个Survivor区域,大部分对象在Edge区生成,
	当Edge区满了之后,虚拟机会将触发Scavenge GC,将edge还存活的对象移到Survivor区(两个其中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,
	当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,
	没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor复制过来的对象。
	而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
	
	年老代:
	
	在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
	
	持久代:
	用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,
	例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。
	*/
	
		//设置虚拟机最大可使用的内存大小 一般根据实际内存情况来设置
		options[1].optionString = "-Xmx1024m";  
		//设置新生代区域大小,看情况具体而定,如果在执行过程如果出现虚拟机崩溃,查看hs_err_pid***.log 每次崩溃的最后调用堆栈是申请内存,初始化对象的时候 那就要增加新生代区域大小。
		options[2].optionString = "-Xmn256m";  // 新生代堆大小256M
		options[3].optionString = "-XX:PrintHeapAtGC"; //打印GC前后的详细堆栈信息
		options[4].optionString = "-Xloggc:gccdebug.txt"; //打印日志到该文件
	
	

	//设置版本号,版本号有JNI_VERSION_1_1,JNI_VERSION_1_2和JNI_VERSION_1_4  
	  //选择一个跟你安装的JRE版本最近的版本号即可,不过你的JRE版本一定要等于或者高于指定的版本号  
		vm_args.version = JNI_VERSION_1_8;
		//vm_args.nOptions = 1;
		vm_args.options = options;
		//该参数指定是否忽略非标准的参数,如果填JNI_FLASE,当遇到非标准参数时,JNI_CreateJavaVM会返回JNI_ERR  
		vm_args.ignoreUnrecognized = JNI_TRUE;
	
	
			ret = m_CreateJavaVM(&jvm, (void**)&env, &vm_args);
	
		}
	
		if (ret < 0)
		{
	
			return false;
		}
	
	
		FreeLibrary(hInstance);
		/*虚拟机是依赖该动态库运行的,所以当你确定你已经不需要在使用java虚拟机的时候,再释放该动态库(虚拟机未释放即时你主动释放该动态库句柄,也是没有作用的,最后章节会具体解释虚拟机destroy问题。)*/
	}
		//到这里虚拟机算是创建成功了 创建虚拟机的时候会返回一个虚拟机上下文env,获取对应的java类和对象,

注:虚拟机崩溃一定会产生hs_err_pid***.log日志,如果没有未则说明不是虚拟机本身引起的崩溃,

3.加载java类

jclass ClsArrayList;
		jclass ClsJsonObject;
		jclass ClsMybankPayDigitalwalletHdsubwalletinitRequestV1;
		jclass ClsMybankPayDigitalwalletHdsubwalletinitRequestV1Biz;
		
		
		//这是java系统类list
		ClsArrayList = env->FindClass("java/util/ArrayList");

//这是java打印类,按照json格式打印当前类的所有数据 调试,C++打印日志超级方便
			ClsJsonObject = env->FindClass("com/icbc/api/internal/util/internal/util/fastjson/JSONObject");

//这是java自定义类,在java中每一个每一个'/'代表当前是一个文件夹
			ClsMybankPayDigitalwalletHdsubwalletinitRequestV1 = env->FindClass("com/icbc/api/request/MybankPayDigitalwalletHdsubwalletinitRequestV1");
			
			
			//这是java自定义类 这是子类 以'$'分隔
			ClsMybankPayDigitalwalletHdsubwalletinitRequestV1Biz = env->FindClass("com/icbc/api/request/MybankPayDigitalwalletHdsubwalletinitRequestV1$MybankPayDigitalwalletHdsubwalletinitRequestV1Biz");

4.加载java类方法或静态成员

java方法从加载方式来说分两种
第一种格式static方法需要调用GetStaticMethodID 来获取jmethodID
第二种普通的方法通过getGetMethodID

jmethodID GetMethodID(jclass clazz, const char *name,const char *sig) 
参数说明 clazz:加载的类方法所属作用域的类名,
name: 方法名称,区分大小写
sig: java函数对应的唯一标识符 ,可以通过 javap -verbose  -***.class > 123.txt  命令 查看对应类,静态	变量,方法的 标识符。

注意:只能加载自己的类成员或者方法,第一个参数为派生类对象,不能加载基类的成员方法。

//这是javalist操作函数
	if (NULL != ClsArrayList)
			{
				//list 初始化方法
				Jmdlist_init = env->GetMethodID(ClsArrayList, "<init>", "()V");
				
				//list  get 方法
				Jmdalist_get = env->GetMethodID(ClsArrayList, "get", "(I)Ljava/lang/Object;");

				// size()
				Jmdalist_size = env->GetMethodID(ClsArrayList, "size", "()I");


				// add
				Jmdlist_add = env->GetMethodID(ClsArrayList, "add", "(Ljava/lang/Object;)Z");

			}
			
			//这是自定义函数
			if (NULL != ClsAbstractIcbcRequest)
			{
				JmdsetServiceUrl = env->GetMethodID(ClsAbstractIcbcRequest, "setServiceUrl", "(Ljava/lang/String;)V");
				JmdsetBizContent = env->GetMethodID(ClsAbstractIcbcRequest, "setBizContent", "(Lcom/icbc/api/BizContent;)V");
			}

//加载静态成员变量, 静态成员变量的标识符获取方式和java函数相同。
			
			jfieldID fileID = env->GetStaticFieldID(Cls_IcbcConstants, "CHARSET_UTF8", "Ljava/lang/String;");
			CHARSET_UTF8 = (jstring) env->GetStaticObjectField(Cls_IcbcConstants, fileID);

			fileID = env->GetStaticFieldID(Cls_IcbcConstants, "FORMAT_JSON", "Ljava/lang/String;");
			FORMAT_JSON = (jstring) env->GetStaticObjectField(Cls_IcbcConstants, fileID);

			fileID = env->GetStaticFieldID(Cls_IcbcConstants, "ENCRYPT_TYPE_AES", "Ljava/lang/String;");
			ENCRYPT_TYPE_AES = (jstring) env->GetStaticObjectField(Cls_IcbcConstants, fileID);

5.创建对象和方法调用

jobject NewObject(jclass clazz, jmethodID methodID, …);
void CallVoidMethod(jobject obj, jmethodID methodID, …) ;

1.NewObject 和 CallVoidMethod 参数形参说明表一模一样,  因为初始化实际上也是调用构造函数,只不过初始化和static方法一样不需要this指针,所以第一个参数是class, CallVoidMethod第一个参数是实例化对象。
2. 后面是一个初始化参数列表(可变长参数),这个参数列表时根据GetMethodID后面的函数标识符来规定的,假如你的标识符说明有三个行参(string,string,string), 但是实际传入的时候你参数类型对不上或者参数个数对不上,编译器不会做类型检查,他会把你传过去的C++对象强转为java对象,jvm虚拟机会在运行的时候出现未知行为(很有可能会崩溃)。
3.第一个参数(一般是类或者类对象)不能为NULL, 调用之前必须做异常判断, 如果为NULL,jvm虚拟机会崩溃,因为 jvm虚拟机对C++传过去的值做校验。
4.调用java方法 根据其返回值采用不同的调用方式, 没有返回值env->CallvoidMethod , 返回值为对象 env->CallobjectMethod, 其他的int, char, bool 参照jin.h头文件,
ClsDefaultIcbcClient = env->FindClass("com/icbc/api/DefaultIcbcClient");
	
	jmethodID Jmd_DefaultIcbcClient_Init = env->GetMethodID(Cls_DefaultIcbcClient, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
	
			jstring appId =  env->NewStringUTF("");
			jstring signType =  env->NewStringUTF("");
			jstring privateKey =  env->NewStringUTF(NULL);
			jstring charset = CHARSET_UTF8;
			jstring format = FORMAT_JSON;
			jstring icbcPulicKey =  env->NewStringUTF(NULL);
			jstring encryptType = ENCRYPT_TYPE_AES;
			jstring encryptKey =  env->NewStringUTF(NULL);
			jstring ca =  env->NewStringUTF("");
			jstring password =  env->NewStringUTF(NULL);
			jstring emSignIp =  env->NewStringUTF(NULL);
			jstring emSignPort =  env->NewStringUTF(NULL);
			jstring emProduct =  env->NewStringUTF(NULL);

			jobject DefaultIcbcClient =  env->NewObject(Cls_DefaultIcbcClient, Jmd_DefaultIcbcClient_Init, appId,  signType,  privateKey,  charset,  format,  icbcPulicKey,  encryptType,  encryptKey,  ca,  password,  emSignIp,  emSignPort,  emProduct);


			jobject request = env->NewObject(ClsMybankPayDigitalwalletBatchhdsubwalinitRequestV1, JmdMybankPayDigitalwalletBatchhdsubwalinitRequestV1_Init);
			//request赋值很复杂这里我不贴代码。


			Jmdexecute = env->GetMethodID(ClsDefaultIcbcClient, "execute", "(Lcom/icbc/api/IcbcRequest;)Lcom/icbc/api/IcbcResponse;");
			
			jobject JObjRespone  = env->CallObjectMethod(DefaultIcbcClient, Jmdexecute, request);

6.辅助通用函数

那些简单的helloworld的测试代码,网上一搜一大堆,我这里再提供一些很常用的基础功能函数

6.1.C++ List -> java List

//因为JniEnv 和其加载的方法不能线程共享 所以需要当做参数传入
jobject ListCToJavaObject(list<string> & CList, JNIEnv* env, jclass ClsArrayList, jmethodID Jmdlist_init, jmethodID Jmdlist_add)
{

	jobject list_obj = env->NewObject(ClsArrayList, Jmdlist_init);

	list<string>::const_iterator ListIter = CList.cbegin();
	while (ListIter != CList.cend())
	{
		env->CallVoidMethod(list_obj, Jmdlist_add, env->NewStringUTF(ListIter->c_str()));
		++ListIter;
	}

	return list_obj;
}

6.2. java List -> C++ List

list<string> ListJavaObjectToC(jobject & JList, JNIEnv* env, jclass ClsArrayList, jmethodID Jmdalist_size, jmethodID Jmdalist_get)
{

	list<string> StrList;

	int list_obj_size = env->CallIntMethod(JList, Jmdalist_size);

	int i = 0;
	while (i < list_obj_size)
	{
		jstring  jStr = (jstring)env->CallObjectMethod(JList, Jmdalist_get, i);

		StrList.push_back(jstring2string(env, jStr));

		env->DeleteLocalRef(jStr);

		++i;
	}

	return StrList;

}

6.3. Java jstring -> C++ string

string jstring2string(JNIEnv* env, jstring jStr)
{
	if (!jStr)
	{
		return "";
	}
	const jclass stringClass = env->GetObjectClass(jStr);
	const jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B");
	const jbyteArray stringJbytes = (jbyteArray) env->CallObjectMethod(jStr, getBytes, env->NewStringUTF("UTF-8"));

	size_t length = (size_t) env->GetArrayLength(stringJbytes);
	jbyte* pBytes = env->GetByteArrayElements(stringJbytes, NULL);

	std::string ret = std::string((char*)pBytes, length);
	env->ReleaseByteArrayElements(stringJbytes, pBytes, JNI_ABORT);

	env->DeleteLocalRef(stringJbytes);
	env->DeleteLocalRef(stringClass);
	return ret;
}

6.3. C++ utf-8转gbk函数

因为java虚拟机默认是utf-8编码,如果有中文的话当前程序不能正常显示,可以转为gbk

int UTF82GBK(const char* szUtf8, char* szGbk, int Len)
{
	int n = MultiByteToWideChar(CP_UTF8, 0, szUtf8, -1, NULL, 0);
	WCHAR* wszGBK = new WCHAR[sizeof(WCHAR) * n];
	memset(wszGBK, 0, sizeof(WCHAR) * n);
	MultiByteToWideChar(CP_UTF8, 0, szUtf8, -1, wszGBK, n);
	n = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL);
	if (n > Len)
	{
		delete[]wszGBK;
		return -1;
	}
	WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, szGbk, n, NULL, NULL);
	delete[]wszGBK;
	wszGBK = NULL;
	return 0;
}

7.多线程开发注意事项

7.1. Jvm共享

jvm是线程共享的,进程唯一的,你可以把他作为一个线程共享变量存储起来,但是当你无法获取该变量是,你可以直接查询当前进程是否存在jvm虚拟机,调用方法为JNI_GetCreatedJavaVMs

JNIEnv* env;
	JavaVM* jvm;


	JavaVM* Tempjvm = NULL;
	jsize count;
	ret = m_GetCreatedJavaVMs(&Tempjvm, sizeof(JavaVM) * 10, &count);

	EnterCriticalSection(&m_JvmMutex);

	if (0 == ret  &&  NULL != Tempjvm && count > 0)
	{
		jvm = Tempjvm;
	}
	else
	{
	
		ret = m_CreateJavaVM(&jvm, (void**)&env, &vm_args);
	}

7.2. 线程获取env

上面是单线程的测试流程,但是众所周知 其他的线程我们无法引用当前线程env,就因为env非线程共享,在其他的线程我们无法使用该线程env以及其获取的class。
首次使用JNI_GetCreatedJavaVMs接口 创建虚拟机的时候会返回以恶搞env,但是这只能再当前线程使用,
在多线程的情况下,调用首次使用JNI_GetCreatedJavaVMs接口成功创建虚拟机之后,再调用该接口会失败,因为一个进程只能存在一个虚拟机。
这个时候我们需要使用AttachCurrentThread()函数, AttachCurrentThread是将当前线程附加到虚拟机上,并获取属于当前线程的env,

JNIEnv* env = NULL;
			jint nRet = jvm->AttachCurrentThread((void**)&env, NULL);
			
			//如果env不为空,则说明附加成功, 在程序最后需要DetachCurrentThread()来释放当前线程的env
			
			if (NULL == env)
			{
				IfGetEnvCorrentsuccess = false;
				break;
			}
			else
			{
				IfGetEnvCorrentsuccess = true;
			}
			

//再当前线程退出时 再调用DetachCurrentThread释放该env	
		if(true == IfGetEnvCorrentsuccess)
		{
			jvm->DetachCurrentThread()
		}

8. Jvm destroy问题

问题引入:当我们首次使用JNI_GetCreatedJavaVMs接口成功创建虚拟机之后
正常流程 我们应当使用jvm->destrory来释放虚拟机, 但是当我们destory(destroy函数是非阻塞的)之后 调用JNI_GetCreatedJavaVMs返回-1,会创建失败,并且JNI_GetCreatedJavaVMs返回NULL,
看这段官方关于destrory函数的说明

”The VM waits until the current thread is the only non-daemon user
thread before it actually unloads. User threads include both Java
threads and attached native threads. This restriction exists because
a Java thread or attached native thread may be holding system
resources, such as locks, windows, and so on. The VM cannot
automatically free these resources. By restricting the current
thread to be the only running thread when the VM is unloaded, the
burden of releasing system resources held by arbitrary threads is on
the programmer.“

中文翻译为

虚拟机等待当前线程成为唯一的非守护进程用户线程,然后才实际卸载。用户线程包括Java线程和附加的本机线程。存在这种限制是因为Java线程或附加的本机线程可能持有系统资源,如锁、窗口等。
虚拟机无法自动释放这些资源。通过在卸载VM时将当前线程限制为唯一正在运行的线程,释放由任意线程持有的系统资源的负担就落在了程序员身上。

JDK / JRE 1.1不完全支持DestroyJavaVM. 在JDK/JRE 1.1 只有主线程可以叫DestroyJavaVM。 从JDK/JRE 1.2, 任何线程都可以调用这个函数, 不管是否从依附的线程中. 如果与当前线程连接,
VM等到当前线程是唯一非守护进程。如果没有与当前线程相连接,那么VM会与当前线程相连,并且等待当前线程是非守护线程. The JDK/JRE still does not support VM unloading, however.

DestroyJavaVM之后 会启动会一个Destroy线程监听进程, 将当前虚拟机状态置为不可用,但是虚拟机还是真实存在的, 当满足这个条件 “当前进程中只存在唯一的非守护线程” ,才会真正的释放jvm虚拟机, 所以才会出现问题引入的那种现象

显然 只有程序退出的时候 才会满足这个条件 “当前进程中只存在唯一的非守护线程”

解决方案:
大家看这样一段话:
Last time I looked at JNI_DestroyJVM(), it said “The JDK/JRE still does not support VM unloading, however.” Just don’t call it, and don’t re-initialize it either.、

所以 jni官方文档这样说 “As of JDK/JRE 1.1.2 unloading of the VM is not supported.”

文档资料出处

意思是我们不需要调用主动destrory来释放虚拟机,当前进程结束时会自动释放虚拟机。



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!