☰
Java 中调用系统命令 外部可执行程序(含真实案例)
2026/10/8 18:46:58 网站建设 项目流程

概述

Java 提供两种方式调用外部系统命令或可执行程序:Runtime.getRuntime().exec()和ProcessBuilder。Runtime.exec是早期API,写法简单但坑较多;ProcessBuilder是JDK1.5引入,生产环境推荐优先使用,支持自定义工作目录、环境变量,参数传递更安全。

绝大多数线上出现的服务假死、线程阻塞、定时任务堆积问题,根源都是对子进程输出缓冲区机制不了解。

底层原理

当Java通过ProcessBuilder/Runtime.exec创建子进程时,操作系统会为子进程分配两块独立缓冲区:标准输出stdout、标准错误stderr。

  • 子进程打印日志,会写入对应缓冲区;
  • 如果Java主线程不去消费缓冲区的数据,缓冲区写满后,子进程会被操作系统阻塞,无法继续执行、无法退出;waitFor()永久阻塞,线程被挂死。

真实线上卡死案例

案例背景

后端文件转码服务,使用FFmpeg做视频抽帧,本地单元测试全部正常,上线后随机出现任务卡死。现象:大部分任务正常,少量任务永久不结束;线程池工作线程被占满,新任务排队超时,最终服务接口大面积超时,只能重启服务临时恢复。

事故原始错误代码
publicclassFFmpegDemo{publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{StringffmpegPath="/usr/bin/ffmpeg";ProcessBuilderpb=newProcessBuilder(ffmpegPath,"-i","input.mp4","frame.jpg");Processprocess=pb.start();// 只等待进程结束,完全不读取stdout和stderr输出process.waitFor();System.out.println("任务完成");}}
根因分析

FFmpeg在处理异常视频文件时,会持续输出大量错误日志,快速填满stderr缓冲区。代码没有读取流,缓冲区满,FFmpeg进程被操作系统阻塞停住;waitFor()一直等待子进程退出,线程永久占用,反复出现后线程池耗尽。

同类高发场景:调用git、mvn、解压工具、PDF转换工具,这类程序容易产生大量日志,极易触发缓冲区满阻塞。

修复方案

使用redirectErrorStream(true)将错误流合并到标准输出流,统一读取输出,清空缓冲区。

publicclassFFmpegDemo{publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{StringffmpegPath="/usr/bin/ffmpeg";ProcessBuilderpb=newProcessBuilder(ffmpegPath,"-i","input.mp4","frame.jpg");// 合并错误流到标准输出流,避免单独stderr缓冲区堆积pb.redirectErrorStream(true);Processprocess=pb.start();// 读取输出,清空缓冲区try(BufferedReaderbr=newBufferedReader(newInputStreamReader(process.getInputStream()))){Stringline;while((line=br.readLine())!=null){System.out.println(line);}}process.waitFor();System.out.println("任务完成");}}

ProcessBuilder 通用生产可用工具方法

增加超时控制,防止极端情况下线程永久阻塞,捕获异常并强制销毁进程。

importjava.io.BufferedReader;importjava.io.InputStreamReader;importjava.nio.charset.StandardCharsets;importjava.util.concurrent.TimeUnit;publicclassCmdExecutor{/** * 执行外部命令/可执行程序 * @param cmd 命令数组 * @return 命令输出结果 * @throws Exception 超时、执行异常 */publicstaticStringexecute(String...cmd)throwsException{ProcessBuilderpb=newProcessBuilder(cmd);pb.redirectErrorStream(true);Processprocess=pb.start();StringBuilderoutput=newStringBuilder();try(BufferedReaderbr=newBufferedReader(newInputStreamReader(process.getInputStream(),StandardCharsets.UTF_8))){Stringline;while((line=br.readLine())!=null){output.append(line).append("\n");}}// 超时控制,30秒未完成则强制终止进程booleanfinished=process.waitFor(30,TimeUnit.SECONDS);if(!finished){process.destroyForcibly();thrownewException("命令执行超时,强制销毁子进程");}intexitCode=process.exitValue();if(exitCode!=0){thrownewException("命令执行失败,退出码:"+exitCode+",输出:"+output);}returnoutput.toString();}publicstaticvoidmain(String[]args)throwsException{// Linux示例Stringres1=execute("df","-h");System.out.println(res1);// Windows内置cmd命令,需要cmd /cStringres2=execute("cmd","/c","ipconfig");System.out.println(res2);// 调用外部exe程序// execute("D:/tools/convert.exe", "source.pdf", "target.png");}}

Runtime.exec 简单示例(不推荐新项目使用)

importjava.io.BufferedReader;importjava.io.InputStreamReader;publicclassRuntimeCmdDemo{publicstaticvoidmain(String[]args)throwsException{Processprocess=Runtime.getRuntime().exec("cmd /c ipconfig");BufferedReaderbr=newBufferedReader(newInputStreamReader(process.getInputStream(),"GBK"));Stringline;while((line=br.readLine())!=null){System.out.println(line);}process.waitFor();}}

Runtime.exec缺点:字符串命令拆分容易出现意外、参数拼接易引发命令注入,不推荐新项目。

关键避坑要点

  1. 缓冲区阻塞:stdout、stderr两个缓冲区都要处理;要么合并流读取,要么开启两个独立线程分别读取两个流。只要有一个缓冲区不消费,就有卡死风险。
  2. 内置系统命令:Windows的dir、echo等属于cmd内置命令,必须使用cmd /c;Linux shell内置命令需要sh -c。
  3. 命令注入风险:禁止直接拼接用户输入字符串构造命令,使用ProcessBuilder可变参数数组传参,底层自动转义参数。
  4. 超时控制必不可少:外部程序可能死循环、挂起,waitFor()一定要设置超时时间,超时调用destroyForcibly()强制销毁子进程,避免线程泄漏。
  5. 编码问题:Windows控制台默认GBK编码,Linux默认UTF-8,读取输入流时手动指定字符集,防止输出乱码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询