子进程用System.out.println致父进程反序列化抛StreamCorruptedException求助
你猜的完全没错——问题核心就是子进程的标准输出被普通文本和序列化二进制数据混合写入了。ObjectInputStream需要严格遵循序列化流的格式开头(固定魔数0xACED+版本号0x0005),而System.out.println()输出的ASCII文本直接破坏了这个开头,导致父进程读取时抛出StreamCorruptedException。
下面给你几种从简单到进阶的修复方案:
方案1:分离普通日志与序列化输出(最简单)
把子进程的普通打印输出转到标准错误流(System.err),让标准输出(System.out)专门用来传输序列化数据。这样父进程读取process.getInputStream()时只会拿到纯净的序列化流,子进程的日志还能通过process.getErrorStream()单独查看。
修改后的Child.java:
public class Child { public static void main(String[] args) throws IOException { DTO dto = new DTO(); // 把普通日志输出到标准错误流 System.err.println("printing random text here"); ObjectOutputStream stream = new ObjectOutputStream(System.out); stream.writeObject(dto); stream.flush(); stream.close(); } }
如果父进程需要查看子进程的日志,可以添加一段异步读取错误流的代码(可选):
// 在Parent.start()方法中添加 new Thread(() -> { try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { String line; while ((line = errReader.readLine()) != null) { System.out.println("Child log: " + line); } } catch (IOException e) { e.printStackTrace(); } }).start();
方案2:标记序列化数据的边界(适合必须共用标准输出的场景)
如果必须让普通文本和序列化数据共用标准输出,可以先在子进程输出序列化数据的字节长度,父进程先读取这个长度,再精确读取对应字节数的序列化数据,剩下的字节就是普通文本。
修改后的Child.java:
public class Child { public static void main(String[] args) throws IOException { DTO dto = new DTO(); System.out.println("printing random text here"); // 先把序列化数据写入字节数组,获取长度 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(dto); oos.flush(); byte[] serializedData = baos.toByteArray(); // 先输出序列化数据的长度(用换行分隔,方便父进程识别) System.out.println(serializedData.length); // 再输出序列化二进制数据 System.out.write(serializedData); System.out.flush(); } }
对应的父进程Parent.java修改:
public void start() throws IOException, InterruptedException, ClassNotFoundException { String classpath = System.getProperty("java.class.path"); String className = Child.class.getCanonicalName(); ProcessBuilder builder = new ProcessBuilder( "java", "-cp", classpath, className); Process process = builder.start(); // 先读取普通文本输出,直到找到序列化数据长度 BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { try { int dataLength = Integer.parseInt(line); // 切换为字节流读取序列化数据,避免BufferedReader缓冲区丢失数据 InputStream rawInputStream = process.getInputStream(); byte[] serializedData = new byte[dataLength]; int totalRead = 0; while (totalRead < dataLength) { int read = rawInputStream.read(serializedData, totalRead, dataLength - totalRead); if (read == -1) break; totalRead += read; } // 反序列化对象 ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedData)); DTO dto = (DTO) ois.readObject(); System.out.println("Received DTO: " + dto.getName()); break; } catch (NumberFormatException e) { // 不是长度,就是普通日志 System.out.println("Child log: " + line); } } }
方案3:使用Socket进行进程间通信(最可靠,适合复杂场景)
如果你的进程间通信需求更复杂(比如双向通信、大传输量),直接用Socket套接字传输序列化数据是最可靠的方案,完全和标准输出/错误流隔离。
子进程Child.java修改:
public class Child { public static void main(String[] args) throws IOException { DTO dto = new DTO(); System.out.println("printing random text here"); // 启动Socket服务(端口可以通过命令行参数传递,避免硬编码冲突) int port = 8888; ServerSocket serverSocket = new ServerSocket(port); Socket socket = serverSocket.accept(); // 通过Socket流序列化对象 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(dto); oos.flush(); // 关闭资源 oos.close(); socket.close(); serverSocket.close(); } }
父进程Parent.java修改:
public void start() throws IOException, InterruptedException, ClassNotFoundException { String classpath = System.getProperty("java.class.path"); String className = Child.class.getCanonicalName(); ProcessBuilder builder = new ProcessBuilder( "java", "-cp", classpath, className); Process process = builder.start(); // 等待子进程启动Socket服务(可以用更可靠的同步机制替代sleep) Thread.sleep(1000); // 连接子进程的Socket Socket socket = new Socket("localhost", 8888); ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); DTO dto = (DTO) ois.readObject(); System.out.println("Received DTO: " + dto.getName()); // 关闭资源 ois.close(); socket.close(); }
小提示:Socket方案中可以把端口作为命令行参数传递给子进程,避免硬编码端口冲突;也可以让子进程启动后向父进程发送就绪信号,代替
Thread.sleep()实现更可靠的同步。
内容的提问来源于stack exchange,提问作者Sameera Manorathna

