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;
}

案例:

  • 父进程
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

处理父子进程之间通信出现的中文乱码问题

唯一要点:保证发送方和读取方使用的同一编码。

  • InputStreamReaderOutputStreamWriter 默认使用jvm默认编码读取数据:Charset.defaultCharset().name();
    • 如果父进程和子进程默认编码不一致,就需要手动指定编码
    • 也可以父进程通过 “java” + “-Dfile.encoding=” + Charset.defaultCharset().name() 保证父子进程编码一致。
  • 也可以使用 DataOutputStreamDataInputStream 这种处理了编码的特殊流。
    • 使用这对流时,建议使用 writeUTFreadUTF 方法,并且关闭流前写一个空字符串。

你可能感兴趣的:(Java 子进程)