杰瑞科技汇

java runtime.exec

Of course. Let's do a deep dive into java.lang.Runtime.exec(). This is a powerful but often misunderstood and misused method in Java.

java runtime.exec-图1
(图片来源网络,侵删)

Overview

Runtime.exec() is a method in the java.lang.Runtime class that allows a Java application to execute an operating system command. It essentially spawns a new process on the host machine and runs the specified command within that process.

There are six overloaded versions of the exec() method, but they all boil down to two main concepts:

  1. Executing a single command string.
  2. Executing an array of command tokens (the command and its arguments).

The Core Problem: Why exec() is Tricky

The most common mistake beginners make is treating exec() like a simple function call that returns a string result. It does not.

When you call Runtime.exec(), you are starting a separate, independent process. Your Java application and the new process run concurrently. If you don't manage the input, output, and error streams of this new process, your Java application can hang indefinitely.

java runtime.exec-图2
(图片来源网络,侵删)

The Golden Rule: You must consume the standard output and error streams of the spawned process. If the process produces a lot of output and the buffer for that stream fills up, the process will block waiting for the buffer to be cleared, which in turn will block your Java application.


The Right Way: A Step-by-Step Guide

Let's execute a simple, safe command: ls -l on Linux/macOS or dir on Windows. We'll follow the best practices.

Step 1: Get the Runtime Instance

The Runtime class is a singleton. You cannot instantiate it yourself. You must get the instance associated with the current Java application using Runtime.getRuntime().

Runtime runtime = Runtime.getRuntime();

Step 2: Define the Command

It's generally safer to pass the command and its arguments as an array of strings. This avoids issues with shell interpretation (like spaces in file names).

java runtime.exec-图3
(图片来源网络,侵删)
// For Linux/macOS
String[] command = {"ls", "-l"};
// For Windows
// String[] command = {"cmd", "/c", "dir"};

Step 3: Execute the Command and Get the Process Object

The exec() method returns a java.lang.Process object. This object is your handle to the newly created process.

Process process = runtime.exec(command);

Step 4: Consume the Output and Error Streams (CRITICAL!)

This is the most important step. We will create two separate Threads to read from the process's output stream (getInputStream()) and error stream (getErrorStream()).

// Create a thread to read the output stream
StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT");
// Create a thread to read the error stream
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR");
// Start the threads
outputGobbler.start();
errorGobbler.start();

What is StreamGobbler? It's a simple helper class that reads from an InputStream line by line and prints it to the console. This prevents the buffer from filling up.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class StreamGobbler extends Thread {
    private final InputStream inputStream;
    private final String type;
    public StreamGobbler(InputStream inputStream, String type) {
        this.inputStream = inputStream;
        this.type = type;
    }
    @Override
    public void run() {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(type + "> " + line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Step 5: Wait for the Process to Finish

After starting the threads, you should wait for the external process to complete. The waitFor() method does this. It blocks the current thread until the process has terminated.

int exitCode = process.waitFor();
System.out.println("\nExit Code: " + exitCode);

Step 6: Clean Up

The waitFor() call ensures that the process has finished. The threads reading its streams should also finish soon after. It's good practice to ensure your main thread waits for the gobbler threads to complete as well.


Complete, Runnable Example

Here is the full, working code combining all the steps.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class RuntimeExecExample {
    public static void main(String[] args) {
        // Define the command and its arguments
        // Use "cmd /c dir" for Windows
        String[] command = {"ls", "-l"};
        try {
            // 1. Get the Runtime instance
            Runtime runtime = Runtime.getRuntime();
            // 2. Execute the command
            Process process = runtime.exec(command);
            // 3. Consume the output and error streams
            StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT");
            StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR");
            outputGobbler.start();
            errorGobbler.start();
            // 4. Wait for the process to finish
            int exitCode = process.waitFor();
            System.out.println("\nProcess finished with exit code: " + exitCode);
            // Optional: Wait for the gobbler threads to finish
            outputGobbler.join();
            errorGobbler.join();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
    // Helper class to read from an input stream
    static class StreamGobbler extends Thread {
        private final InputStream inputStream;
        private final String type;
        public StreamGobbler(InputStream inputStream, String type) {
            this.inputStream = inputStream;
            this.type = type;
        }
        @Override
        public void run() {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(type + "> " + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Modern Alternative: ProcessBuilder

Since Java 5, java.lang.ProcessBuilder has been the recommended way to start external processes. It offers a more flexible and robust API.

Why prefer ProcessBuilder?

  1. Better Command Handling: It's designed to work with an array of command tokens from the start.
  2. Directory Control: You can easily set the working directory for the process using directory(File).
  3. Environment Control: You can modify the environment variables of the new process using environment().
  4. Stream Redirection: You can easily redirect the process's input, output, and error streams.

Here is the same example rewritten using ProcessBuilder:

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ProcessBuilderExample {
    public static void main(String[] args) {
        // 1. Create a new ProcessBuilder
        ProcessBuilder pb = new ProcessBuilder("ls", "-l");
        // Optional: Set the working directory
        // pb.directory(new File("/path/to/directory"));
        // 2. Start the process
        try {
            Process process = pb.start();
            // 3. Consume the streams (same as before)
            StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT");
            StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR");
            outputGobbler.start();
            errorGobbler.start();
            // 4. Wait for the process to finish
            int exitCode = process.waitFor();
            System.out.println("\nProcess finished with exit code: " + exitCode);
            outputGobbler.join();
            errorGobbler.join();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
    // The same StreamGobbler helper class can be reused
    static class StreamGobbler extends Thread {
        // ... (same code as before) ...
    }
}

Summary: Runtime.exec() vs. ProcessBuilder

Feature Runtime.exec() ProcessBuilder Recommendation
API Older, less intuitive. Six overloaded methods. Modern, cleaner, object-oriented. ProcessBuilder
Command Handling Prone to shell injection if using a single string. Designed for an array of tokens. Safer. ProcessBuilder
Working Directory Difficult to set. Easy with directory(File). ProcessBuilder
Environment Hard to modify. Easy with environment(). ProcessBuilder
Stream Handling The same critical issue exists for both. You must consume streams. The same critical issue exists for both. Same for both
Availability Since Java 1.0. Since Java 5. ProcessBuilder is standard.

Final Recommendation: Always use ProcessBuilder. It's a more powerful, safer, and more modern API. Only use Runtime.exec() if you are working with legacy code that relies on it. The core principle of consuming the process's streams, however, is essential regardless of which API you choose.

分享:
扫描分享到社交APP
上一篇
下一篇