はじめに
最近Java言語をちょびっとだけ触っている新卒エンジニアが、ProcessBuilderクラスがどのようにしてプロセスを立ち上げる仕組みについて深さ優先探索で調査してみます。
(前提)ProcessBuilderとは
ProcessBuilderとは、JavaプログラムからOSの外部プロセス(例: シェルスクリプトやPythonなどのJava以外の言語で書かれたプログラムなど)を起動するためのクラスです。
個人的には、「Java言語でJava言語以外のプログラムも実行できるようにするためのモノ」と認識しています。(正確な表現ではないかもしれません。その際はご指摘をよろしくお願いします。)
なお、本記事では、2025年12月現在JavaのLTSであるJava25を対象とします。
本記事にて対象とするサンプルコード
簡単なコマンドを実行するサンプルコードを例に、どのようにしてコマンドを実行しているのかをソースコードベースで見ていきたいと思います。
(Windowsにてメモ帳を立ち上げるだけのコードです。)
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.io.IOException;
public class ProcessBuilderSample {
public static void main(String[] args) {
try {
// コマンドを文字列のリストで指定
List<String> command = new ArrayList<>();
command.add("notepad.exe");
// ProcessBuilderインスタンスを作成
ProcessBuilder pb = new ProcessBuilder(command);
// プロセスを開始(←今回は調査してみるのはここの部分)
Process process = pb.start();
// プロセスの出力を読み取る
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
// 出力を1行ずつ読み込み、コンソールに出力
String line;
System.out.println("--- コマンド実行結果 (ls -l) ---");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
System.out.println("-----------------------------------");
// プロセスの終了を待機し、終了コードを取得
int exitCode = process.waitFor();
System.out.println("\nプロセスが終了しました。終了コード: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
(本題)ソースコードを辿ってみる
それでは、ProcessBuilderはどのようにして、新たなプロセスを立ち上げているのかについて、ProcessBuilderクラスのコードを深掘ってみます。
↓ソースコードへのリンク↓
https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/share/classes/java/lang/ProcessBuilder.java
インスタンスの作成
以下のコードでは、引数に実行するコマンドをコンストラクタの引数として設定した上でインスタンスを作成します。
サンプルコード(抜粋)
// コマンドを文字列のリストで指定
List<String> command = new ArrayList<>();
command.add("notepad.exe");
// ProcessBuilderインスタンスを作成
ProcessBuilder pb = new ProcessBuilder(command);
実際に呼び出されるJavaコードは以下になります(実際のJavaコードへのリンク)。
public ProcessBuilder(String... command) {
this.command = new ArrayList<>(command.length);
for (String arg : command)
this.command.add(arg);
}
これ以上はコマンド実行と関係がないため、この部分の探索は終了します。
→プロセスの開始部分を見てみます。
プロセスの開始
// サンプルコード(抜粋)
// プロセスを開始
Process process = pb.start();
実際に呼び出されるJavaコードは以下になります(実際のJavaコードへのリンク)。
public Process start() throws IOException {
return start(redirects);
}
上記コードにて呼び出される別シグネチャのメソッド全体(オーバーロード)は以下になります(実際のJavaコードへのリンク)。
private Process start(Redirect[] redirects) throws IOException {
// Must convert to array first -- a malicious user-supplied
// list might try to circumvent the security check.
String[] cmdarray = command.toArray(new String[command.size()]);
cmdarray = cmdarray.clone();
for (String arg : cmdarray)
if (arg == null)
throw new NullPointerException();
// Throws IndexOutOfBoundsException if command is empty
String prog = cmdarray[0];
String dir = directory == null ? null : directory.toString();
for (String s : cmdarray) {
if (s.indexOf('\u0000') >= 0) {
throw new IOException("invalid null character in command");
}
}
try {
Process process = ProcessImpl.start(cmdarray,
environment,
dir,
redirects,
redirectErrorStream);
ProcessStartEvent event = new ProcessStartEvent();
if (event.isEnabled()) {
event.directory = dir;
event.command = String.join(" ", cmdarray);
event.pid = process.pid();
event.commit();
}
// Racy initialization for logging; errors in configuration may throw exceptions
System.Logger logger = LOGGER;
if (logger == null) {
LOGGER = logger = System.getLogger("java.lang.ProcessBuilder");
}
if (logger.isLoggable(System.Logger.Level.DEBUG)) {
boolean detail = logger.isLoggable(System.Logger.Level.TRACE);
var level = (detail) ? System.Logger.Level.TRACE : System.Logger.Level.DEBUG;
var cmdargs = (detail) ? String.join("\" \"", cmdarray) : cmdarray[0];
RuntimeException stackTraceEx = new RuntimeException("ProcessBuilder.start() debug");
LOGGER.log(level, "ProcessBuilder.start(): pid: " + process.pid() +
", dir: " + dir +
", cmd: \"" + cmdargs + "\"",
stackTraceEx);
}
return process;
} catch (IOException | IllegalArgumentException e) {
// It's much easier for us to create a high-quality error
// message than the low-level C code which found the problem.
throw new IOException(
"Cannot run program \"" + prog + "\""
+ (dir == null ? "" : " (in directory \"" + dir + "\")")
+ ": " + e.getMessage(),
e);
}
}
Redirectクラスの配列であるredirectsを引数に取っており、そのクラスもProcessBuilder.java内にstatic(静的)クラスとして定義されています。
今回の場合redirectsはnullなので、Redirectクラスの詳細は省略します。
(補足)Redirectクラスについて簡単に紹介
ざっくりと一言でまとめると、外部プロセスを起動する際に、標準入出力(stdin, stdout, stderr)をどのように扱うかを定義するクラスのことです。
参考:
ProcessBuilderクラスのstart()メソッドにて、ProcessImplクラスのstart()メソッドを呼び出している部分で実際に渡されたコマンドを実行する処理が走ってそうです(実際のJavaコードへのリンク)。
Process process = ProcessImpl.start(cmdarray,
environment,
dir,
redirects,
redirectErrorStream);
GitHub上でソースコードを検索してみると、unixとwindowsそれぞれに対してProcessImplクラスが存在していることがわかります。
GitHubにおける検索結果のURL
https://github.com/search?q=repo%3Aopenjdk%2Fjdk%20ProcessImpl&type=code
→本記事では普段業務で使用しているPCのOSであるWindows OSにおける具体的な処理を調査してみます。
Windows OSにおける処理
Windows OSにおけるProcessImplクラスのstart()メソッドの全体像は以下になります(実際のJavaコードへのリンク)。
// System-dependent portion of ProcessBuilder.start()
static Process start(String cmdarray[],
java.util.Map<String,String> environment,
String dir,
ProcessBuilder.Redirect[] redirects,
boolean redirectErrorStream)
throws IOException
{
String envblock = ProcessEnvironment.toEnvironmentBlock(environment);
FileInputStream f0 = null;
FileOutputStream f1 = null;
FileOutputStream f2 = null;
try {
boolean forceNullOutputStream = false;
long[] stdHandles;
if (redirects == null) {
stdHandles = new long[] { -1L, -1L, -1L };
} else {
stdHandles = new long[3];
if (redirects[0] == Redirect.PIPE) {
stdHandles[0] = -1L;
} else if (redirects[0] == Redirect.INHERIT) {
stdHandles[0] = fdAccess.getHandle(FileDescriptor.in);
} else if (redirects[0] instanceof ProcessBuilder.RedirectPipeImpl) {
stdHandles[0] = fdAccess.getHandle(((ProcessBuilder.RedirectPipeImpl) redirects[0]).getFd());
} else {
f0 = new FileInputStream(redirects[0].file());
stdHandles[0] = fdAccess.getHandle(f0.getFD());
}
if (redirects[1] == Redirect.PIPE) {
stdHandles[1] = -1L;
} else if (redirects[1] == Redirect.INHERIT) {
stdHandles[1] = fdAccess.getHandle(FileDescriptor.out);
} else if (redirects[1] instanceof ProcessBuilder.RedirectPipeImpl) {
stdHandles[1] = fdAccess.getHandle(((ProcessBuilder.RedirectPipeImpl) redirects[1]).getFd());
// Force getInputStream to return a null stream,
// the handle is directly assigned to the next process.
forceNullOutputStream = true;
} else {
f1 = newFileOutputStream(redirects[1].file(),
redirects[1].append());
stdHandles[1] = fdAccess.getHandle(f1.getFD());
}
if (redirects[2] == Redirect.PIPE) {
stdHandles[2] = -1L;
} else if (redirects[2] == Redirect.INHERIT) {
stdHandles[2] = fdAccess.getHandle(FileDescriptor.err);
} else if (redirects[2] instanceof ProcessBuilder.RedirectPipeImpl) {
stdHandles[2] = fdAccess.getHandle(((ProcessBuilder.RedirectPipeImpl) redirects[2]).getFd());
} else {
f2 = newFileOutputStream(redirects[2].file(),
redirects[2].append());
stdHandles[2] = fdAccess.getHandle(f2.getFD());
}
}
Process p = new ProcessImpl(cmdarray, envblock, dir,
stdHandles, forceNullOutputStream, redirectErrorStream);
if (redirects != null) {
// Copy the handles's if they are to be redirected to another process
if (stdHandles[0] >= 0
&& redirects[0] instanceof ProcessBuilder.RedirectPipeImpl) {
fdAccess.setHandle(((ProcessBuilder.RedirectPipeImpl) redirects[0]).getFd(),
stdHandles[0]);
}
if (stdHandles[1] >= 0
&& redirects[1] instanceof ProcessBuilder.RedirectPipeImpl) {
fdAccess.setHandle(((ProcessBuilder.RedirectPipeImpl) redirects[1]).getFd(),
stdHandles[1]);
}
if (stdHandles[2] >= 0
&& redirects[2] instanceof ProcessBuilder.RedirectPipeImpl) {
fdAccess.setHandle(((ProcessBuilder.RedirectPipeImpl) redirects[2]).getFd(),
stdHandles[2]);
}
}
return p;
} finally {
// In theory, close() can throw IOException
// (although it is rather unlikely to happen here)
try { if (f0 != null) f0.close(); }
finally {
try { if (f1 != null) f1.close(); }
finally { if (f2 != null) f2.close(); }
}
}
}
処理の流れ(Windows版)
-
環境変数を準備します(実際のJavaコードへのリンク)。
ProcessImpl.javaString envblock = ProcessEnvironment.toEnvironmentBlock(environment); -
I/Oリダイレクトの準備とハンドル(Handle)を取得します(実際のJavaコードへのリンク)。
ProcessImpl.javalong[] stdHandles; if (redirects == null) { stdHandles = new long[] { -1L, -1L, -1L }; } else { stdHandles = new long[3]; // 続く... -
標準入出力(stdin, stdout, stderr)に対応するOSハンドルを格納する配列を用意(
redirectsがnullでない場合)します。 -
プロセスの起動(
ProcessImplクラスのコンストラクタの呼び出し)→下にて詳述します(実際のJavaコードへのリンク)。ProcessImpl.javaProcess p = new ProcessImpl(cmdarray, envblock, dir, stdHandles, ...) -
finallyブロックで、リダイレクト処理のために一時的に作成されたJavaのファイルストリーム(f0, f1, f2)を閉じます(リソースのクリーンアップ)(実際のJavaコードへのリンク)。
ProcessImpl.java} finally { // In theory, close() can throw IOException // (although it is rather unlikely to happen here) try { if (f0 != null) f0.close(); } finally { try { if (f1 != null) f1.close(); } finally { if (f2 != null) f2.close(); } } }
WindowsにおけるProcessImplコンストラクタの呼び出し
Windows OSにおけるProcessImplクラスのコンストラクタの全体像は以下になります(実際のJavaコードへのリンク)。
private ProcessImpl(String cmd[],
final String envblock,
final String path,
final long[] stdHandles,
boolean forceNullOutputStream,
final boolean redirectErrorStream)
throws IOException
{
String cmdstr;
final String value = System.getProperty("jdk.lang.Process.allowAmbiguousCommands", "true");
final boolean allowAmbiguousCommands = !"false".equalsIgnoreCase(value);
if (allowAmbiguousCommands) {
// Legacy mode.
// Normalize path if possible.
String executablePath = new File(cmd[0]).getPath();
// No worry about internal, unpaired ["], and redirection/piping.
if (needsEscaping(VERIFICATION_LEGACY, executablePath) )
executablePath = quoteString(executablePath);
cmdstr = createCommandLine(
//legacy mode doesn't worry about extended verification
VERIFICATION_LEGACY,
executablePath,
cmd);
} else {
String executablePath;
try {
executablePath = getExecutablePath(cmd[0]);
} catch (IllegalArgumentException e) {
// Workaround for the calls like
// Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar")
// No chance to avoid CMD/BAT injection, except to do the work
// right from the beginning. Otherwise we have too many corner
// cases from
// Runtime.getRuntime().exec(String[] cmd [, ...])
// calls with internal ["] and escape sequences.
// Restore original command line.
StringBuilder join = new StringBuilder();
// terminal space in command line is ok
for (String s : cmd)
join.append(s).append(' ');
// Parse the command line again.
cmd = getTokensFromCommand(join.toString());
executablePath = getExecutablePath(cmd[0]);
}
// Quotation protects from interpretation of the [path] argument as
// start of longer path with spaces. Quotation has no influence to
// [.exe] extension heuristic.
boolean isShell = allowAmbiguousCommands ? isShellFile(executablePath)
: !isExe(executablePath);
cmdstr = createCommandLine(
// We need the extended verification procedures
isShell ? VERIFICATION_CMD_BAT
: (allowAmbiguousCommands ? VERIFICATION_WIN32 : VERIFICATION_WIN32_SAFE),
quoteString(executablePath),
cmd);
}
handle = create(cmdstr, envblock, path,
stdHandles, redirectErrorStream);
// Register a cleaning function to close the handle
final long local_handle = handle; // local to prevent capture of this
CleanerFactory.cleaner().register(this, () -> closeHandle(local_handle));
processHandle = ProcessHandleImpl.getInternal(getProcessId0(handle));
if (stdHandles[0] == -1L)
stdin_stream = ProcessBuilder.NullOutputStream.INSTANCE;
else {
FileDescriptor stdin_fd = new FileDescriptor();
fdAccess.setHandle(stdin_fd, stdHandles[0]);
fdAccess.registerCleanup(stdin_fd);
stdin_stream = new BufferedOutputStream(
new PipeOutputStream(stdin_fd));
}
if (stdHandles[1] == -1L || forceNullOutputStream)
stdout_stream = ProcessBuilder.NullInputStream.INSTANCE;
else {
FileDescriptor stdout_fd = new FileDescriptor();
fdAccess.setHandle(stdout_fd, stdHandles[1]);
fdAccess.registerCleanup(stdout_fd);
stdout_stream = new BufferedInputStream(
new PipeInputStream(stdout_fd));
}
if (stdHandles[2] == -1L)
stderr_stream = ProcessBuilder.NullInputStream.INSTANCE;
else {
FileDescriptor stderr_fd = new FileDescriptor();
fdAccess.setHandle(stderr_fd, stdHandles[2]);
fdAccess.registerCleanup(stderr_fd);
stderr_stream = new PipeInputStream(stderr_fd);
}
}
処理の流れ
-
コマンドラインの整形と検証をします。Legacy modeを使用するか否かの判定を行った上で、入力から得られるコマンドを一つの文字列として整形します(実際のJavaコードへのリンク)。
ProcessImpl.javatry { executablePath = getExecutablePath(cmd[0]); } catch (IllegalArgumentException e) { // Workaround for the calls like // Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar") // No chance to avoid CMD/BAT injection, except to do the work // right from the beginning. Otherwise we have too many corner // cases from // Runtime.getRuntime().exec(String[] cmd [, ...]) // calls with internal ["] and escape sequences. // Restore original command line. StringBuilder join = new StringBuilder(); // terminal space in command line is ok for (String s : cmd) join.append(s).append(' '); // Parse the command line again. cmd = getTokensFromCommand(join.toString()); executablePath = getExecutablePath(cmd[0]); } // Quotation protects from interpretation of the [path] argument as // start of longer path with spaces. Quotation has no influence to // [.exe] extension heuristic. boolean isShell = allowAmbiguousCommands ? isShellFile(executablePath) : !isExe(executablePath); cmdstr = createCommandLine( // We need the extended verification procedures isShell ? VERIFICATION_CMD_BAT : (allowAmbiguousCommands ? VERIFICATION_WIN32 : VERIFICATION_WIN32_SAFE), quoteString(executablePath), cmd); -
ネイティブコードを呼び出してプロセスを起動します(実際のJavaコードへのリンク)。
ProcessImpl.javahandle = create(cmdstr, envblock, path, stdHandles, redirectErrorStream);↓呼び出されるコードは以下になります(実際のJavaコードへのリンク)。
ProcessImpl.javaprivate static synchronized native long create(String cmdstr, String envblock, String dir, long[] stdHandles, boolean redirectErrorStream) throws IOException;native修飾子を確認できました!
ここでネイティブメソッド(Java以外の言語で実装する必要のあるメソッド)が定義されていることがわかります。
→このメソッドの具体的な実装部分で新しいプロセスを起動している、と考えられます!→今回の目的は果たせたため、一旦ここでコードリーディングは終了とします。
-
I/O ストリームをセットアップします。
今回はProcessBuilderコマンドが実際にコマンドを指定して実行している部分を探してみることが目的だったため、具体的なソースコードは割愛します。。
まとめ
- ProcessBuilderクラスは、Javaから別のプロセス作成に使用される
- ProcessBuilderクラスをインスタンス化する際に、実行するコマンドや環境変数を設定する
- プロセスの開始時に、
ProcessImplクラスのstart()メソッドを呼び出す- (
ProcessImplクラスはOSの種類毎に別々のJavaファイルが動く。どちらのOSでもProcessBuilderクラスが似たような使用感で使えるもはこのためであると考えられる)
- (
- Windows OSの場合、
start()メソッドでは、環境変数やリダイレクトの設定をした上で新たなProcessImplクラスのコンストラクタを呼び出し、そこでコマンドラインの整形やネイティブコードの呼び出しを行い、プロセスを起動する- 実際にプロセスを起動する際の挙動は
create()という名前のネイティブメソッドで定義されており、具体的な実装は別の言語(恐らくC or C++)
- 実際にプロセスを起動する際の挙動は
感想
普段呼び出して活用しているだけのクラスの中身を読んでみる機会があまりなかったため、個人的には面白かったです。
まだまだ自分の理解度が浅い部分はたくさんありそう(どうやってプログラムが動くか、など)なので、引き続き「原理・原則が何なのかを調べる姿勢」を念頭に置きながら技術に触れていけたら、と思います。
余裕があれば、UnixベースのOSにおけるProcessImplクラスのstart()メソッドについても調べてみたいです。
(ここまで読んでくださった方、本当にありがとうございます。)
