FFM menjembatani Java dengan kode native di dalam proses yang sama. Jalur interop kedua sama sekali berbeda: meluncurkan program eksternal — git, ffmpeg, atau program Java lain — sebagai proses sistem operasi terpisah dengan aliran masukan dan keluarannya sendiri. Java menyediakan ProcessBuilder untuk merakit konfigurasi semacam itu: perintah, direktori kerja, environment, dan pengalihan berkas. Kelas ini bersifat immutable builder: directory() atau environment() mengubah salinan konfigurasi di dalam objek, dan satu builder bisa dipakai berulang untuk meluncurkan banyak proses identik. Chapter ini berfokus pada sisi konfigurasi; membaca aliran, menunggu, dan menghentikan proses dibahas pada chapter berikutnya.

Merakit Perintah #

Perintah dinyatakan sebagai daftar string — elemen pertama program, sisanya argumen — dan daftar ini dikirim ke sistem operasi apa adanya tanpa perantara shell. Format daftar menghilangkan satu kelas kesalahan klasik: argumen yang mengandung spasi atau karakter khusus tidak akan terpecah karena tidak ada parsing yang terjadi. Sebaliknya, API lawas Runtime.exec(String) memecah satu string besar dengan aturan spasi sendiri sehingga argumen bertanda kutip mudah salah potong. Jika shell sungguhan diperlukan (piping, globbing), shell harus diminta eksplisit: new ProcessBuilder("/bin/sh", "-c", "git log | head") — dan tanggung jawab atas isinya menjadi milik pemanggil.

Direktori Kerja dan Environment #

Anak proses mewarisi dua hal dari parent secara default: direktori kerja dan environment. Keduanya bisa diatur sebelum peluncuran. directory() memindahkan titik jalannya anak, sedangkan environment() mengembalikan Map yang bisa dimodifikasi — tambah variabel baru, hapus variabel sensitif, atau ganti nilai warisan. Program berikut meluncurkan sh dengan environment yang sudah disesuaikan dan direktori kerja yang dibuat lebih dulu:

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

public class KonfigurasiProses {
    public static void main(String[] args) throws Exception {
        // Direktori kerja anak dibuat lebih dulu.
        Path proyek = Path.of("proyek");
        Files.createDirectories(proyek);

        ProcessBuilder pb = new ProcessBuilder(
                "/bin/sh", "-c",
                "echo PENGGUNA=$PENGGUNA; echo WARNA=$WARNA; touch penanda.txt");
        pb.directory(proyek.toFile());

        // Environment anak: salinan dari parent, lalu kita sesuaikan.
        Map<String, String> env = pb.environment();
        env.put("PENGGUNA", "Budi");
        env.remove("WARNA");            // variabel dari parent tidak diteruskan
        env.putIfAbsent("WARNA", "netral");

        Process anak = pb.start();
        String keluaran = new String(anak.getInputStream().readAllBytes(),
                StandardCharsets.UTF_8);
        anak.waitFor();

        System.out.print(keluaran);
        System.out.println("penanda dibuat di direktori kerja anak: "
                + Files.exists(proyek.resolve("penanda.txt")));
    }
}

Output yang Dihasilkan:

text
PENGGUNA=Budi
WARNA=netral
penanda dibuat di direktori kerja anak: true

Mari kita telaah baris-baris kuncinya:

  1. Baris new ProcessBuilder("/bin/sh", "-c", "..."): tiga elemen daftar dikirim apa adanya — sh menerima -c dan seluruh skrip sebagai satu argumen utuh, tanpa pemenggalan spasi.
  2. Baris pb.directory(proyek.toFile()): touch penanda.txt dijalankan dari dalam proyek/, dibuktikan baris terakhir yang memeriksa proyek/penanda.txt ada.
  3. Baris env.put("PENGGUNA", "Budi"): variabel baru hanya terlihat oleh anak — environment parent tidak berubah.
  4. Baris env.remove("WARNA") lalu putIfAbsent: menghapus dulu memastikan nilai warisan dari parent tidak lolos; hasilnya WARNA=netral apa pun kondisi parent.

Pengalihan Aliran ke Berkas #

Secara default aliran anak tersambung ke parent lewat pipa (Redirect.PIPE). Untuk keluaran besar atau yang memang ditujukan ke berkas, pengalihan langsung lebih efisien daripada membaca lalu menulis ulang:

Bentuk RedirectEfek
Redirect.PIPE (default)parent membaca lewat anak.getInputStream()
Redirect.INHERITanak berbagi stdin/stdout/stderr parent
Redirect.to(berkas)keluaran menimpa isi berkas
Redirect.appendTo(berkas)keluaran ditambahkan di akhir berkas
Redirect.from(berkas)stdin anak dibaca dari berkas

Program berikut memakai tiga pola sekaligus — tulis ke berkas, tambahkan, dan gabungkan stderr anak ke stdout-nya:

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class PengalihanProses {
    public static void main(String[] args) throws Exception {
        Path berkas = Path.of("laporan.txt");

        // 1) Output anak ditulis ke berkas (mengganti isi).
        ProcessBuilder tulis = new ProcessBuilder("/bin/echo", "baris pertama");
        tulis.redirectOutput(berkas.toFile());
        tulis.start().waitFor();
        System.out.println("isi berkas : " + Files.readString(berkas).trim());

        // 2) Mode append menambah tanpa mengganti.
        ProcessBuilder tambah = new ProcessBuilder("/bin/echo", "baris kedua");
        tambah.redirectOutput(ProcessBuilder.Redirect.appendTo(berkas.toFile()));
        tambah.start().waitFor();
        System.out.println("jumlah baris: " + Files.readAllLines(berkas).size());

        // 3) stderr anak digabung ke stdout-nya.
        ProcessBuilder gabung = new ProcessBuilder("/bin/sh", "-c",
                "echo ke-stdout; echo ke-stderr >&2");
        gabung.redirectErrorStream(true);
        Process anak = gabung.start();
        String tampil = new String(anak.getInputStream().readAllBytes(),
                StandardCharsets.UTF_8);
        anak.waitFor();
        System.out.println("tergabung  : " + tampil.replace("\n", " ").trim());
    }
}

Output yang Dihasilkan:

text
isi berkas : baris pertama
jumlah baris: 2
tergabung  : ke-stdout ke-stderr

Bedah singkatnya: baris redirectOutput(berkas.toFile()) membuat sistem operasi menulis keluaran langsung ke berkas sehingga parent tidak perlu memediasi satu byte pun; Redirect.appendTo memakai mode tulis-tambah sehingga baris kedua menempel di bawah baris pertama; redirectErrorStream(true) menyatukan dua pipa menjadi satu sehingga anak.getInputStream() memuat kedua aliran dalam urutan penulisan anak.

Konfigurasi kini lengkap: perintah, direktori, environment, dan pengalihan. Chapter berikutnya mengambil alih sisi operasionalnya — menjalankan proses, membaca aliran tanpa kehabisan buffer, menunggu dengan batas waktu, dan menafsirkan exit code.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

KonfigurasiProses.java

Unduh berkas
java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

public class KonfigurasiProses {
    public static void main(String[] args) throws Exception {
        // Direktori kerja anak dibuat lebih dulu.
        Path proyek = Path.of("proyek");
        Files.createDirectories(proyek);

        ProcessBuilder pb = new ProcessBuilder(
                "/bin/sh", "-c",
                "echo PENGGUNA=$PENGGUNA; echo WARNA=$WARNA; touch penanda.txt");
        pb.directory(proyek.toFile());

        // Environment anak: salinan dari parent, lalu kita sesuaikan.
        Map<String, String> env = pb.environment();
        env.put("PENGGUNA", "Budi");
        env.remove("WARNA");            // variabel dari parent tidak diteruskan
        env.putIfAbsent("WARNA", "netral");

        Process anak = pb.start();
        String keluaran = new String(anak.getInputStream().readAllBytes(),
                StandardCharsets.UTF_8);
        anak.waitFor();

        System.out.print(keluaran);
        System.out.println("penanda dibuat di direktori kerja anak: "
                + Files.exists(proyek.resolve("penanda.txt")));
    }
}

PengalihanProses.java

Unduh berkas
java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class PengalihanProses {
    public static void main(String[] args) throws Exception {
        Path berkas = Path.of("laporan.txt");

        // 1) Output anak ditulis ke berkas (mengganti isi).
        ProcessBuilder tulis = new ProcessBuilder("/bin/echo", "baris pertama");
        tulis.redirectOutput(berkas.toFile());
        tulis.start().waitFor();
        System.out.println("isi berkas : " + Files.readString(berkas).trim());

        // 2) Mode append menambah tanpa mengganti.
        ProcessBuilder tambah = new ProcessBuilder("/bin/echo", "baris kedua");
        tambah.redirectOutput(ProcessBuilder.Redirect.appendTo(berkas.toFile()));
        tambah.start().waitFor();
        System.out.println("jumlah baris: " + Files.readAllLines(berkas).size());

        // 3) stderr anak digabung ke stdout-nya.
        ProcessBuilder gabung = new ProcessBuilder("/bin/sh", "-c",
                "echo ke-stdout; echo ke-stderr >&2");
        gabung.redirectErrorStream(true);
        Process anak = gabung.start();
        String tampil = new String(anak.getInputStream().readAllBytes(),
                StandardCharsets.UTF_8);
        anak.waitFor();
        System.out.println("tergabung  : " + tampil.replace("\n", " ").trim());
    }
}