🎯导读:本文介绍了使用Java调用本地DLL及EXE程序的方法。针对DLL调用,文章提供了基于Java Native Access (JNA) 库的具体实现方案,包括定义Java接口以映射DLL中的函数,并展示了如何加载DLL及调用其中的方法。对于EXE程序的调用,则提出了一种通过批处理文件(BAT)启动外部可执行文件的方式,并通过轮询检查结果文件的存在来判断计算是否完成。此外,还探讨了使用ProcessBuilder启动独立进程来运行DLL调用程序DllRunner.jar,以及如何处理子进程的输入输出流以避免阻塞。文中还提到了在不同JDK版本间编译与运行时可能遇到的兼容性问题及其解决方案。
文章目录
- Java调用DLL程序
- jna介绍
- 依赖
- 编写接口
- 使用
- DLL错误导致java进程退出
- 处理方式
- **在C++代码中加强错误处理**
- **使用守护进程(Watchdog)**
- Java**使用分离的进程调用DLL**
- java调用dll的程序打包成`DllRunner.jar`
- 使用Process新开进程调用`DllRunner.jar`
- Java调用exe
Java调用DLL程序
jna介绍
Java Native Access (JNA) 是一个Java开发库,它允许Java程序直接调用本地操作系统API和C语言库。JNA库通过JNI (Java Native Interface) 进行工作,但是它不需要编写任何C代码或者创建本机库。
依赖
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.14.0</version>
</dependency>
编写接口
编写接口的目的是对应dll的方法,dll的方法如下
import com.sun.jna.Library;
import com.sun.jna.Native;
import java.io.File;
/**
* @Author dam
* @create 2024/7/2 15:24
*/
public interface AlgorithmDll extends Library {
String dllPath = System.getProperty("user.dir") + File.separator + "algorithmDll" + File.separator + "GDUT_PACK.dll";
/**
* 对接C++程序的方法
* @param input 输入
* @return 输出
*/
String RunDLL(String input);
public static AlgorithmDll getInstance() {
return Native.loadLibrary(dllPath, AlgorithmDll.class);
}
}
使用
调用dll的方法
output = AlgorithmDll.getInstance().RunDLL(input);
下面方法的作用是接收一个计算任务,然后调用dll程序进行计算,最后接收dll程序的输出进行返回
public static CIMSTask calculate(CIMSTask cimsTask) {
System.out.println(cimsTask.getNestTaskCode() + "开始计算");
String input = JSON.toJSONString(cimsTask);
String output;
try {
TxtUtil.write(new File(inputPath + "input" + cimsTask.getNestTaskID() + ".json"), input, "utf-8");
output = AlgorithmDll.getInstance().RunDLL(input);
if ("".equals(output)) {
throw new RuntimeException(cimsTask.getNestTaskCode() + "计算失败");
}
System.out.println(cimsTask.getNestTaskCode() + "计算完成");
try {
TxtUtil.write(new File(outputPath + "output" + cimsTask.getNestTaskID() + ".json"), output, "utf-8");
} catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return JSON.parseObject(output, CIMSTask.class);
}
上述代码就可以实现DLL的简单调用,但是如果要多线程调用该方法同时计算多个任务的话,会有风险,请继续阅读下一小节
DLL错误导致java进程退出
当C++的DLL在Java中调用时,如果DLL发生了严重错误导致进程退出(例如PROCESS EXIT(-1)
),Java虚拟机(JVM)也会随之中断。这种情况通常是由于未捕获的异常或访问无效内存等导致的C++崩溃。由于JVM和C++共享同一个进程空间,如果C++导致整个进程崩溃,Java程序也会被迫退出。
如果多个任务在同时计算,一旦一个任务发生了错误中断,会导致所有任务都被迫中断。
处理方式
在C++代码中加强错误处理
首先,确保C++代码中所有可能引发异常或导致崩溃的地方都进行了适当的错误处理。例如,使用try-catch
块捕获所有异常并妥善处理,避免异常传递到Java层导致进程崩溃。
使用守护进程(Watchdog)
编写一个守护进程监控Java应用程序,如果发现Java程序因C++崩溃而终止,可以自动重新启动它。虽然这并不能防止崩溃,但可以提供一种恢复机制。
Java使用分离的进程调用DLL
通过将DLL调用放在一个单独的进程中运行,主Java程序通过IPC(进程间通信)与这个进程进行通信。如果DLL进程崩溃,主Java程序不会受到影响。
java调用dll的程序打包成DllRunner.jar
调用dll程序如下:
import com.sun.jna.Native;
import com.sun.jna.Library;
import java.io.*;
/**
* @Author dam
* @create 2024/8/26 10:17
*/
public class DllRunner {
public interface AlgorithmDll extends Library {
String dllPath = System.getProperty("user.dir") + File.separator + "algorithmDll" + File.separator + "GDUT_PACK.dll";
public static AlgorithmDll getInstance() {
return Native.loadLibrary(dllPath, AlgorithmDll.class);
}
String RunDLL(String input);
}
public static void main(String[] args) throws UnsupportedEncodingException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
PrintWriter writer = new PrintWriter(new OutputStreamWriter(System.out, "UTF-8"), true);
try {
// 从标准输入读取输入数据
String input = reader.readLine();
// 调用DLL函数
String output = AlgorithmDll.INSTANCE.RunDLL(input);
// 将结果输出到标准输出
writer.println("RESULT:" +output);
} catch (Exception e) {
// 处理异常并输出到标准错误
System.err.println("Error: " + e.getMessage());
e.printStackTrace(System.err);
System.exit(-1);
}
}
}
将jna-5.14.0.jar
和DllRunner程序放在一个包下面
执行如下命令,将程序打包
javac -classpath jna-5.14.0.jar -d . DllRunner.java
jar cf DllRunner.jar -C . com/
执行报错,因为有的字符没有响应的编码,直接把注释改成英文就行,或者直接删除注释
打包成功之后的目录如下:
打包之后,可以使用命令jar tf DllRunner.jar
来查看jar包里面的结构
如果运行子进程之后报下面这种错误,意思是使用了更高版本的JDK来编译了程序,但是运行程序的时候使用的JDK版本较低,会出现版本不兼容问题。我的电脑安装了多个JDK,默认使用JDK17来编译了代码,但是后面运行程序使用的是JDK8,所以出现了如下报错
解决方法很简单,在编译代码的时候,使用--release 版本号
来指定就行
javac --release 8 -classpath jna-5.14.0.jar -d . DllRunner.java
jar cf DllRunner.jar -C . com/
关于javac的命令,可以在命令行用javac查看
使用Process新开进程调用DllRunner.jar
我们在主线程中启动了一个新的线程来处理子进程的输出流。由于 ProcessBuilder
中的流是阻塞的,会使用一个缓冲区来存储这些输出,如果子进程的输出数据量占满了缓冲区,可能会导致线程挂起或阻塞,需要使用下面的代码,将dll的输出流合并到java中
// 合并标准输出和错误输出
builder.redirectErrorStream(true);
最终的调用DLL的方法如下
public static CIMSTask calculateWithDll(CIMSTask cimsTask) {
System.out.println(cimsTask.getNestTaskCode() + " 开始计算");
String input = JSON.toJSONString(cimsTask);
AtomicReference<String> output = new AtomicReference<>();
try {
// 将输入数据写入文件
// TxtUtil.write(new File(inputPath + "input" + cimsTask.getNestTaskID() + ".json"), input, "utf-8");
// 使用ProcessBuilder启动DllRunner
String jarPath = System.getProperty("user.dir") + File.separator + "process" + File.separator + "DllRunner.jar;" +
System.getProperty("user.dir") + File.separator + "process" + File.separator + "jna-5.14.0.jar";
System.out.println("jarPath:" + jarPath);
List<String> command = Arrays.asList(
"java",
"-cp",
jarPath,
"com.dam.algorithm.DllRunner"
);
ProcessBuilder builder = new ProcessBuilder(command);
// 合并标准输出和错误输出
builder.redirectErrorStream(true);
Process process = builder.start();
// 向进程的标准输入写入任务数据
OutputStream outputStream = process.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true);
writer.println(input);
// 确保所有数据都被写入
writer.flush();
// 处理子进程输出和错误流
new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
// 将子进程的输出打印到主进程运行窗口
if (line.contains("RESULT:")) {
output.set(line.split("RESULT:")[1]);
} else {
System.out.println("任务:" + cimsTask.getNestTaskCode() + "的子进程输出:" + line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 等待进程结束
int exitVal = process.waitFor();
if (exitVal == 0) {
System.out.println("子进程运行成功");
} else {
System.err.println("子进程运行出错,错误码: " + exitVal);
}
if (output.get() == null || output.get().isEmpty()) {
throw new RuntimeException(cimsTask.getNestTaskCode() + " 计算失败");
}
System.out.println(cimsTask.getNestTaskCode() + " 计算完成");
} catch (Exception e) {
throw new RuntimeException(e);
}
CIMSTask cimsTaskRes = JSON.parseObject(output.get(), CIMSTask.class);
cimsTaskRes.setNestTaskCode(cimsTask.getNestTaskCode());
return cimsTaskRes;
}
Java调用exe
上面的调用方法比较麻烦,需要先打包才能调用,也可以通过调用exe来执行c++程序。调用逻辑是先生成一个bat文件,在bat文件中定位到exe程序所在位置,然后执行exe。"> " + cimsTask.getNestTaskCode() + "_output.log 2>&1"
的作用是把exe的输出转移到日志文件中,防止exe进程陷入阻塞状态
/**
* 调用exe来求解
* 通过自旋来判断任务是否计算完成
*
* @param cimsTask
* @return
*/
public static CIMSTask calculateWithExe(CIMSTask cimsTask) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(cimsTask.getNestTaskCode() + " 开始计算,时间:" + sdf.format(new Date()));
String input = JSON.toJSONString(cimsTask);
try {
// 将输入数据写入文件
String taskInputPath = inputPath + cimsTask.getNestTaskCode() + ".json";
TxtUtil.write(new File(taskInputPath), input, "utf-8");
String exeStr =
exePath.substring(0, 1) + ":\n" +
"cd " + exePath + "\n" +
"GDUT_PACK.exe " + cimsTask.getNestTaskCode() + ".json> " + cimsTask.getNestTaskCode() + "_output.log 2>&1";
String batPath = exePath + File.separator + "bat" + File.separator + cimsTask.getNestTaskCode() + ".bat";
TxtUtil.write(new File(batPath), exeStr, "utf-8");
System.out.println("batPath:" + batPath);
// 执行bat文件,开始计算
ProcessBuilder processBuilder = new ProcessBuilder(batPath);
Process process = processBuilder.start();
// 扫描获取结果
String resultPath = outputPath + cimsTask.getNestTaskCode() + ".json";
File resFile;
while (true) {
resFile = new File(resultPath);
if (resFile.exists()) {
System.out.println(cimsTask.getNestTaskCode() + " 计算完成,时间:" + sdf.format(new Date()));
String resultStr = TxtUtil.read(new File(outputPath + cimsTask.getNestTaskCode() + ".json"), "utf-8");
CIMSTask resCimsTask = JSON.parseObject(resultStr, CIMSTask.class);
resCimsTask.setNestTaskCode(cimsTask.getNestTaskCode());
// 删除结果文件
resFile.delete();
// 删除输入文件
File taskInputFile = new File(taskInputPath);
if (taskInputFile.exists()) {
taskInputFile.delete();
}
// 删除bat文件
File batFile = new File(batPath);
if (batFile.exists()) {
batFile.delete();
}
// 删除输出日志
File logFile = new File(exePath + File.separator + cimsTask.getNestTaskCode() + "_output.log");
if (logFile.exists()) {
logFile.delete();
}
// 停止进程
process.destroy();
return resCimsTask;
}
Thread.sleep(1000);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
这种方法实现比较方便,但是有几个缺点:
- 主进程无法得知子进程在什么时候执行完毕,只能通过自旋的方式来判断结果文件是否生成,文件成功生成则说明exe运行结束
- 需要将exe的输出转移到log文件中,如果进程出错进入死循环,疯狂输出,可能导致日志文件非常大。当然可以定时清空日志里面的内容来解决该问题
- 当java程序判断出exe程序存在问题时,无法直接通过java代码中断exe程序