在Java中我们可以使用ProcessBuilder来创建一个进程并执行命令。在使用前我们首先了了解下两个程序。
一、认识命令行解释程序
不论在windows中还是linux中都可以通过命令行方式来执行某些程序或脚本,而解析命令行的程序被称为shell,其本身是一种命令行解释器,用于与操作系统进行交互和执行命令。
我们在java中执行某些命令的时也是借助操作系统的shell程序来完成的,但Windows系统和Linux系统中的shell是有区别的,我们先看看两类系统中的shell应该如何被使用。
1、Windows
在windows中可通过cmd.exe来启动一个新的命令解释器实例,其格式为
CMD [/A | /U] [/Q] [/D] [/E:ON | /E:OFF] [/F:ON | /F:OFF] [/V:ON | /V:OFF]
[[/S] [/C | /K] string]
CMD可单独执行,例如我们在widows系统中通过cmd命令打开命令行窗口进行交互,也可以携带一些参数和指令直接执行。
关于CMD的更多说明可在windows系统中通过 help cmd 命令查看
与在Java中执行命令相关的命令格式为
CMD [[/S] [/C | /K] string]
- /C 执行字符串指定的命令然后终止
- /K 执行字符串指定的命令但保留
- /S 修改 /C 或 /K 之后的字符串处理
如果指定了 /C 或 /K,则会将其之后的剩余部分作为一个命令行处理,其中,会使用下列逻辑处理可能存在的引号(")字符:
- 如果符合下列所有条件,则会保留命令行上的引号字符:
- 不带/S开关
- 正好两个引号字符
- 在两个引号字符之间无任何特殊字符.特殊字符:&<>()@^|
- 在两个引号字符之间至少有一个空格字符
- 在两个引号字符之间的字符串是某个可执行文件的名称。
- 否则,老办法是看第一个字符是否是引号字符,如果是,则去掉首字符并删除命令行上最后一个引号,保留最后一个引号之后的所有文本。
例如要执行以下命令
cmd /c D:/Program Files/apache-maven-3.9.1/bin/mvn compile
由于路径中的“Program Files”带有空格,会导致解析异常,所以我们可以按照以下方式修改
cmd /c "D:/Program Files/apache-maven-3.9.1/bin/mvn" compile
或者
cmd /c D:/"Program Files"/apache-maven-3.9.1/bin/mvn compile
2、Linux
在Linux中可以通过以下的方式来执行命令行文件或命令
sh <command_file> [argument...]
sh -c <command_string> [command_name [argument...]]
command_file:包含命令的文件的路径名。如果路径名包含一个或多个<斜杠>字符,则实现尝试读取该文件;该文件不必是可执行的。如果路径名不包含<斜杠>字符:
argument:位置参数($1,$2,等等)如果有,应该设置为实参。
command_string:由shell解释为一个或多个命令的字符串。如果command_string操作数为空字符串,sh将以零退出状态退出。
command_name:在执行command_string中的命令时分配给特殊参数0的字符串。如果未指定command_name,则应将特殊参数0设置为从父程序传递给sh的第一个参数的值(例如,C程序的argv[0]),该参数通常是用于执行sh实用程序的路径名。
如果有-c选项,则从string中读取命令。如果字符串后面有参数,则将它们赋值给位置形参,从$0开始。
注意:对同样的命令行,其在windows和linux中得到的解释会有所不同,例如有以下的命令
mvn clean compile
在windows中通过[cmd /c mvn clean compile]是可以正常执行的,因为cmd会将/c后面的所有内容作为一个命令行;当然也可以改成[cmd /c 'mvn clean compile']
但在linux中通过“sh -c mvn clean compile”执行时,其sh只会将-c后的第一个作为命令行字符串,后面的只是当作参数,也就是只执行了mvn命令;如果要让其达到希望的结果,则可以改成 [sh -c 'mvn $0 $1' clean compile]; 也可以将其直接改成[sh -c 'mvn clean compile'] ;其中的单引号也可以换成双应用;
二、使用ProcessBuilder
1、ProcessBuilder介绍
ProcessBuilder类用于创建操作系统进程,每个ProcessBuilder实例管理一个Process属性集合,通过start()方法用这些属性创建一个新的Process实例。
start()方法可以从同一实例中重复调用,以创建具有相同或相关属性的新子流程。
每个ProcessBuilder管理这些流程属性:
1) 一个字符串列表形式的命令(command),表示要调用的外部程序文件及其参数(如果有的话).哪个字符串列表表示有效的操作系统命令取决于系统
2) 一个environment,它是从变量到值的依赖于系统的映射。初始值是当前进程环境的副本(参见System.getenv())。
3) 工作目录(working directory),默认值是当前进程的当前工作目录,通常是由系统属性user.dir命名的目录。
4) 标准输入的来源。默认情况下,子进程从管道读取输入。Java代码可以通过Process.getOutputStream()返回的输出流访问该管道。 但是,可以使用redirectInput将标准输入重定向到另一个源。在这种情况下,Process.getOutputStream()将返回一个空输出流,其中:
- 写方法总是抛出IOException
- close方法什么也不做
5)标准输出和标准错误的目标。默认情况下,子流程将标准输出和标准错误写入管道。Java代码可以通过Process.getInputStream()和Process.getErrorStream()返回的输入流访问这些管道。但是,可以使用redirectOutput和redirectError将标准输出和标准错误重定向到其他目的地。在这种情况下,Process.getInputStream()和/或Process.getErrorStream()将返回一个空输入流,其中:
- read方法总是返回-1
- 可用方法总是返回0
- close方法什么也不做
6)redirectErrorStream属性。最初该属性为false,这意味着子流程的标准输出和错误输出被发送到两个独立的流,可以使用Process.getInputStream()和Process.getErrorStream()方法访问这两个流。如果该值被设置为true,则:
- 标准错误与标准输出合并,并始终发送到相同的目的地(这使得将错误消息与相应的输出关联起来更容易)
- 标准错误和标准输出的共同目标可以使用redirectOutput重定向
- 在创建子进程时,redirectError方法设置的任何重定向都将被忽略
- Process.getErrorStream()返回的流将始终是一个空输入流
修改进ProcessBuilder的属性将影响随后由该对象的start()方法启动的进程,但不会影响先前启动的进程或Java进程本身。 大多数错误检查是由start()方法执行的。可以修改对象的状态,使start()失败。例如,将command属性设置为空列表不会抛出异常,除非调用start()。
注意,这个类不是同步的。如果多个线程并发访问ProcessBuilder实例,并且至少有一个线程在结构上修改了其中一个属性,则必须在外部同步。
2、常用方法介绍
public void methods(File workingDirectory,List<String> commands){
ProcessBuilder processBuilder = new ProcessBuilder() ;
/*
* 使用指定的操作系统程序和参数构造进程构建器。
* 此构造函数不复制命令列表。对列表的后续更新将反映在流程构建器的状态中。
* 不检查command是否对应有效的操作系统命令。
* */
//new ProcessBuilder(List<String> command) ;
/*
* 使用指定的操作系统程序和参数构造进程构建器。
* 这是一个方便的构造函数,它将流程构建器的命令设置为一个字符串列表,其中包含与命令数组相同的字符串,顺序相同。
* 不检查command是否对应有效的操作系统命令。
* */
//new ProcessBuilder(String... command) ;
//设置此ProcessBuilder的操作系统程序和参数。如果构造函数没有设置命令可通过方法设置
processBuilder.command(commands) ;
//返回此进程构建器的操作系统程序和参数。
List<String> commandList = processBuilder.command() ;
//设置工作目录.默认是当前进程的System.getProperty("user.dir")值
processBuilder.directory(workingDirectory) ;
//返回此ProcessBuilder环境的字符串映射视图
Map<String,String> environment = processBuilder.environment() ;
//添加环境变量(如果有必要)
environment.put("EVN_NAME","") ;
/*
* 将子进程标准I/O的源和目标设置为与当前Java进程的源和目标相同。
* 这是一个便捷方法,等同调用了下面三个方法
* redirectInput(Redirect.INHERIT)
* redirectOutput(Redirect.INHERIT)
* redirectError(Redirect.INHERIT)
* */
processBuilder.inheritIO() ;
/*
* 设置此进程构建器的redirectErrorStream属性
* 如果此属性为true,则随后由该对象的start()方法启动的子进程生成的任何错误输出将与标准输出合并,
* 以便可以使用Process.getInputStream()方法读取两者。
* 这使得将错误消息与相应的输出相关联变得更加容易。初始值为false。
* */
processBuilder.redirectErrorStream(true) ;
/*
* 说明此ProcessBuilder是否合并标准错误和标准输出
* */
boolean flag = processBuilder.redirectErrorStream() ;
//将此流程构建器的标准输入源设置为文件
processBuilder.redirectInput(new File("")) ;
//设置此流程构建器的标准输入源
processBuilder.redirectInput(ProcessBuilder.Redirect.to(new File("log.txt"))) ;
//返回此流程构建器的标准输入源。
ProcessBuilder.Redirect redirectInput = processBuilder.redirectInput() ;
//将此流程构建器的标准输出目的地设置为文件。
processBuilder.redirectOutput(new File("")) ;
//设置此流程构建器的标准输出目的地。
processBuilder.redirectOutput(ProcessBuilder.Redirect.from(new File("command.txt"))) ;
//返回此流程构建器的标准输出目的地。
ProcessBuilder.Redirect redirectOutput = processBuilder.redirectOutput() ;
//将此进程构建器的标准错误目标设置为文件
processBuilder.redirectError(new File("")) ;
//设置此流程构建器的标准错误目的地。
processBuilder.redirectError( ProcessBuilder.Redirect.PIPE) ;
//返回此流程构建器的标准错误目的地。
ProcessBuilder.Redirect redirectError = processBuilder.redirectError() ;
/*
* 使用此流程构建器的属性启动新流程。
* Process可用于控制该进程并获取有关该进程的信息,其提供了执行进程输入、执行进程输出、等待进程完成、检查进程退出状态以及销毁(杀死)进程的方法
*
* 创建进程的方法可能不适用于某些本机平台上的特殊进程,例如本机窗口进程、守护进程、Windows上的Win16/DOS进程或shell脚本
* 默认情况下,创建的子进程没有自己的终端或控制台。它的所有标准I/O(即stdin, stdout, stderr)操作将被重定向到父进程,
* 在那里它们可以通过使用getOutputStream(), getInputStream()和getErrorStream()方法获得的流来访问。
* 父进程使用这些流向子进程提供输入并从子进程获取输出。由于某些本机平台仅为标准输入和输出流提供有限的缓冲区大小,因此不能及时写入输入流或读取子进程的输出流可能会导致子进程阻塞,甚至死锁。
* 如果需要,还可以使用ProcessBuilder类的方法重定向子进程I/O。
*
* 当不再有对Process对象的引用时,子进程不会被终止,而是继续异步执行。
* 由process对象表示的进程并不要求相对于拥有process对象的Java进程异步或并发地执行。
*
* 此方法检查该命令是否为有效的操作系统命令。
* 哪些命令有效取决于系统,但至少命令必须是由非空字符串组成的非空列表。
* 在执行该方法的过程中,可能有以下情况导致出现错误
* 1)找不到操作系统程序文件
* 2)程序文件的访问被拒绝了
* 3)工作目录不存在
* */
Process process = processBuilder.start() ;
/*
* 返回连接到子进程的正常输出的输入流。流通过管道从这个process对象表示的流程的标准输出中获取数据。
* 如果子进程的标准输出已使用ProcessBuilder重定向。redirectOutput则此方法将返回一个空输入流。
* 否则,如果使用ProcessBuilder重定向了子进程的标准错误(redirectErrorStream)则此方法返回的输入流将接收合并的标准输出和子进程的标准错误。
* */
InputStream inputStream = process.getInputStream() ;
/*
* 返回连接到子进程的正常输入的输出流。流的输出通过管道连接到由此process对象表示的流程的标准输入。
* 如果子进程的标准输入已使用ProcessBuilder重定向。redirectInput则此方法将返回空输出流。
* */
OutputStream outputStream = process.getOutputStream() ;
/*
* 返回连接到子进程的错误输出的输入流。流通过管道从process对象表示的流程的错误输出中获取数据。
* 如果子进程的标准错误已使用ProcessBuilder重定向(ProcessBuilder#redirectError(Redirect)、ProcessBuilder#redirectErrorStream(boolean))
* 那么这个方法将返回一个空输入流。
*/
InputStream errorStream = process.getErrorStream() ;
//测试由此进程表示的子进程是否处于活动状态。
boolean isAlive = process.isAlive() ;
/*
* 返回此Process对象表示的子进程的退出值。按照惯例,0表示正常终止。
* */
int exitCode = process.exitValue() ;
/*
* 如果需要,使当前线程等待,直到由此process对象表示的进程终止
* 返回此Process对象表示的子进程的退出值。按照惯例,0表示正常终止。
*/
exitCode = process.waitFor() ;
//使当前线程等待(如有必要),直到由此Process对象表示的子进程终止或指定的等待时间结束。
boolean isSuccess = process.waitFor(1, TimeUnit.MINUTES);
//杀死子进程。此Process对象表示的子进程是否被强制终止取决于实现。
process.destroy();
/*
* 杀死子进程。此Process对象表示的子进程被强制终止。
* 此方法的默认实现调用destroy(),因此可能不会强制终止进程。强烈建议该类的具体实现使用兼容的实现覆盖此方法。
* 在ProcessBuilder.start()和Runtime.exec(java.lang.String)返回的Process对象上调用此方法将强制终止进程。
* 注意:子进程可能不会立即终止。也就是说,isAlive()可能会在destroy强行()被调用后的一小段时间内返回true。如果需要,这个方法可以链接到waitFor()
* */
process.destroyForcibly() ;
}
3、样例代码
package org.example;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @Description ShellUtil
* @Date 2023/9/3 10:38
* @Version V1.0
**/
public class ShellUtil {
protected static final String SH_SHELL = "/bin/sh";
protected static final String CMD_SHELL = "cmd.exe";
private static final int EXIT_SUCCESS_CODE = 0 ;
public static class Result{
//命令执行是否成功
public boolean success ;
//执行的信息
private String message ;
public Result(boolean success){
this(success,null) ;
}
public Result(boolean success,String message){
this.success = success ;
this.message = message ;
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder() ;
sb.append("isSuccess:") ;
sb.append(success) ;
sb.append(System.lineSeparator()) ;
sb.append(System.lineSeparator()) ;
sb.append(message) ;
return sb.toString() ;
}
}
private static List<String> prefixCommands(){
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");
List<String> commands = new ArrayList<>() ;
if (isWindows) {
commands.add(CMD_SHELL) ;
commands.add("/c") ;
}else{
commands.add(SH_SHELL) ;
commands.add("-c") ;
}
return commands ;
}
/**
* @param commands 要执行的命令行
* @throws Exception
*/
public static Result exec(List<String> commands) throws Exception {
return exec(null,commands);
}
/**
* @param workingDirectory 命令执行时候的工种目录
* @param execCommands 要执行的命令行
* @throws Exception
*/
public static Result exec(File workingDirectory,List<String> execCommands) throws Exception {
List<String> commands = prefixCommands() ;
commands.addAll(execCommands) ;
ProcessBuilder processBuilder = new ProcessBuilder(commands) ;
if(workingDirectory != null){
processBuilder.directory(workingDirectory) ;
}
//将子进程标准I/O的源和目标设置为与当前Java进程的源和目标相同
//processBuilder.inheritIO() ;
//设置合并标准错误和标准输出
processBuilder.redirectErrorStream(true) ;
boolean isSuccess = false ;
StringBuilder msgBuilder = new StringBuilder() ;
Process process = null ;
try{
//启动新进程并执行命令
process = processBuilder.start() ;
//使当前线程等待,直到子进程终止或指定的等待时间结束
//返回值:如果子进程已退出,则为True;如果子进程退出之前超过了等待时间,则为false
process.waitFor(30, TimeUnit.MINUTES);
isSuccess = (process.exitValue() == EXIT_SUCCESS_CODE) ;
//返回连接到子进程的正常输出的输入流
try(InputStream inputStream = process.getInputStream()){
String message = processMessage(inputStream,StandardCharsets.UTF_8.name());
msgBuilder.append(message) ;
}
//返回连接到子进程的错误输出的输入流
try(InputStream errorStream = process.getErrorStream()){
String errorMsg = processMessage(errorStream,osEncode()) ;
msgBuilder.append(System.lineSeparator()) ;
msgBuilder.append(System.lineSeparator()) ;
msgBuilder.append(errorMsg) ;
}
}finally {
if(process != null){
//销毁进程
process.destroy();
}
}
return new Result(isSuccess,msgBuilder.toString()) ;
}
private static String processMessage(InputStream inputStream,String charsetName) throws Exception {
if(charsetName == null){
//JVM默认的字符编码
charsetName = Charset.defaultCharset().name() ;
}
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, charsetName) ;
StringBuilder sb = new StringBuilder() ;
BufferedReader bufferedReader = new BufferedReader(inputStreamReader) ;
String line = null ;
//重要:这里将一直阻塞,直到输入数据可用、检测到流的结尾或抛出异常
//也就是说,如果inputStream中没有可用的数据(例如命令执行时的错误信息)则程序就一直阻塞了
while((line = bufferedReader.readLine()) != null){
sb.append(line) ;
sb.append(System.lineSeparator()) ;
}
return sb.toString() ;
}
/**
* 操作系统编码
*/
private static String osEncode(){
String osEncode = (String)System.getProperties().get("sun.jnu.encoding");
return osEncode ;
}
/**
* 文件系统编码
*/
private static String fileEncode(){
String fileEncode=(String)System.getProperties().get("file.encoding");
System.out.println(fileEncode);
return fileEncode ;
}
public static void main(String[] args) throws Exception {
/*
* 方式1:将命令行按空格切分后设置成每个字符串
* 在Windows下可以正常执行,linux下不能正常执行
* 注意:以下写法不推荐,与Linux不兼容
*/
List<String> commandList1 = new ArrayList<>() ;
commandList1.add("mvn" ) ;
commandList1.add("clean" ) ;
commandList1.add("compile" ) ;
/*
* 会被ProcessBuilder处理成:cmd.exe /c mvn clean compile
* 在Windows下可以正常执行,linux下不能正常执行
*/
Result result1 = ShellUtil.exec(commandList1);
System.out.println(result1);
/*
* 方式2:将整个命令生成一个字符串【推荐】
*/
List<String> commandList2 = new ArrayList<>() ;
commandList2.add("mvn clean compile" ) ;
/*
* 会被ProcessBuilder处理成:cmd.exe /c "mvn clean compile"
* 可以正常执行
*/
Result result2 = ShellUtil.exec(commandList2);
System.out.println(result2);
/*
* 方式3:执行带空格路径的指令
*/
String mvnPath3 = "F:/Program Files/apache-maven-3.9.1/bin/mvn" ;
List<String> commandList3 = new ArrayList<>() ;
commandList3.add(mvnPath3) ;
commandList3.add("compile") ;
/*
* 会被处理后变成:cmd.exe /c "F:/Program Files/apache-maven-3.9.1/bin/mvn" compile
* 在Windows下可以正常执行,linux下不能正常执行
*/
Result result3 = ShellUtil.exec(commandList3);
System.out.println(result3);
/*
* 方式4:执行带空格路径的指令【推荐】
*/
List<String> commandList4 = new ArrayList<>() ;
String mvnPath4 = "\"\"F:/Program Files/apache-maven-3.9.1/bin/mvn\" compile\"" ;
commandList4.add(mvnPath4) ;
/*
* 会被ProcessBuilder处理成:cmd.exe /c ""F:/Program Files/apache-maven-3.9.1/bin/mvn" compile"
* 可以正常被执行
*/
Result result4 = ShellUtil.exec(commandList4);
System.out.println(result4);
}
}