Wenn Runtime.exec () nicht

Als Teil der Java-Sprache wird das java.langPaket implizit in jedes Java-Programm importiert. Die Fallstricke dieses Pakets treten häufig auf und betreffen die meisten Programmierer. Diesen Monat werde ich die Fallen besprechen, die in der Runtime.exec()Methode lauern .

Fallstricke 4: Wenn Runtime.exec () dies nicht tut

Die Klasse java.lang.Runtimeverfügt über eine statische Methode namens getRuntime(), die die aktuelle Java-Laufzeitumgebung abruft. Nur so erhalten Sie einen Verweis auf das RuntimeObjekt. Mit dieser Referenz können Sie externe Programme ausführen, indem Sie die Methode der RuntimeKlasse aufrufen exec(). Entwickler rufen diese Methode häufig auf, um einen Browser zum Anzeigen einer Hilfeseite in HTML zu starten.

Es gibt vier überladene Versionen des exec()Befehls:

  • public Process exec(String command);
  • public Process exec(String [] cmdArray);
  • public Process exec(String command, String [] envp);
  • public Process exec(String [] cmdArray, String [] envp);

Für jede dieser Methoden wird ein Befehl - und möglicherweise eine Reihe von Argumenten - an einen betriebssystemspezifischen Funktionsaufruf übergeben. Dadurch wird anschließend ein betriebssystemspezifischer Prozess (ein laufendes Programm) mit einem Verweis auf eine ProcessKlasse erstellt, die an die Java-VM zurückgegeben wird. Die ProcessKlasse ist eine abstrakte Klasse, da Processfür jedes Betriebssystem eine bestimmte Unterklasse von vorhanden ist.

Sie können drei mögliche Eingabeparameter an diese Methoden übergeben:

  1. Eine einzelne Zeichenfolge, die sowohl das auszuführende Programm als auch alle Argumente für dieses Programm darstellt
  2. Ein Array von Zeichenfolgen, die das Programm von seinen Argumenten trennen
  3. Ein Array von Umgebungsvariablen

Übergeben Sie die Umgebungsvariablen im Formular name=value. Wenn Sie die Version von exec()mit einer einzelnen Zeichenfolge sowohl für das Programm als auch für seine Argumente verwenden, beachten Sie, dass die Zeichenfolge mithilfe von Leerzeichen als Trennzeichen über die StringTokenizerKlasse analysiert wird .

Stolpern in eine IllegalThreadStateException

Die erste Falle in Bezug auf Runtime.exec()ist die IllegalThreadStateException. Der vorherrschende erste Test einer API besteht darin, ihre offensichtlichsten Methoden zu codieren. Um beispielsweise einen Prozess außerhalb der Java-VM auszuführen, verwenden wir die exec()Methode. Um den Wert anzuzeigen, den der externe Prozess zurückgibt, verwenden wir die exitValue()Methode für die ProcessKlasse. In unserem ersten Beispiel werden wir versuchen, den Java-Compiler ( javac.exe) auszuführen :

Listing 4.1: BadExecJavac.java

import java.util. *; import java.io. *; öffentliche Klasse BadExecJavac {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Prozess proc = rt.exec ("javac"); int exitVal = proc.exitValue (); System.out.println ("Process exitValue:" + exitVal); } catch (Throwable t) {t.printStackTrace (); }}}

Eine Reihe von BadExecJavacProdukten:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java BadExecJavac java.lang.IllegalThreadStateException: Der Prozess wurde bei java.lang.Win32Process.exitValue (native Methode) bei BadExecJavac.main (BadExecJavac.java:13) nicht beendet 

Wenn ein externer Prozess noch nicht abgeschlossen ist, exitValue()löst die Methode ein IllegalThreadStateException; Deshalb ist dieses Programm fehlgeschlagen. Während die Dokumentation diese Tatsache angibt, warum kann diese Methode nicht warten, bis sie eine gültige Antwort geben kann?

Ein genauerer Blick auf die in der ProcessKlasse verfügbaren Methoden zeigt eine waitFor()Methode, die genau das tut. Gibt waitFor()auch den Exit-Wert zurück, was bedeutet, dass Sie nicht exitValue()und waitFor()in Verbindung miteinander verwenden, sondern den einen oder anderen auswählen würden. Die einzige mögliche Zeit, die Sie exitValue()anstelle von verwenden waitFor()würden, wäre, wenn Sie nicht möchten, dass Ihr Programm das Warten auf einen externen Prozess blockiert, der möglicherweise nie abgeschlossen wird. Anstatt die waitFor()Methode zu verwenden, würde ich lieber einen booleschen Parameter übergeben, der waitForin die exitValue()Methode aufgerufen wird, um zu bestimmen, ob der aktuelle Thread warten soll oder nicht. Ein Boolescher Wert wäre da vorteilhafterexitValue()ist ein passenderer Name für diese Methode, und es ist nicht erforderlich, dass zwei Methoden dieselbe Funktion unter verschiedenen Bedingungen ausführen. Eine solche einfache Bedingungsunterscheidung ist die Domäne eines Eingabeparameters.

Um diese Falle zu vermeiden, fangen Sie entweder die IllegalThreadStateExceptionoder warten Sie, bis der Vorgang abgeschlossen ist.

Beheben wir nun das Problem in Listing 4.1 und warten, bis der Vorgang abgeschlossen ist. In Listing 4.2 versucht das Programm erneut, es auszuführen, javac.exeund wartet dann, bis der externe Prozess abgeschlossen ist:

Listing 4.2: BadExecJavac2.java

import java.util. *; import java.io. *; öffentliche Klasse BadExecJavac2 {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Prozess proc = rt.exec ("javac"); int exitVal = proc.waitFor (); System.out.println ("Process exitValue:" + exitVal); } catch (Throwable t) {t.printStackTrace (); }}}

Leider BadExecJavac2erzeugt ein Lauf von keine Ausgabe. Das Programm hängt und wird nie abgeschlossen. Warum wird der javacProzess nie abgeschlossen?

Warum Runtime.exec () hängt

Die Javadoc-Dokumentation des JDK bietet die Antwort auf diese Frage:

Da einige native Plattformen nur eine begrenzte Puffergröße für Standardeingabe- und -ausgabestreams bereitstellen, kann das Versäumnis, den Eingabestream sofort zu schreiben oder den Ausgabestream des Unterprozesses zu lesen, dazu führen, dass der Unterprozess blockiert und sogar blockiert wird.

Ist dies nur ein Fall von Programmierern, die die Dokumentation nicht lesen, wie in den oft zitierten Ratschlägen impliziert: Lesen Sie das feine Handbuch (RTFM)? Die Antwort lautet teilweise ja. In diesem Fall bringt Sie das Lesen des Javadoc auf halbem Weg dorthin. Es wird erklärt, dass Sie die Streams für Ihren externen Prozess verarbeiten müssen, es wird jedoch nicht angegeben, wie.

Eine andere Variable spielt hier eine Rolle, wie aus der großen Anzahl von Programmiererfragen und Missverständnissen in Bezug auf diese API in den Newsgroups hervorgeht: Obwohl Runtime.exec()und die Prozess-APIs äußerst einfach erscheinen, täuscht diese Einfachheit aufgrund der einfachen oder offensichtlichen Verwendung der API ist fehleranfällig. Die Lektion hier für den API-Designer besteht darin, einfache APIs für einfache Operationen zu reservieren. Vorgänge, die zu Komplexitäten und plattformspezifischen Abhängigkeiten neigen, sollten die Domäne genau widerspiegeln. Es ist möglich, dass eine Abstraktion zu weit getragen wird. Die JConfigBibliothek bietet ein Beispiel für eine vollständigere API zur Verarbeitung von Datei- und Prozessvorgängen (weitere Informationen finden Sie unter Ressourcen unten).

Folgen wir nun der JDK-Dokumentation und behandeln die Ausgabe des javacProzesses. Wenn Sie javacohne Argumente ausführen , werden eine Reihe von Verwendungsanweisungen erstellt, die beschreiben, wie das Programm ausgeführt wird und welche Bedeutung alle verfügbaren Programmoptionen haben. Wenn Sie wissen, dass dies in den stderrStream geht, können Sie einfach ein Programm schreiben, um diesen Stream zu erschöpfen, bevor Sie auf den Abschluss des Prozesses warten. Listing 4.3 schließt diese Aufgabe ab. Dieser Ansatz wird zwar funktionieren, ist jedoch keine gute allgemeine Lösung. Daher wird das Programm von Listing 4.3 benannt MediocreExecJavac. es bietet nur eine mittelmäßige Lösung. Eine bessere Lösung würde sowohl den Standardfehlerstrom als auch den Standardausgabestream leeren. Und die beste Lösung würde diese Streams gleichzeitig leeren (das werde ich später demonstrieren).

Listing 4.3: MediocreExecJavac.java

import java.util. *; import java.io. *; öffentliche Klasse MediocreExecJavac {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Prozess proc = rt.exec ("javac"); InputStream stderr = proc.getErrorStream (); InputStreamReader isr = neuer InputStreamReader (stderr); BufferedReader br = neuer BufferedReader (isr); String line = null; System.out.println (""); while ((line = br.readLine ())! = null) System.out.println (line); System.out.println (""); int exitVal = proc.waitFor (); System.out.println ("Process exitValue:" + exitVal); } catch (Throwable t) {t.printStackTrace (); }}}

Eine Reihe von MediocreExecJavacgeneriert:

E:\classes\com\javaworld\jpitfalls\article2>java MediocreExecJavac  Usage: javac   where  includes: -g Generate all debugging info -g:none Generate no debugging info -g:{lines,vars,source} Generate only some debugging info -O Optimize; may hinder debugging or enlarge class files -nowarn Generate no warnings -verbose Output messages about what the compiler is doing -deprecation Output source locations where deprecated APIs are used -classpath  Specify where to find user class files -sourcepath  Specify where to find input source files -bootclasspath  Override location of bootstrap class files -extdirs  Override location of installed extensions -d  Specify where to place generated class files -encoding  Specify character encoding used by source files -target  Generate class files for specific VM version  Process exitValue: 2 

So, MediocreExecJavac works and produces an exit value of 2. Normally, an exit value of 0 indicates success; any nonzero value indicates an error. The meaning of these exit values depends on the particular operating system. A Win32 error with a value of 2 is a "file not found" error. That makes sense, since javac expects us to follow the program with the source code file to compile.

Thus, to circumvent the second pitfall -- hanging forever in Runtime.exec() -- if the program you launch produces output or expects input, ensure that you process the input and output streams.

Assuming a command is an executable program

Under the Windows operating system, many new programmers stumble upon Runtime.exec() when trying to use it for nonexecutable commands like dir and copy. Subsequently, they run into Runtime.exec()'s third pitfall. Listing 4.4 demonstrates exactly that:

Listing 4.4 BadExecWinDir.java

import java.util.*; import java.io.*; public class BadExecWinDir { public static void main(String args[]) { try { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("dir"); InputStream stdin = proc.getInputStream(); InputStreamReader isr = new InputStreamReader(stdin); BufferedReader br = new BufferedReader(isr); String line = null; System.out.println(""); while ( (line = br.readLine()) != null) System.out.println(line); System.out.println(""); int exitVal = proc.waitFor(); System.out.println("Process exitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } } 

A run of BadExecWinDir produces:

E:\classes\com\javaworld\jpitfalls\article2>java BadExecWinDir java.io.IOException: CreateProcess: dir error=2 at java.lang.Win32Process.create(Native Method) at java.lang.Win32Process.(Unknown Source) at java.lang.Runtime.execInternal(Native Method) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at BadExecWinDir.main(BadExecWinDir.java:12) 

Wie bereits erwähnt, bedeutet der Fehlerwert 2"Datei nicht gefunden", was in diesem Fall bedeutet, dass die benannte ausführbare Datei nicht gefunden dir.exewerden konnte. Dies liegt daran, dass der Verzeichnisbefehl Teil des Windows-Befehlsinterpreters und keine separate ausführbare Datei ist. Führen Sie zum Ausführen des Windows-Befehlsinterpreters je nach verwendetem Windows-Betriebssystem entweder command.comoder cmd.exeaus. Listing 4.5 führt eine Kopie des Windows-Befehlsinterpreters aus und führt dann den vom Benutzer angegebenen Befehl aus (z dir. B. ).

Listing 4.5: GoodWindowsExec.java