Programmering

Når Runtime.exec () ikke gør det

Som en del af Java-sproget er java.lang pakken er implicit importeret til hvert Java-program. Denne pakkes faldgruber overflader ofte og påvirker de fleste programmører. Denne måned vil jeg diskutere de fælder, der lurer i Runtime.exec () metode.

Pitfall 4: When Runtime.exec () ikke

Klassen java.lang.Runtime har en statisk metode kaldet getRuntime (), der henter det aktuelle Java Runtime-miljø. Det er den eneste måde at få en henvisning til Kørselstid objekt. Med denne reference kan du køre eksterne programmer ved at påkalde Kørselstid klassens udføre () metode. Udviklere kalder ofte denne metode for at starte en browser til visning af en hjælpeside i HTML.

Der er fire overbelastede versioner af udføre () kommando:

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

For hver af disse metoder sendes en kommando - og muligvis et sæt argumenter - til et operativsystem-specifikt funktionsopkald. Dette skaber efterfølgende en operativsystemspecifik proces (et kørende program) med en henvisning til en Behandle klasse vendte tilbage til Java VM. Det Behandle klasse er en abstrakt klasse, fordi en bestemt underklasse af Behandle findes for hvert operativsystem.

Du kan videregive tre mulige inputparametre til disse metoder:

  1. En enkelt streng, der repræsenterer både det program, der skal udføres, og eventuelle argumenter for det program
  2. En række strenge, der adskiller programmet fra dets argumenter
  3. En række miljøvariabler

Indsend miljøvariablerne i formularen navn = værdi. Hvis du bruger versionen af udføre () med en enkelt streng til både programmet og dets argumenter, bemærk at strengen er analyseret ved hjælp af hvidt mellemrum som afgrænser via StringTokenizer klasse.

Snubler ind i en ulovlig undtagelse

Den første faldgrube vedrørende Runtime.exec () er IllegalThreadStateException. Den udbredte første test af en API er at kode de mest oplagte metoder. For eksempel, for at udføre en proces, der er ekstern til Java VM, bruger vi udføre () metode. For at se den værdi, som den eksterne proces returnerer, bruger vi exitValue () metode til Behandle klasse. I vores første eksempel vil vi forsøge at udføre Java-kompilatoren (javac.exe):

Notering 4.1 BadExecJavac.java

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

Et løb af BadExecJavac producerer:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java BadExecJavac java.lang.IllegalThreadStateException: processen er ikke afsluttet på java.lang.Win32Process.exitValue (Native Method) på BadExecJavac.main (BadExecJavac.java:13) 

Hvis en ekstern proces endnu ikke er afsluttet, exitValue () metoden vil kaste en IllegalThreadStateException; Derfor mislykkedes dette program. Mens dokumentationen angiver denne kendsgerning, hvorfor kan denne metode ikke vente, indtil den kan give et gyldigt svar?

Et mere grundigt kig på de tilgængelige metoder i Behandle klasse afslører en vent på() metode, der gør netop det. Faktisk, vent på() returnerer også exitværdien, hvilket betyder, at du ikke ville bruge exitValue () og vent på() i forbindelse med hinanden, men ville hellere vælge det ene eller det andet. Den eneste mulige tid, du vil bruge exitValue () i stedet for vent på() ville være, når du ikke vil have, at dit program blokerer for at vente på en ekstern proces, der muligvis aldrig afsluttes. I stedet for at bruge vent på() metode, foretrækker jeg at sende en boolsk parameter kaldet vent på ind i exitValue () metode til at bestemme, om den aktuelle tråd skal vente eller ej. En boolsk ville være mere fordelagtig, fordi exitValue () er et mere passende navn til denne metode, og det er ikke nødvendigt for to metoder at udføre den samme funktion under forskellige forhold. En sådan simpel betingelsesdiskrimination er domænet for en inputparameter.

Derfor skal du enten fange den for at undgå denne fælde IllegalThreadStateException eller vent på, at processen er afsluttet.

Lad os nu løse problemet i Listing 4.1 og vente på, at processen er afsluttet. I oversigt 4.2 forsøger programmet igen at udføre javac.exe og venter derefter på, at den eksterne proces er afsluttet:

Notering 4.2 BadExecJavac2.java

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

Desværre et løb af BadExecJavac2 producerer ingen output. Programmet hænger og afsluttes aldrig. Hvorfor gør javac processen aldrig afsluttet?

Hvorfor Runtime.exec () hænger

JDK's Javadoc-dokumentation giver svaret på dette spørgsmål:

Da nogle oprindelige platforme kun leverer begrænset bufferstørrelse til standard input- og output-streams, kan undladelse af straks at skrive input-stream eller læse output-stream fra underprocessen få subprocessen til at blokere og endda blokere.

Er dette kun et tilfælde af, at programmører ikke læser dokumentationen, som det antydes i det ofte citerede råd: læs finmanualen (RTFM)? Svaret er delvist ja. I dette tilfælde ville det være halvvejs at læse Javadoc; det forklarer, at du skal håndtere streams til din eksterne proces, men det fortæller dig ikke hvordan.

En anden variabel er i spil her, som det fremgår af det store antal programmeringsspørgsmål og misforståelser vedrørende denne API i nyhedsgrupperne: dog Runtime.exec () og proces-API'erne synes ekstremt enkle, at enkelheden bedrager, fordi den enkle eller åbenlyse brug af API'en er tilbøjelig til fejl. Lektionen her for API-designeren er at reservere enkle API'er til enkle operationer. Operationer, der er tilbøjelige til kompleksitet og platformsspecifikke afhængigheder, skal afspejle domænet nøjagtigt. Det er muligt for en abstraktion at blive ført for langt. Det JConfig biblioteket giver et eksempel på en mere komplet API til håndtering af fil- og proceshandlinger (se Ressourcer nedenfor for mere information).

Lad os nu følge JDK-dokumentationen og håndtere output fra javac behandle. Når du løber javac uden argumenter producerer den et sæt brugsanvendelser, der beskriver, hvordan man kører programmet og betydningen af ​​alle de tilgængelige programindstillinger. At vide, at dette går til stderr stream, kan du nemt skrive et program for at udtømme den stream, før du venter på, at processen afsluttes. Liste 4.3 fuldfører denne opgave. Selv om denne fremgangsmåde fungerer, er det ikke en god generel løsning. Således er Listing 4.3s program navngivet MiddelmådigExecJavac; det giver kun en middelmådig løsning. En bedre løsning ville tømme både standardfejlstrømmen og standardoutputstrømmen. Og den bedste løsning ville tømme disse streams samtidigt (det viser jeg senere).

Notering 4.3 MediocreExecJavac.java

importer java.util. *; import java.io. *; offentlig klasse MediocreExecJavac {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Process proc = rt.exec ("javac"); InputStream stderr = proc.getErrorStream (); InputStreamReader isr = ny InputStreamReader (stderr); BufferedReader br = ny BufferedReader (isr); String line = null; System.out.println (""); mens ((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 (); }}} 

Et løb af MediocreExecJavac genererer:

E: \ klasser \ com \ javaworld \ jpitfalls \ article2> java MediocreExecJavac Anvendelse: javac hvor inkluderer: -g Generer alle fejlretningsoplysninger -g: ingen Generer ingen fejlretningsoplysninger -g: {linjer, vars, kilde} Generer kun nogle fejlretningsoplysninger -O Optimer; kan forhindre debugging eller forstørre klassefiler -nowarn Generere ingen advarsler -verbose Output-meddelelser om, hvad kompilatoren laver -deprecation Outputkildelokaliteter, hvor forældede API'er bruges -classpath Angiv, hvor man skal finde brugerklassefiler -sourcepath Angiv, hvor man finder inputkildefiler -bootclasspath Tilsidesættelse af bootstrap-klassefilers placering -extdirs Tilsidesættelse af installerede udvidelsers placering -d Angiv, hvor genererede klassefiler skal placeres -kodning Angiv tegnkodning, der bruges af kildefiler -target Generer klassefiler til specifik VM-version Process exitValue: 2 

Så, MiddelmådigExecJavac fungerer og producerer en exitværdi på 2. Normalt er en exitværdi på 0 angiver succes; enhver værdi, der ikke er nul, angiver en fejl. Betydningen af ​​disse udgangsværdier afhænger af det bestemte operativsystem. En Win32-fejl med værdien 2 er en "fil ikke fundet" -fejl. Det giver mening, siden javac forventer, at vi følger programmet med kildekodefilen, der skal kompileres.

Således at omgå den anden faldgrube - hængende for evigt i Runtime.exec () - hvis det program, du starter, producerer output eller forventer input, skal du sørge for at behandle input- og output-streams.

Forudsat at en kommando er et eksekverbart program

Under Windows-operativsystemet snubler mange nye programmører over Runtime.exec () når du prøver at bruge det til ikke-eksekverbare kommandoer som dir og kopi. Derefter løber de ind Runtime.exec ()tredje faldgrube. Liste 4.4 viser nøjagtigt det:

Notering 4.4 BadExecWinDir.java

importer java.util. *; import java.io. *; offentlig klasse BadExecWinDir {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Process proc = rt.exec ("dir"); InputStream stdin = proc.getInputStream (); InputStreamReader isr = ny InputStreamReader (stdin); BufferedReader br = ny BufferedReader (isr); String line = null; System.out.println (""); mens ((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 (); }}} 

Et løb af BadExecWinDir producerer:

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. (Ukendt kilde) på java.lang.Runtime.execInternal (Native Method) på java.lang.Runtime.exec (Ukendt kilde) hos java.lang.Runtime.exec (Ukendt kilde) på java.lang.Runtime.exec (Ukendt kilde) hos java .lang.Runtime.exec (Ukendt kilde) på BadExecWinDir.main (BadExecWinDir.java:12) 

Som tidligere nævnt er fejlværdien af 2 betyder "fil ikke fundet", hvilket i dette tilfælde betyder, at den eksekverbare navngivne dir.exe kunne ikke findes. Det skyldes, at katalogkommandoen er en del af Windows-kommandotolken og ikke en separat eksekverbar. For at køre Windows-kommandotolken skal du udføre enten command.com eller cmd.exeafhængigt af det Windows-operativsystem, du bruger. Listing 4.5 kører en kopi af Windows-kommandotolken og udfører derefter den brugerleverede kommando (f.eks. dir).

Notering 4.5 GoodWindowsExec.java