启动进程的方式说明
- 通过
new ProcessBuilder(String ...commands).start()
启动进程-
ProcessBuilder
支持链式编程来配置子进程的相关设置- redirectXXX:重定向子进程的流(标准输入,标准输出,错误信息)
- environment() 获取环境设置,可修改
- 注意:commands 不是单纯的将命令行参数以空格分隔得到。如果 commands 中单个值过长,可能会启动失败。Runtime 中是用的
StringTokenizer
解析分割参数为一个 commands 数组。
-
- 通过 Runtime 封装的方法启动进程比如:
- Runtime.getRuntime().exec(xxx)
- 如果是启动 java 程序。并且不是其他 jar 包,可以如下拼接命令行:
String javaHome = System.getProperty("java.home");
String java = javaHome + File.separator + "bin" + File.separator + "java";
String sysCp = System.getProperty("java.class.path");
String currPath = ClassLoader.getSystemResource("").getPath();
String cp = "\\"" + sysCp + File.pathSeparator + currPath + "\\"";
String encoding = " -Dfile.encoding=" + Charset.defaultCharset().name();
String cmd = java + encoding + " -cp " + cp + ChildProcess.class;
- Runtime 解析命令行为 commands 数组的方法:
public static String[] resolveCommand(String command) {
if (command.length() == 0) {
throw new IllegalArgumentException("Empty command");
}
StringTokenizer st = new StringTokenizer(command);
String[] cmdArray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++) {
cmdArray[i] = st.nextToken();
}
return cmdArray;
}
案例:
- 父进程
import cn.hutool.core.thread.ThreadUtil;
import java.io.*;
import java.nio.charset.Charset;
import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;
public class FatherProcess {
public static void main(String[] args) throws IOException {
String javaHome = System.getProperty("java.home");
String java = javaHome + File.separator + "bin" + File.separator + "java";
String sysCp = System.getProperty("java.class.path");
String currPath = ClassLoader.getSystemResource("").getPath();
String cp = "\\"" + sysCp + File.pathSeparator + currPath + "\\"";
// 父进程和子进程如果编码不一致,会出现中文乱码。可以包装流,使得双方编码一致
// 或者父进程启动子进程的时候,设置 " -Dfile.encoding=" + Charset.defaultCharset().name();
String encoding = " -Dfile.encoding=" + Charset.defaultCharset().name();
String cmd = java + encoding + " -cp " + cp + ChildProcess.class;
// Process p = Runtime.getRuntime().exec(cmd);
// 失败,需要用 StringTokenizer 解析命令行为数组,可能单个字符串是太长了
// Process p = new ProcessBuilder(java, "-classpath", cp, ChildProcess.class.toString()).start();
// 可以通过 ProcessBuilder 重定向子进程的流到文件,此时父进程将无法通过 p.getInputStream() 获取子进程输出
ProcessBuilder processBuilder = new ProcessBuilder(resolveCommand(cmd));
processBuilder.redirectOutput(new File("target/output.txt"));
processBuilder.redirectError(new File("target/error.txt"));
Process p = processBuilder.start();
System.out.println("FatherProcess's default charset is: " + Charset.defaultCharset().name());
// 父进程通过 IO 流将信息写入子进程的输入流
System.out.println("【父进程发送两条数据】:" + 2);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()))) {
// 子进程用的时 readLine,因此需要换行符
writer.write("消灭人类暴政\\n");
writer.flush();
ThreadUtil.sleep(1, TimeUnit.SECONDS);
writer.write("世界属于三体\\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public static String[] resolveCommand(String command) {
if (command.length() == 0) {
throw new IllegalArgumentException("Empty command");
}
StringTokenizer st = new StringTokenizer(command);
String[] cmdArray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++) {
cmdArray[i] = st.nextToken();
}
return cmdArray;
}
}
- 子进程
import cn.hutool.core.thread.ThreadUtil;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ChildProcess {
public static void main(String[] args) throws IOException {
System.out.println("ChildProcess's default charset is: " + Charset.defaultCharset().name());
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
String line;
List all = new ArrayList<>();
// readLine 以换行符作为一行结束
while ((line = reader.readLine()) != null) {
all.add(line);
}
// println 带有换行符
System.out.println("【子进程收到的消息数量】:" + all.size());
ThreadUtil.sleep(200, TimeUnit.MILLISECONDS);
System.out.println("给岁月以文明");
ThreadUtil.sleep(200, TimeUnit.MILLISECONDS);
System.out.println("而不是给文明以岁月");
System.out.println(all);
}
}
}
通过IO流进行通信——DataOutputStream 和 DataInputStream
也可以使用 BufferedWriter、BufferedReader。如果使用 readLine 方法,要注意 BufferedWriter 必须写入换行符(或关闭流)后,BufferedReader.readLine() 才能读取到内容。
基于默认IO流的方式:
- 父进程使用 process.getOutputStream 向子进程写入数据,子进程通过 System.in 读取数据
- 父进程使用 process.getInputStream 读取子进程输出的数据。
案例
- 父进程
import cn.hutool.core.util.StrUtil;
import java.io.*;
import java.util.StringTokenizer;
/**
* @author zhy
*/
public class FatherProcessWithDOS {
private final Process process;
private final DataOutputStream dos;
public FatherProcessWithDOS() throws IOException {
String javaHome = System.getProperty("java.home");
String java = javaHome + File.separator + "bin" + File.separator + "java";
String sysCp = System.getProperty("java.class.path");
String currPath = ClassLoader.getSystemResource("").getPath();
String cp = "\\"" + sysCp + File.pathSeparator + currPath + "\\"";
String cmd = java + " -cp " + cp + ChildProcessWithDIS.class;
process = new ProcessBuilder(resolveCommand(cmd)).start();
// region 监听子进程的错误输出
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// endregion
// region 监听子进程的标准输出
new Thread(() -> {
try (DataInputStream dis = new DataInputStream(process.getInputStream())) {
String line;
while (!StrUtil.isEmpty((line = dis.readUTF()))) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// endregion
dos = new DataOutputStream(process.getOutputStream());
}
public void sendToChild(String message) {
try {
dos.writeUTF(message);
// dos.writeBytes(message);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean isChildAlive() {
return process.isAlive();
}
public void destroyChild() {
process.destroy();
}
private String[] resolveCommand(String command) {
if (command.length() == 0) {
throw new IllegalArgumentException("Empty command");
}
StringTokenizer st = new StringTokenizer(command);
String[] cmdArray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++) {
cmdArray[i] = st.nextToken();
}
return cmdArray;
}
public static void main(String[] args) throws IOException, InterruptedException {
FatherProcessWithDOS father = new FatherProcessWithDOS();
String message = "消灭人类暴政";
System.out.println("父进程发送数据:" + message);
father.sendToChild(message);
Thread.sleep(100);
message = "世界属于三体";
System.out.println("父进程发送数据:" + message);
father.sendToChild(message);
Thread.sleep(100);
message = "man remember love because romantic only.";
System.out.println("父进程发送数据:" + message);
father.sendToChild(message);
Thread.sleep(100);
message = "exit";
System.out.println("父进程结束命令:" + message);
father.sendToChild(message);
Thread.sleep(100);
System.out.println("子进程是否存活:" + father.isChildAlive());
System.exit(0);
}
}
- 子进程
import cn.hutool.core.util.StrUtil;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author zhy
*/
public class ChildProcessWithDIS {
public static void main(String[] args) {
read(System.in);
}
private static void read(InputStream inputStream) {
DataOutputStream out = new DataOutputStream(System.out);
try (DataInputStream reader = new DataInputStream(inputStream)) {
String line;
while (!StrUtil.isEmpty((line = reader.readUTF()))) {
if ("exit".equalsIgnoreCase(line)) {
// 必须写一个空字符串,不然抛出异常 java.io.EOFException
out.writeUTF("");
return;
}
String dateStr = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
String msg = MessageFormat.format("[{0}][receive a line message]:{1}", dateStr, line);
// System.out.println(msg);
out.writeUTF(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
通过 IO 流 + Socket 进行通信(需要端口,大可不必使用)
- 父进程
import cn.hutool.core.net.NetUtil;
import java.io.*;
import java.net.Socket;
import java.util.StringTokenizer;
/**
* @author zhy
*/
public class FatherProcessWithSocket {
private final Process process;
private final int port;
public FatherProcessWithSocket() throws IOException {
String javaHome = System.getProperty("java.home");
String java = javaHome + File.separator + "bin" + File.separator + "java";
String sysCp = System.getProperty("java.class.path");
String currPath = ClassLoader.getSystemResource("").getPath();
String cp = "\\"" + sysCp + File.pathSeparator + currPath + "\\"";
port = NetUtil.getUsableLocalPort();
String cmd = java + " -cp " + cp + ChildProcessWithSocket.class + " " + port;
process = new ProcessBuilder(resolveCommand(cmd)).start();
// region 监听子进程的错误输出
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// endregion
// region 监听子进程的标准输出
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// endregion
}
public void sendToChild(String message) {
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new Socket("localhost", port).getOutputStream()))) {
writer.write(message);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean isChildAlive() {
return process.isAlive();
}
public void destroyChild() {
process.destroy();
}
private String[] resolveCommand(String command) {
if (command.length() == 0) {
throw new IllegalArgumentException("Empty command");
}
StringTokenizer st = new StringTokenizer(command);
String[] cmdArray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++) {
cmdArray[i] = st.nextToken();
}
return cmdArray;
}
public static void main(String[] args) throws IOException, InterruptedException {
FatherProcessWithSocket father = new FatherProcessWithSocket();
String message = "消灭人类暴政";
System.out.println("父进程发送数据:" + message);
father.sendToChild(message);
Thread.sleep(100);
message = "世界属于三体";
System.out.println("父进程发送数据:" + message);
father.sendToChild(message);
Thread.sleep(100);
message = "exit";
System.out.println("父进程结束命令:" + message);
father.sendToChild(message);
Thread.sleep(100);
System.out.println("子进程是否存活:" + father.isChildAlive());
}
}
- 子进程
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author zhy
*/
public class ChildProcessWithSocket {
public static void main(String[] args) {
if (args.length > 0) {
int port = Integer.parseInt(args[0]);
new Thread(() -> startSocket(port)).start();
}
// read(System.in);
}
private static void startSocket(int port) {
try {
ServerSocket serverSocket = new ServerSocket(port);
while (true) {
Socket accept = serverSocket.accept();
InputStream inputStream = accept.getInputStream();
if (read(inputStream)) {
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static boolean read(InputStream inputStream) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
if ("exit".equalsIgnoreCase(line)) {
return true;
}
String dateStr = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
String msg = MessageFormat.format("[{0}][receive a line message]:{1}", dateStr, line);
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
小结
Java 启动子进程的方式
- 通过
new ProcessBuilder(String ...commands).start()
启动进程-
ProcessBuilder
支持链式编程来配置子进程的相关设置- redirectXXX:重定向子进程的流(标准输入,标准输出,错误信息)
- environment() 获取环境设置,可修改
- 注意:commands 不是单纯的将命令行参数以空格分隔得到。如果 commands 中单个值过长,可能会启动失败。Runtime 中是用的
StringTokenizer
解析分割参数为一个 commands 数组。
-
- 通过 Runtime 封装的方法启动进程比如:
- Runtime.getRuntime().exec(xxx)
- 如果是启动 java 程序。并且不是其他 jar 包,可以如下拼接命令行:
String javaHome = System.getProperty("java.home");
String java = javaHome + File.separator + "bin" + File.separator + "java";
String sysCp = System.getProperty("java.class.path");
String currPath = ClassLoader.getSystemResource("").getPath();
String cp = "\\"" + sysCp + File.pathSeparator + currPath + "\\"";
String encoding = " -Dfile.encoding=" + Charset.defaultCharset().name();
String cmd = java + encoding + " -cp " + cp + ChildProcess.class;
- Runtime 解析命令行为 commands 数组的方法:
public static String[] resolveCommand(String command) {
if (command.length() == 0) {
throw new IllegalArgumentException("Empty command");
}
StringTokenizer st = new StringTokenizer(command);
String[] cmdArray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++) {
cmdArray[i] = st.nextToken();
}
return cmdArray;
}
进程 API
- isAlive():判断是否存活
- destroy():结束进程
通信
基于默认IO流的方式:
- 父进程使用 process.getOutputStream 向子进程写入数据,子进程通过 System.in 读取数据
- 父进程使用 process.getInputStream 读取子进程输出的数据。
其他方式:Socket
处理父子进程之间通信出现的中文乱码问题
唯一要点:保证发送方和读取方使用的同一编码。
-
InputStreamReader
与OutputStreamWriter
默认使用jvm默认编码读取数据:Charset.defaultCharset().name();- 如果父进程和子进程默认编码不一致,就需要手动指定编码
- 也可以父进程通过 “java” + “-Dfile.encoding=” + Charset.defaultCharset().name() 保证父子进程编码一致。
- 也可以使用
DataOutputStream
与DataInputStream
这种处理了编码的特殊流。- 使用这对流时,建议使用
writeUTF
和readUTF
方法,并且关闭流前写一个空字符串。
- 使用这对流时,建议使用