Quando Runtime.exec () non lo farà

Come parte del linguaggio Java, il java.langpacchetto viene importato implicitamente in ogni programma Java. Le insidie ​​di questo pacchetto emergono spesso, interessando la maggior parte dei programmatori. Questo mese parlerò delle trappole in agguato nel Runtime.exec()metodo.

Insidia 4: quando Runtime.exec () non lo farà

La classe java.lang.Runtimepresenta un metodo statico chiamato getRuntime(), che recupera l'attuale Java Runtime Environment. Questo è l'unico modo per ottenere un riferimento Runtimeall'oggetto. Con quel riferimento, puoi eseguire programmi esterni invocando il metodo Runtimedella classe exec(). Gli sviluppatori spesso chiamano questo metodo per avviare un browser per visualizzare una pagina della guida in HTML.

Esistono quattro versioni sovraccaricate del exec()comando:

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

Per ciascuno di questi metodi, un comando, e possibilmente un insieme di argomenti, viene passato a una chiamata di funzione specifica del sistema operativo. Successivamente, viene creato un processo specifico del sistema operativo (un programma in esecuzione) con un riferimento a una Processclasse restituita alla Java VM. La Processclasse è una classe astratta, perché Processesiste una sottoclasse specifica di per ogni sistema operativo.

È possibile passare tre possibili parametri di input in questi metodi:

  1. Una singola stringa che rappresenta sia il programma da eseguire che gli eventuali argomenti di quel programma
  2. Un array di stringhe che separano il programma dai suoi argomenti
  3. Un array di variabili d'ambiente

Passa le variabili d'ambiente nel modulo name=value. Se usi la versione di exec()con una singola stringa sia per il programma che per i suoi argomenti, nota che la stringa viene analizzata usando uno spazio bianco come delimitatore tramite la StringTokenizerclasse.

Inciampare in un'eccezione IllegalThreadStateException

La prima trappola relativa a Runtime.exec()è il IllegalThreadStateException. Il primo test prevalente di un'API è codificare i suoi metodi più ovvi. Ad esempio, per eseguire un processo esterno alla Java VM, utilizziamo il exec()metodo. Per vedere il valore restituito dal processo esterno, utilizziamo il exitValue()metodo sulla Processclasse. Nel nostro primo esempio, tenteremo di eseguire il compilatore Java ( javac.exe):

Listato 4.1 BadExecJavac.java

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

Una serie di BadExecJavacproduce:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java BadExecJavac java.lang.IllegalThreadStateException: il processo non è terminato su java.lang.Win32Process.exitValue (Native Method) su BadExecJavac.main (BadExecJavac.java:13) 

Se un processo esterno non è stato ancora completato, il exitValue()metodo genererà un IllegalThreadStateException; ecco perché questo programma non è riuscito. Sebbene la documentazione affermi questo fatto, perché questo metodo non può aspettare fino a quando non può dare una risposta valida?

Uno sguardo più approfondito ai metodi disponibili nella Processclasse rivela un waitFor()metodo che fa esattamente questo. Infatti waitFor()restituisce anche il valore di uscita, il che significa che non usereste exitValue()e waitFor()in congiunzione tra loro, ma piuttosto scegliereste l'uno o l'altro. L'unico tempo possibile che useresti exitValue()invece di waitFor()sarebbe quando non vuoi che il tuo programma si blocchi in attesa di un processo esterno che potrebbe non completarsi mai. Invece di utilizzare il waitFor()metodo, preferirei passare un parametro booleano chiamato waitFornel exitValue()metodo per determinare se il thread corrente deve attendere o meno. Un booleano sarebbe più vantaggioso perchéexitValue()è un nome più appropriato per questo metodo e non è necessario che due metodi eseguano la stessa funzione in condizioni diverse. Tale semplice discriminazione delle condizioni è il dominio di un parametro di input.

Pertanto, per evitare questa trappola, catturare IllegalThreadStateExceptiono attendere il completamento del processo.

Ora risolviamo il problema nel Listato 4.1 e attendiamo il completamento del processo. Nel Listato 4.2, il programma tenta di nuovo di essere eseguito javac.exee quindi attende il completamento del processo esterno:

Listato 4.2 BadExecJavac2.java

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

Sfortunatamente, una serie di BadExecJavac2non produce output. Il programma si blocca e non si completa mai. Perché il javacprocesso non viene mai completato?

Perché Runtime.exec () si blocca

La documentazione Javadoc di JDK fornisce la risposta a questa domanda:

Poiché alcune piattaforme native forniscono solo dimensioni limitate del buffer per flussi di input e output standard, la mancata scrittura tempestiva del flusso di input o la lettura del flusso di output del sottoprocesso può causare il blocco del sottoprocesso e persino un deadlock.

È solo un caso di programmatori che non leggono la documentazione, come implicito nel consiglio spesso citato: leggere il bel manuale (RTFM)? La risposta è parzialmente sì. In questo caso, leggere il Javadoc ti porterebbe a metà strada; spiega che devi gestire i flussi al tuo processo esterno, ma non ti dice come.

Un'altra variabile è in gioco qui, come è evidente dal gran numero di domande del programmatore e idee sbagliate riguardo a questa API nei newsgroup: sebbene Runtime.exec()e le API di processo sembrino estremamente semplici, quella semplicità inganna perché l'uso semplice, o ovvio, dell'API è soggetto a errori. La lezione qui per il progettista API è riservare API semplici per operazioni semplici. Le operazioni soggette a complessità e dipendenze specifiche della piattaforma dovrebbero riflettere accuratamente il dominio. È possibile che un'astrazione venga portata troppo lontano. La JConfiglibreria fornisce un esempio di un'API più completa per gestire le operazioni su file e processi (vedere Risorse di seguito per ulteriori informazioni).

Ora, seguiamo la documentazione JDK e gestiamo l'output del javacprocesso. Quando si esegue javacsenza argomenti, produce una serie di istruzioni di utilizzo che descrivono come eseguire il programma e il significato di tutte le opzioni di programma disponibili. Sapendo che questo sta andando al stderrflusso, puoi facilmente scrivere un programma per esaurire quel flusso prima di aspettare che il processo termini. Il Listato 4.3 completa questo compito. Anche se questo approccio funzionerà, non è una buona soluzione generale. Pertanto, il programma del Listato 4.3 è denominato MediocreExecJavac; fornisce solo una soluzione mediocre. Una soluzione migliore svuoterebbe sia il flusso di errore standard che il flusso di output standard. E la soluzione migliore sarebbe svuotare questi flussi contemporaneamente (lo dimostrerò più tardi).

Listato 4.3 MediocreExecJavac.java

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

Una serie di MediocreExecJavacgenera:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java MediocreExecJavac Utilizzo: javac dove include: -g Genera tutte le informazioni di debug -g: nessuna Genera nessuna informazione di debug -g: {righe, vars, sorgente} Genera solo alcune informazioni di debug -O Ottimizza; può ostacolare il debug o ingrandire i file di classe -nowarn Non genera avvisi -verbose Messaggi di output su ciò che sta facendo il compilatore -deprecation Posizioni di output di origine in cui vengono utilizzate API deprecate -classpath Specifica dove trovare i file di classe utente -sourcepath Specifica dove trovare i file di origine di input -bootclasspath Sostituisce la posizione dei file di classe bootstrap -extdirs Sostituisce la posizione delle estensioni installate -d Specifica dove collocare i file di classe generati -encoding Specifica la codifica dei caratteri utilizzata dai file di origine -target Genera i file di classe per una specifica versione della VM Process exitValue: 2

Quindi, MediocreExecJavacfunziona e produce un valore di uscita di 2. Normalmente, un valore di uscita di 0indica il successo; qualsiasi valore diverso da zero indica un errore. Il significato di questi valori di uscita dipende dal particolare sistema operativo. Un errore Win32 con un valore 2è un errore "file non trovato". Questo ha senso, poiché si javacaspetta che seguiamo il programma con il file del codice sorgente da compilare.

Pertanto, per aggirare la seconda trappola - che rimane per sempre Runtime.exec()- se il programma che si avvia produce output o si aspetta input, assicurarsi di elaborare i flussi di input e output.

Supponendo che un comando sia un programma eseguibile

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) 

Come affermato in precedenza, il valore di errore di 2significa "file non trovato", che, in questo caso, significa che dir.exenon è stato possibile trovare l'eseguibile denominato . Questo perché il comando directory fa parte dell'interprete dei comandi di Windows e non un eseguibile separato. Per eseguire l'interprete dei comandi di Windows, esegui command.como cmd.exe, a seconda del sistema operativo Windows in uso. Il Listato 4.5 esegue una copia dell'interprete dei comandi di Windows e quindi esegue il comando fornito dall'utente (ad esempio dir).

Listato 4.5 GoodWindowsExec.java