目前java提供两种方式调用Windows下的其他exe程序, Runtime和ProcessBulider。原理相同,同时通过执行类似cmd命令的方式启动和关闭程序。两种方法都会返回一个用于管理操作系统进程的Process对象,最终生成的Process子进程独立于Jvm之外,就算没有对象应用指向它,也不会被GC关闭或者释放。
创建子进程Process后,要及时取走子进程输出信息和错误信息,否则很可能因为信息太多导致子进程阻塞。
这里我就以ProcessBulider为例,介绍一下Springboot代码中如何实现其功能。
ProcessBuilder是java 5.0引入的,start()方法返回Process的一个实例。创建Controller层代码,只要实现两个方法,开启程序的startEXE()方法和关闭程序的closetEXE()方法:
@RestController
@Slf4j
@RequestMapping("/test")
public class ProcessController {
@RequestMapping("/start")
//通过cmd命令开启exe程序
public String startEXE() throws IOException {
log.info("Line[" + Thread.currentThread().getStackTrace()[1].getLineNumber() + "] :start cuteftppro.exe");
ProcessBuilder process=createProcessBuilder("cuteftppro.exe","D:\\yaken\\Program Files\\CuteFTP9",null);
try {
Process proc = process.start();
//执行程序
// BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream(), "gbk"));
// SequenceInputStream是一个串联流,能够把两个流结合起来,可以将getInputStream和getErrorStream获取到的流一起查看
BufferedReader br = new BufferedReader(new InputStreamReader(new SequenceInputStream(proc.getInputStream(), proc.getErrorStream()), "gbk"));
CmdGetter cg = new CmdGetter(br, proc);
cg.start();
log.info("Line[" + Thread.currentThread().getStackTrace()[1].getLineNumber() + "] :process log - "+cg.output.toString());
} catch (Exception e) {
log.error("子线程错误");
}
return "Your application has been launched !!!";
}
private ProcessBuilder createProcessBuilder(String executable,String filePath, List<String> args) {
// 针对以.exe结尾的可执行文件 将先判断其是否存在
if (!(FileUtil.exist(filePath+File.separator+executable))) {
throw new IllegalArgumentException("找不到对应可执行文件:" + executable);
}
List<String> cmd = new ArrayList<>();
cmd.add("cmd");
cmd.add("/c");
cmd.add(executable);
if (CollUtil.isNotEmpty(args)) {
cmd.addAll(args);
}
log.info("将执行的命令-->{}", cmd);
File dir = FileUtil.file(filePath);
log.info("命令执行的目录-->{}", dir.getAbsolutePath());
//directory的参数为工作路径
return new ProcessBuilder(cmd).directory(dir).redirectErrorStream(true);
}
@RequestMapping("/close")
//通过cmd命令,关闭程序
public String closeEXE(){
log.info("Line[" + Thread.currentThread().getStackTrace()[1].getLineNumber() + "] :close cuteftppro.exe");
closeHistoryProgram("cuteftppro.exe");
return "Your application has been closed !!!";
}
public void closeHistoryProgram(String processName){
String cmd = "taskkill /f /t /im "+processName;
try
{
//exec执行cmd命令
Process process = Runtime.getRuntime().exec(cmd);
//获取CMD命令结果的输出流
InputStream fis=process.getInputStream();
InputStream isE=process.getErrorStream();
InputStreamReader isr=new InputStreamReader(new SequenceInputStream(isI, isE), "GBK");
//使用缓冲器读取
BufferedReader br=new BufferedReader(isr);
String line=null;
//全部读取完成为止,一行一行读取
while((line=br.readLine())!=null)
{
//输出读取的内容
System.out.println( line );
}
}catch (IOException e)
{
e.printStackTrace();
}
}
}
通过SequenceInputStream对象将getInputStream和getErrorStream合并。
专门创建了CmdGetter,并继承Thread,以便开启多线程支持。
public class CmdGetter extends Thread {
private BufferedReader br;
public static List<String> output;
private Process process;
public CmdGetter(BufferedReader br, Process p) {
this.br = br;
this.process = p;
}
@Override
public void run() {
output = new ArrayList<>();
String line;
try {
while (process.isAlive() && (line = br.readLine()) != null) {
output.add(line);
if (output.size() >= 20) {
output.clear();
output.add(line);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
最后在前端添加两个按钮用于测试程序的启动与停止,并绑定事件。我这里使用了vue3,前后端分离。
<template>
<!--用于测试打开系统安装的程序-->
<el-button type="warning" @click="startExe">Start</el-button>
<el-button type="warning" @click="closeExe">Close</el-button>
</template>
<script setup>
import requestUtil from '@/util/request'
const startExe=async ()=>{
let result=await requestUtil.get("test/start");
console.log("start cuteftppro.exe : "+result.data);
}
const closeExe=async ()=>{
let result=await requestUtil.get("test/close");
console.log("close cuteftppro.exe : "+result.data);
}
</script>
其中用到了request组件的get请求方法,组件代码如下所示:
// 引入axios
import axios from 'axios';
import store from '@/store'
//请求地址:后台地址
// let baseUrl="http://localhost:80/";
// let baseUrl="http://192.168.9.50:80/";
let baseUrl="http://127.0.0.1:80/";
// 创建实例
const httpService = axios.create({
baseURL: baseUrl,
timeout: 3000
});
// 请求拦截器
httpService.interceptors.request.use(
function (config) {
// 在发送请求前做处理
let token = store.getters.GET_TOKEN;
if(token){
console.log("从sessionStorage中获取到的token:"+token);
config.headers.token=token;
}else {
let token_1 = store.state.token;
console.log("从store.state中获取到的token:"+token_1);
config.headers.token=token_1;
}
return config;
}, function (error) {
// 对请求错误做处理
return Promise.reject(error);
});
// 响应拦截器
httpService.interceptors.response.use(function (response) {
// 对响应数据做处理
return response;
}, function (error) {
// 对响应错误做处理
return Promise.reject(error);
});
/*
* get请求
* url:请求地址
* params:参数
* */
export function get(url, params = {}) {
return new Promise((resolve, reject) => {
httpService({
url: url,
method: 'get',
params: params,
}).then(response => {
console.log(response);
resolve(response);
}).catch(error => {
console.log(error);
reject(error);
});
});
}
export function getServerUrl(){
return baseUrl;
}
export default {
get,
post,
fileUpload,
getServerUrl
}
前后台代码编写完成,现在来做个测试,启动前、后台工程,分别点击两个按钮查看结果:
可以看到从后台控制台打出来的消息, 分别对应着点击事件。到此为止,基本功能已实现。