1. Strumienie wejścia/wyjścia procesu
Gdy uruchamiasz proces zewnętrzny z Javy, między nim a twoim programem otwiera się prawdziwy mini-dialog – trzy kanały komunikacji.
Pierwszy kanał to standardowe wyjście. Wszystko, co proces zewnętrzny chce pokazać „na ekranie”, możesz przechwycić przez Process.getInputStream(). Drugi kanał to strumień błędów. Jeśli proces postanowi poskarżyć się lub wypisać komunikat o błędzie, dowiesz się o tym przez Process.getErrorStream(). I wreszcie trzeci – standardowe wejście. Tędy możesz wysłać do procesu jakieś dane, jeśli czegoś od ciebie oczekuje. Robi się to za pomocą Process.getOutputStream().
To może trochę mylić, bo nazwy strumieni wyglądają, jakby były „na odwrót”. Dla procesu InputStream to to, co czytasz (jego wyjście), a OutputStream to to, dokąd piszesz (jego wejście). Ale z perspektywy twojego programu w Javie wszystko jest logiczne: czytasz to, co proces wypisuje, i zapisujesz to, co chcesz mu przekazać.
Schemat strumieni procesu
+-------------------+ +---------------------+
| Twój program | <-------> | Proces zewnętrzny |
+-------------------+ +---------------------+
| |
| getOutputStream() --------> stdin
| getInputStream() <-------- stdout
| getErrorStream() <-------- stderr
2. Odczyt wyjścia procesu (stdout)
Nauczmy się czytać to, co proces zewnętrzny wypisuje na ekran. Może to być wynik polecenia, wersja programu, komunikat o powodzeniu – cokolwiek.
Przykład 1: Odczyt wyjścia polecenia java -version
Najpierw uruchomimy proces, a potem odczytamy jego wyjście.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class ProcessOutputExample {
public static void main(String[] args) throws IOException {
// Tworzymy proces: polecenie zależy od twojego systemu operacyjnego!
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// Czytamy strumień błędów (java -version zwykle tam pisze!)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
}
}
Uwaga! Polecenie java -version wypisuje wynik nie do zwykłego stdout, lecz do strumienia błędów stderr. To nie błąd, a cecha Javy. Dlatego czytamy process.getErrorStream(). Dla większości innych poleceń (na przykład echo, ls, dir) używaj getInputStream().
Przykład 2: Odczyt wyjścia polecenia echo
ProcessBuilder pb = new ProcessBuilder("echo", "Hello, Java!");
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
Dlaczego potrzebny jest BufferedReader?
Standardowy strumień wejścia to strumień bajtów (InputStream). Aby czytać go jako linie tekstu, używamy zestawu InputStreamReader (przekształca bajty w znaki) + BufferedReader (wygodne czytanie wierszami).
3. Zapis do wejściowego strumienia procesu (stdin)
Czasem proces zewnętrzny oczekuje od nas danych. Na przykład gdy uruchamiasz skrypt proszący o nazwę użytkownika albo kalkulator czekający na liczby.
Do tego użyj process.getOutputStream() – tak, właśnie tak: „piszesz” do strumienia procesu.
Przykład 3: Przekazywanie danych do procesu
Załóżmy, że masz prosty skrypt w Pythonie, który czyta wiersz i wypisuje go z powrotem:
echo_input.py:
# echo_input.py
line = input()
print("You said:", line)
Uruchomimy go z Javy i wyślemy mu wiersz:
import java.io.*;
public class ProcessStdinExample {
public static void main(String[] args) throws IOException {
ProcessBuilder pb = new ProcessBuilder("python", "echo_input.py");
Process process = pb.start();
// Piszemy do stdin procesu
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(process.getOutputStream()))) {
writer.write("Pozdrowienia z Javy!\n");
writer.flush();
}
// Odczytujemy stdout procesu
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Odpowiedź procesu: " + line);
}
}
}
}
Nie zapominaj o flush() – w przeciwnym razie dane mogą „utknąć” w buforze i nie trafić do procesu.
4. Obsługa błędów: odczyt stderr
Proces zewnętrzny może nie tylko cieszyć nas wynikami, ale też narzekać – wypisywać błędy. Jeśli nie czytasz strumienia błędów, możesz przeoczyć ważne informacje, a nawet doprowadzić do zawieszenia (deadlock), jeśli bufor błędów się zapełni.
Przykład 4: Odczyt stderr procesu
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// Czytamy błędy
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
System.out.println("Błąd procesu: " + line);
}
}
Dlaczego warto czytać oba strumienie?
Jeśli proces bardzo dużo zapisuje do stdout lub stderr, a ty nie czytasz tych strumieni, bufor systemu operacyjnego może się zapełnić i proces „zawiśnie”, czekając aż ktoś zwolni miejsce. Dlatego dobrą praktyką jest czytać oba strumienie równolegle (albo przynajmniej naprzemiennie).
5. Praktyka: uruchamianie polecenia, odczyt wyjścia, obsługa błędów
Połączmy wszystko powyżej w jedno małe narzędzie, które uruchamia polecenie zewnętrzne, czyta jego standardowe wyjście i błędy oraz wypisuje wszystko na ekran.
Przykład 5: Uniwersalne uruchamianie polecenia
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class RunCommand {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("Podaj polecenie do uruchomienia, na przykład: java RunCommand ls -l");
return;
}
var process = new ProcessBuilder(args).start();
// Uruchamiamy oddzielne wątki do czytania stdout i stderr
var t1 = Thread.ofPlatform().start(() -> readStream(process.inputReader(), "[stdout]"));
var t2 = Thread.ofPlatform().start(() -> readStream(process.errorReader(), "[stderr]"));
int exitCode = process.waitFor(); // czekamy na zakończenie procesu
t1.join();
t2.join();
System.out.println("Proces zakończył się kodem: " + exitCode);
}
private static void readStream(BufferedReader reader, String prefix) {
try (reader) {
reader.lines().forEach(line -> System.out.println(prefix + " " + line));
} catch (Exception e) {
System.err.println("Błąd odczytu " + prefix + ": " + e.getMessage());
}
}
}
Taka metoda pozwala uniknąć deadlocka i nie stracić żadnego błędu!
6. Krótko o kodowaniach
Domyślnie InputStreamReader używa kodowania systemowego. Jeśli proces zewnętrzny zapisuje w innym kodowaniu (na przykład UTF-8, a u ciebie Windows-1251), wyjście może wyglądać jak „krzaczki”. Wskaż kodowanie jawnie:
new InputStreamReader(process.getInputStream(), "UTF-8")
7. Przykład: uruchomienie skryptu Pythona i przekazanie mu danych
Załóżmy, że masz skrypt w Pythonie, który oczekuje kilku wierszy na wejściu, a następnie wypisuje wynik.
adder.py:
# adder.py
a = int(input())
b = int(input())
print(a + b)
Z Javy możesz wysłać mu liczby i odczytać odpowiedź:
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
public class PythonAdder {
public static void main(String[] args) throws Exception {
var process = new ProcessBuilder("python", "adder.py").start();
// Wysyłamy dwie liczby do stdin skryptu w Pythonie
try (var writer = process.outputWriter(StandardCharsets.UTF_8)) {
writer.println("10");
writer.println("25");
}
// Czytamy wynik ze stdout za pomocą inputReader
try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
reader.lines().forEach(line -> System.out.println("Odpowiedź od Pythona: " + line));
}
int exitCode = process.waitFor();
System.out.println("Python zakończył się kodem: " + exitCode);
}
}
8. Skąd się bierze deadlock? Jak go uniknąć
Deadlock to sytuacja, gdy proces i twój program czekają na siebie nawzajem w nieskończoność. Na przykład jeśli bufor wyjścia procesu się zapełni, a ty nie czytasz tego strumienia, proces się zatrzyma. Jeśli czekasz na zakończenie procesu, a on czeka, aż mu coś zapiszesz – to również deadlock.
Jak uniknąć:
- Zawsze czytaj oba strumienie (stdout i stderr).
- Dla długotrwałych/wielomównych procesów używaj osobnych wątków do odczytu.
- Nie blokuj zapisu i odczytu jednocześnie w jednym wątku.
9. Typowe błędy przy pracy ze strumieniami wejścia/wyjścia procesu
Błąd nr 1: Odczyt tylko jednego strumienia (stdout lub stderr).
Jeśli czytasz tylko stdout, a proces zapisuje błąd do stderr, bufor błędów może się zapełnić – proces się zawiesi. Rozwiązanie: czytać oba strumienie (najlepiej w osobnych wątkach).
Błąd nr 2: Nieprawidłowe kodowanie.
Jeśli nie wskażesz odpowiedniego kodowania, wyjście może być nieczytelne. Zawsze sprawdzaj, w jakim kodowaniu zapisuje proces zewnętrzny (na przykład jawnie ustaw "UTF-8" lub użyj StandardCharsets.UTF_8).
Błąd nr 3: Niezamknięte strumienie.
Otwarte strumienie mogą prowadzić do wycieków zasobów. Używaj try-with-resources lub zamykaj wszystkie strumienie jawnie.
Błąd nr 4: Brak flush() podczas zapisu do stdin procesu.
Jeśli zapomnisz wywołać flush(), dane mogą nie trafić do procesu – będzie on wiecznie czekał na wejście.
Błąd nr 5: Różnice między platformami.
Polecenia i składnia mogą się różnić między Windows i Linux/Mac. Sprawdzaj system przez System.getProperty("os.name") i dobieraj polecenia odpowiednio.
Błąd nr 6: Brak obsługi wyjątków.
Praca z procesami często wiąże się z IOException. Zawsze obsługuj wyjątki, aby program nie zakończył się awaryjnie.
GO TO FULL VERSION