Procesos externos con ProcessBuilder
Una aplicación Java puede iniciar otro proceso y observar su salida, errores y código de terminación. ProcessBuilder no interpreta una línea de shell: recibe el ejecutable y cada argumento por separado.
Un proceso Java portable
Para evitar depender de bash, cmd.exe, ls o PowerShell, usaremos como proceso hijo una clase Java controlada:
public class ProcesoHijo {
public static void main(String[] args) {
String nombre = args.length == 0 ? "mundo" : args[0];
System.out.println("Hola, " + nombre);
System.err.println("Diagnóstico del hijo");
}
}
La ruta del ejecutable se obtiene de la JVM actual:
String java = Path.of(
System.getProperty("java.home"), "bin", "java").toString();
ProcessBuilder builder = new ProcessBuilder(
java,
"-cp",
System.getProperty("java.class.path"),
"ProcesoHijo",
"Ada Lovelace"
);
"Ada Lovelace" es un único argumento porque ocupa un elemento. No construyas "java ProcesoHijo Ada Lovelace" esperando que ProcessBuilder lo divida como una shell.
Directorio y entorno
builder.directory(Path.of(".").toFile());
builder.environment().put("MODO_HIJO", "prueba");
directory(...) fija el directorio de trabajo del hijo. El mapa de environment() modifica el entorno que heredará el proceso hijo; no cambia el entorno del proceso Java padre. No registres secretos del entorno.
Consumir stdout y stderr
Los procesos escriben en pipes con buffers limitados. Si el padre espera sin consumir una salida abundante, el hijo puede bloquearse. Para un ejemplo sencillo podemos combinar ambas salidas y consumirlas mientras el proceso vive:
builder.redirectErrorStream(true);
Process process = builder.start();
try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
reader.lines().forEach(line -> System.out.println("HIJO> " + line));
}
int exitCode = process.waitFor();
System.out.println("Código: " + exitCode);
Con redirectErrorStream(true), stderr se incorpora a stdout. Si necesitas conservarlos separados, debes consumir ambos correctamente, por ejemplo con tareas distintas o redirigiéndolos a archivos. Leer enormes salidas solo después de waitFor() no es un patrón universalmente seguro.
También existen redirecciones explícitas:
builder.redirectOutput(Path.of("salida.log").toFile());
builder.redirectError(Path.of("error.log").toFile());
Código de salida
waitFor() espera la terminación y devuelve el código de salida. Por convenio, cero suele indicar éxito, pero el significado lo define cada programa. No llames a exitValue() antes de saber que terminó: puede lanzar IllegalThreadStateException.
Timeout y política de terminación
Process process = builder.start();
boolean termino = process.waitFor(2, TimeUnit.SECONDS);
if (!termino) {
process.destroy();
if (!process.waitFor(500, TimeUnit.MILLISECONDS)) {
process.destroyForcibly();
process.waitFor();
}
}
Si waitFor(timeout, ...) devuelve false, el proceso sigue vivo. destroy() solicita una terminación normal. destroyForcibly() es el último recurso si continúa vivo y la política lo requiere; incluso entonces esperamos a que finalice.
Portabilidad
Un ejecutable externo debe existir en la máquina destino y puede comportarse de forma distinta según plataforma. El ejemplo principal reutiliza el ejecutable Java actual. Si utilizas ls, grep, cmd.exe o PowerShell, documenta expresamente esa dependencia; no ocultes una shell detrás de bash -c o cmd /c.
Experimento observable
Compila ProcesoHijo y un lanzador. Antes de ejecutar, predice:
- qué argumentos recibirá el hijo;
- qué stream leerá el padre tras combinar errores;
- cuándo estará disponible el código de salida.
Repite y separa evidencia de garantía. Está garantizado que cada argumento se conserva como elemento y que waitFor() retorna tras terminar. No está garantizado cuánto tardará ni cómo se intercalarían stdout y stderr si se consumieran por separado.
Práctica
Corrige esta construcción:
new ProcessBuilder("java ProcesoHijo argumento 1");
Solución
new ProcessBuilder(
java,
"-cp",
System.getProperty("java.class.path"),
"ProcesoHijo",
"argumento 1"
);
Errores habituales
- Pasar toda la orden como una cadena.
- Depender de una shell sin documentarlo.
- Esperar antes de consumir una salida potencialmente grande.
- Consultar
exitValue()antes de terminar. - Creer que un timeout mata el proceso.
- Usar
destroyForcibly()como primera opción. - Imprimir secretos del entorno.
En JV5-07 integraremos estos criterios con tareas, races, interrupción y cierre.