java Runtime.exec()执行shell/cmd命令:常见的几种陷阱与一种完善实现

news/2024/11/17 18:47:37/文章来源:https://www.cnblogs.com/bigcat26/p/18550891

@

目录
  • 背景说明
  • 前言
  • Runtime.exec()常见的几种陷阱以及避免方法
    • 陷阱1:IllegalThreadStateException
    • 陷阱2:Runtime.exec()可能hang住,甚至死锁
    • 陷阱3:不同平台上,命令的兼容性
    • 陷阱4:错把Runtime.exec()的command参数当做命令行
  • 一个比较完善的工具类
    • 封装返回结果
    • 对外接口
    • StreamGobbler类,用来完成stream的管理
    • 实现类

背景说明

我们项目要java执行命令“dmidecode -s system-uuid”获取结果,然而碰到问题,当项目一直执行好久后,Runtime.getRuntime().exec()获取结果为空,但也不报错,重启项目就又可以了,所以猜测属于陷阱2,并进行记录。

Runtime.getRuntime().exec()执行JVM之外的程序:常见的几种陷阱

前言

日常java开发中,有时需要通过java运行其它应用功程序,比如shell命令等。jdk的Runtime类提供了这样的方法。首先来看Runtime类的文档, 从文档中可以看出,每个java程序只会有一个Runtime实例,显然这是一个单例模式。

/*** Every Java application has a single instance of class* <code>Runtime</code> that allows the application to interface with* the environment in which the application is running. The current* runtime can be obtained from the <code>getRuntime</code> method.* <p>* An application cannot create its own instance of this class.*/public class Runtime {private static Runtime currentRuntime = new Runtime();/*** Returns the runtime object associated with the current Java application.* Most of the methods of class <code>Runtime</code> are instance* methods and must be invoked with respect to the current runtime object.** @return  the <code>Runtime</code> object associated with the current*          Java application.*/public static Runtime getRuntime() {return currentRuntime;}/** Don't let anyone else instantiate this class */private Runtime() {}......
}

要运行JVM中外的程序,Runtime类提供了如下方法,详细使用方法可参见源码注释

public Process exec(String command) throws IOExceptionpublic Process exec(String cmdarray[]) throws IOExceptionpublic Process exec(String command, String[] envp) throws IOExceptionpublic Process exec(String command, String[] envp, File dir) throws IOExceptionpublic Process exec(String[] cmdarray, String[] envp) throws IOExceptionpublic Process exec(String[] cmdarray, String[] envp, File dir) throws IOException

通过这种方式运行外部程序,有几个陷阱需要注意,本文尝试总结常见的几个陷阱,并给出相应的解决方法。同时封装一种比较完善的工具类,用来运行外部应用,并提供超时功能。

Runtime.exec()常见的几种陷阱以及避免方法

陷阱1:IllegalThreadStateException

通过exec执行java命令为例子,最简单的方式如下。执行exec后,通过Process获取外部进程的返回值并输出。

import java.io.IOException;/*** Created by yangjinfeng02 on 2016/4/27.*/
public class Main {public static void main(String[] args) {Runtime runtime = Runtime.getRuntime();try {Process process = runtime.exec("java");int exitVal = process.exitValue();System.out.println("process exit value is " + exitVal);} catch (IOException e) {e.printStackTrace();}}
}

很遗憾的是,我们发现输出结果如下,抛出了IllegalThreadStateException异常

Exception in thread "main" java.lang.IllegalThreadStateException: process has not exited
at java.lang.ProcessImpl.exitValue(ProcessImpl.java:443)
at com.baidu.ubqa.agent.runner.Main.main(Main.java:18)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

为什么会抛出IllegalThreadStateException异常?

这是因为外部线程还没有结束,这个时候去获取退出码,exitValue()方法抛出了异常。看到这里读者可能会问,为什么这个方法不能阻塞到外部进程结束后再返回呢?确实如此,Process有一个waitFor()方法,就是这么做的,返回的也是退出码。因此,我们可以用waitFor()方法替换exitValue()方法。

陷阱2:Runtime.exec()可能hang住,甚至死锁

首先看下Process类的文档说明

 * <p>By default, the created subprocess does not have its own terminal* or console.  All its standard I/O (i.e. stdin, stdout, stderr)* operations will be redirected to the parent process, where they can* be accessed via the streams obtained using the methods* {@link #getOutputStream()},* {@link #getInputStream()}, and* {@link #getErrorStream()}.* The parent process uses these streams to feed input to and get output* from the subprocess.  Because some native platforms only provide* limited buffer size for standard input and output streams, failure* to promptly write the input stream or read the output stream of* the subprocess may cause the subprocess to block, or even deadlock.

从这里可以看出,Runtime.exec()创建的子进程公用父进程的流,不同平台上,父进程的stream buffer可能被打满导致子进程阻塞,从而永远无法返回。
针对这种情况,我们只需要将子进程的stream重定向出来即可。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;/*** Created by yangjinfeng02 on 2016/4/27.*/
public class Main {public static void main(String[] args) {List<String> strList = new ArrayList<>();Process process = null;InputStreamReader ir = null;LineNumberReader input = null;InputStreamReader errorStream = null;LineNumberReader errorReader = null;try {process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", shStr});// 处理标准输出流ir = new InputStreamReader(process.getInputStream());input = new LineNumberReader(ir);String line;while ((line = input.readLine()) != null){line = line.replaceAll(" ","");strList.add(line.toUpperCase());}// 处理错误输出流errorStream = new InputStreamReader(process.getErrorStream());errorReader = new LineNumberReader(errorStream);String errorLine;while ((errorLine = errorReader.readLine()) != null) {// 可以选择处理错误信息,或者直接打印出来System.err.println("Error output: " + errorLine);}// 等待命令执行结束process.waitFor();} catch (Exception e) {log.error("执行Shell命令失败", e);} finally {try {// 关闭输入流和错误流if (input != null) {input.close();}if (ir != null) {ir.close();}if (errorReader != null) {errorReader.close();}if (errorStream != null) {errorStream.close();}// 销毁进程if (process != null) {process.destroy();}} catch (IOException e) {log.error("关闭Shell流失败", e);}}return strList;}
}

陷阱3:不同平台上,命令的兼容性

如果要在windows平台上运行dir命令,如果直接指定命令参数为dir,会提示命令找不到。而且不同版本windows系统上,运行改命令的方式也不一样。对这宗情况,需要根据系统版本进行适当区分。

String osName = System.getProperty("os.name" );
String[] cmd = new String[3];
if(osName.equals("Windows NT")) {cmd[0] = "cmd.exe" ;cmd[1] = "/C" ;cmd[2] = args[0];
} else if(osName.equals("Windows 95")) {cmd[0] = "command.com" ;cmd[1] = "/C" ;cmd[2] = args[0];
}  
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(cmd);

陷阱4:错把Runtime.exec()的command参数当做命令行

本质上来讲,Runtime.exec()的command参数只是一个可运行的命令或者脚本,并不等效于Shell解器或者Cmd.exe,如果你想进行输入输出重定向,pipeline等操作,则必须通过程序来实现。不能直接在command参数中做。例如,下面的例子

Process process = runtime.exec("java -version > a.txt");

这样并不会产出a.txt文件。要达到这种目的,需通过编程手段实现,如下

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;/*** Created by yangjinfeng02 on 2016/4/27.*/
class StreamGobbler extends Thread {InputStream is;String type;OutputStream os;StreamGobbler(InputStream is, String type) {this(is, type, null);}StreamGobbler(InputStream is, String type, OutputStream redirect) {this.is = is;this.type = type;this.os = redirect;}public void run() {try {PrintWriter pw = null;if (os != null)pw = new PrintWriter(os);InputStreamReader isr = new InputStreamReader(is);BufferedReader br = new BufferedReader(isr);String line;while ((line = br.readLine()) != null) {if (pw != null)pw.println(line);System.out.println(type + ">" + line);}if (pw != null)pw.flush();} catch (IOException ioe) {ioe.printStackTrace();}}
}public class Main {public static void main(String args[]) {try {FileOutputStream fos = new FileOutputStream("logs/a.log");Runtime rt = Runtime.getRuntime();Process proc = rt.exec("cmd.exe /C dir");// 重定向输出流和错误流StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR");StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT", fos);errorGobbler.start();outputGobbler.start();int exitVal = proc.waitFor();System.out.println("ExitValue: " + exitVal);fos.flush();fos.close();} catch (Throwable t) {t.printStackTrace();}}
}

一个比较完善的工具类

下面提供一种比较完善的实现,提供了超时功能。

封装返回结果

/**
* ExecuteResult.java
*/
import lombok.Data;
import lombok.ToString;@Data
@ToString
public class ExecuteResult {private int exitCode;private String executeOut;public ExecuteResult(int exitCode, String executeOut) {this.exitCode = exitCode;this.executeOut = executeOut;}
}

对外接口

/**
* LocalCommandExecutor.java
*/
public interface LocalCommandExecutor {ExecuteResult executeCommand(String command, long timeout);
}

StreamGobbler类,用来完成stream的管理

/**
* StreamGobbler.java
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class StreamGobbler extends Thread {private static Logger logger = LoggerFactory.getLogger(StreamGobbler.class);private InputStream inputStream;private String streamType;private StringBuilder buf;private volatile boolean isStopped = false;/*** @param inputStream the InputStream to be consumed* @param streamType  the stream type (should be OUTPUT or ERROR)*/public StreamGobbler(final InputStream inputStream, final String streamType) {this.inputStream = inputStream;this.streamType = streamType;this.buf = new StringBuilder();this.isStopped = false;}/*** Consumes the output from the input stream and displays the lines consumed* if configured to do so.*/@Overridepublic void run() {try {// 默认编码为UTF-8,这里设置编码为GBK,因为WIN7的编码为GBKInputStreamReader inputStreamReader = new InputStreamReader(inputStream, "GBK");BufferedReader bufferedReader = new BufferedReader(inputStreamReader);String line = null;while ((line = bufferedReader.readLine()) != null) {this.buf.append(line + "\n");}} catch (IOException ex) {logger.trace("Failed to successfully consume and display the input stream of type " + streamType + ".", ex);} finally {this.isStopped = true;synchronized (this) {notify();}}}public String getContent() {if (!this.isStopped) {synchronized (this) {try {wait();} catch (InterruptedException ignore) {ignore.printStackTrace();}}}return this.buf.toString();}
}

实现类

通过SynchronousQueue队列保证只有一个线程在获取外部进程的退出码,由线程池提供超时功能。

/**
* LocalCommandExecutorImpl.java
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;public class LocalCommandExecutorImpl implements LocalCommandExecutor {static final Logger logger = LoggerFactory.getLogger(LocalCommandExecutorImpl.class);static ExecutorService pool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 3L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());public ExecuteResult executeCommand(String command, long timeout) {Process process = null;InputStream pIn = null;InputStream pErr = null;StreamGobbler outputGobbler = null;StreamGobbler errorGobbler = null;Future<Integer> executeFuture = null;try {logger.info(command.toString());process = Runtime.getRuntime().exec(command);final Process p = process;// close process's output stream.p.getOutputStream().close();pIn = process.getInputStream();outputGobbler = new StreamGobbler(pIn, "OUTPUT");outputGobbler.start();pErr = process.getErrorStream();errorGobbler = new StreamGobbler(pErr, "ERROR");errorGobbler.start();// create a Callable for the command's Process which can be called by an ExecutorCallable<Integer> call = new Callable<Integer>() {public Integer call() throws Exception {p.waitFor();return p.exitValue();}};// submit the command's call and get the result from aexecuteFuture = pool.submit(call);int exitCode = executeFuture.get(timeout, TimeUnit.MILLISECONDS);return new ExecuteResult(exitCode, outputGobbler.getContent());} catch (IOException ex) {String errorMessage = "The command [" + command + "] execute failed.";logger.error(errorMessage, ex);return new ExecuteResult(-1, null);} catch (TimeoutException ex) {String errorMessage = "The command [" + command + "] timed out.";logger.error(errorMessage, ex);return new ExecuteResult(-1, null);} catch (ExecutionException ex) {String errorMessage = "The command [" + command + "] did not complete due to an execution error.";logger.error(errorMessage, ex);return new ExecuteResult(-1, null);} catch (InterruptedException ex) {String errorMessage = "The command [" + command + "] did not complete due to an interrupted error.";logger.error(errorMessage, ex);return new ExecuteResult(-1, null);} finally {if (executeFuture != null) {try {executeFuture.cancel(true);} catch (Exception ignore) {ignore.printStackTrace();}}if (pIn != null) {this.closeQuietly(pIn);if (outputGobbler != null && !outputGobbler.isInterrupted()) {outputGobbler.interrupt();}}if (pErr != null) {this.closeQuietly(pErr);if (errorGobbler != null && !errorGobbler.isInterrupted()) {errorGobbler.interrupt();}}if (process != null) {process.destroy();}}}private void closeQuietly(Closeable c) {try {if (c != null) {c.close();}} catch (IOException e) {logger.error("exception", e);}}
}

重要信息

  • 官网:https://ais.cn/u/vEbMBz

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/835461.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

昆工891数据库系统原理强化课程

--昆工昆明理工大学、计算机技术、人工智能、软件工程、网络空间安全、891计算机专业核心综合、计算机系统结构、计算机软件与理论、网络与信息安全、计算机应用技术、综合程序设计、通信工程、817信号与系统、信号与信息处理、通信与信息系统

第7篇Scrum博客

1.站立式会议 1.1 会议照片1.2 会议内容 昨天已完成的工作: 昨天已基本实现用条形图,折线图,饼图展示数据界面功能。 今天计划完成的工作项目模块 需要实现的功能 负责人 预计用时主界面模块 整合代码,查漏补缺 王伊若 5h主界面模块 主界面设计 王伊若 2h主界面模块 查询界…

Ant Design Vue组件安装

https://www.antdv.com/docs/vue/getting-started-cn

书生共学大模型实战营L1G6000 XTuner微调

任务描述:使用XTuner微调InternLM2-Chat-7B实现自己的小助手认知 该任务分为数据集处理、微调训练、合并部署三个环节。数据处理:主要是将目标json文件中的字段替换为和自己用户名相关的字段,这里我们将“尖米”替换为“科研狗1031”:微调训练:采用教程中的XTuner框架,在…

request to https://registry.npm.taobao.org/ant-design-vue failed, reason: certificate has expire

一、原因分析 其实早在 2021 年,淘宝就发文称,npm 淘宝镜像已经从 http://registry.npm.taobao.org 切换到了 http://registry.npmmirror.com。旧域名也将于 2022 年 5 月 31 日停止服务(直到 HTTPS 证书到期才真正不能用了)2024年1 月 22 日,淘宝原镜像域名(http…

【学校训练记录】11月个人训练赛4个人题解

A题意可以理解为在a,b的范围内如果一个数是某个整数的立方,求与其距离为k的范围内有几个整数的平方数,我们可以对于每个立方数求出其数量,注意边界问题 #include <bits/stdc++.h> #define int long long using namespace std;int a, b, k; void solve(){cin >>…

第六篇Scrum博客

1.站立式会议 1.1 会议照片1.2 会议内容 昨天已完成的工作: 已经完成了账目的查询界面功能,按日期、备注以及收入支出查询等功能。 今天计划完成的工作项目模块 需要实现的功能 负责人 预计用时主界面模块 协助他人完成工作 王伊若 2h主界面模块 分类报告界面 王伊若 3h主界面…

学校个人训练记录

A题意可以理解为在a,b的范围内如果一个数是某个整数的立方,求与其距离为k的范围内有几个整数的平方数,我们可以对于每个立方数求出其数量,注意边界问题 #include <bits/stdc++.h> #define int long long using namespace std;int a, b, k; void solve(){cin >>…

Scrum 冲刺博客-day2

一、每天会议 昨天完成的任务与今天计划完成任务成员 昨天已完成任务 今天计划完成任务董雯霖 组织会议,确立各自工作 用户注册页面陈金星 参会,发表意见 用户登录页面邱列圻 参会,发表意见 用户模块的接口开发李嘉远 参会,发表意见 页面测试詹洛熙 参会,发表意见 接口测试…

第4篇Scrum冲刺博客

1.站立式会议 1.1 会议照片1.2 会议内容 昨天已完成的工作: 已初步完成主界面设计和数据库编写记录 今天计划完成的工作项目模块 需要实现的功能 负责人 预计用时数据库模块 数据库记录的备份、恢复和退出 王伊若 2h主界面模块 账目记录的增删改功能及界面 王伊若 6h主界面模块…

闲话 11.17(附『模拟赛』多校A层冲刺NOIP2024模拟赛23)(更新模拟赛 T3)

杂项乱写 11.17$settle\ into\ ash$ 好大雷 EP,真的耐听。The embers settle into ash 残火中 余温成灰 Refuse to bend, to break, look back 不屈 不折 不曾回眸往昔 It’s all decided in the moment we both choose to fight it 在那决断时刻 我们选择了抗争 You don’t n…