2023年11月29日发(作者:)

执⾏()需要注意的陷阱

作为Java语⾔的⼀部分。包被隐藏的导⼊到每⼀个Java程序。这个包的表⾯陷阱,经常影响到⼤多数程序员。这个⽉,我将讨论

运⾏时exec()⽅法时的潜伏陷阱。

陷阱4:当运⾏exec()时不会执⾏命令

e类,突出了静态⽅法calledgetRuntime(),,它会检索当前的Java运⾏时环境。这是唯⼀的⽅法来获取Runtime对象的引

⽤。获取该引⽤,您通过可以调⽤Runtime类的exec()⽅法运⾏外部程序。开发⼈员经常调⽤这个⽅法来启动浏览器显⽰⼀个HTML帮助页

⾯。

exec()有四个重载:

1 public Process exec(String command);

2 public Process exec(String [] cmdArray);

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

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

对于每个这样的⽅法,都会产⽣⼀个命令,并可能携带⼀组参数——被传递给⼀个特定操作系统的函数调⽤。这随后创建⼀个特定操作系统

的进程(⼀个运⾏着的程序)procss类将持有该程序返回Java VM的引⽤。这个procss类是⼀个抽象类,具体⼦类的实现依赖于不同的底层

操作系统。

你可以通过三种可能的输⼊参数到这些⽅法:

1、⼀个字符串,表⽰程序执⾏和程序的任何参数。

2、⼀个字符串数组,通过参数来区分出程序的实现功能。

3、⼀个环境变量的数组

传递环境变量是,使⽤格式化的⽅式:名称=值。如果你使⽤单个字符串和它的参数的⽅式调⽤exec()的重载,,注意字符串是通过

StringTokenizer类被解析,使⽤空格作为分隔符。

陷⼊ IllegalThreadStateException

运⾏exec()的第⼀个陷阱,是theIllegalThreadStateException 普遍上,第⼀次对api的尝试,都是基于⼀些最常⽤的⽅法。例如,执⾏⼀

java vm的外部过程,我们使⽤exec()⽅法。查看外部过程的返回值,我们使⽤process类的exitValue()⽅法。看到的值外部过程的回报,

们使⽤exitValue()⽅法在过程类。在我们的第⼀个⽰例中,我们将尝试执⾏Java编译器(javac exe)

清单 4.1

1 import .*;

2 import .*;

3 public class BadExecJavac

4 {

5 public static void main(String args[])

6 {

7 try

8 {

9 Runtime rt = time();

10 Process proc = ("javac");

11 int exitVal = lue();

12 n("Process exitValue: " + exitVal);

13 } catch (Throwable t)

14 {

15 tackTrace();

16 }

17 }

18 }

运⾏的BadExecJavac产⽣:

1 E:classescomjavaworldjpitfallsarticle2>java BadExecJavac

2 lThreadStateException: process has not exited

3 at lue(Native Method)

4 at (:13)

如果⼀个外部进程尚未完成,exitValue()⽅法将抛出IllegalThreadStateException。这就是程序失败的原因。尽管⽂档声明了这个事实,为

什么这个⽅法不能等待⼀个有效性的结果返回?

更彻底的看看process的可⽤⽅法,我们发现waitFor()⽅法,能准确地完成这⼀⼯作。事实上,waitFor()也返回退出值,这意味着你不⽤同时使⽤

exitValue()waitFor(),⽽是选择其中之⼀。唯⼀可能的情况,你会使⽤exitValue()⽽不是waitFor(),是当你不希望你的程序块等待⼀个外部

的过程,⽽这个外部过程可能永远不会完成。取代waitFor()⽅法,我宁愿在exitValue()⽅法内部,传递⼀个布尔参数称为waitFor,来确定是

否当前线程应该等待。⼀个布尔变量会更好,因为exitValue()是⼀个更合适的名称,,也没有必要让两个⽅法在在不同条件下来执⾏相同的功

能。这种简单的通过输⼊参数传递,来区分条件,执⾏不同的功能。

因此,为了避免这个陷阱,要么抓住theIllegalThreadStateException或等待进程完成。

现在,让我们在清单4.1的基础,通过等待进程完成来解决这个问题。清单4.2,程序会再次execute ,然后等待外部过程来完成。

清单4.2

1 import .*;

2 import .*;

3 public class BadExecJavac2

4 {

5 public static void main(String args[])

6 {

7 try

8 {

9 Runtime rt = time();

10 Process proc = ("javac");

11 int exitVal = r();

12 n("Process exitValue: " + exitVal);

13 } catch (Throwable t)

14 {

15 tackTrace();

16 }

17 }

18 }

不幸的是,⼀个运⾏的BadExecJavac2不产⽣任何输出。程序挂起、⼀直未完成。为什么javac进程⼀直没有完成?

为什么 () 挂起

JDKJavadoc⽂档提供了这个问题的答案:

因为⼀些本机平台只提供有限的缓冲区⼤⼩为标准输⼊和输出流,未能及时写输⼊流或读取输出流的⼦流程可能会导致⼦流程阻⽌,甚⾄死

锁。

这只是程序员不阅读⽂档的⼀个案例。隐含常听到的建议:读好⼿册(RTFM)?答案是部分是的。在这种情况下,阅读Javadoc将让你停在半途;

它解释说,你需要处理流到你的外部过程,但是它没有告诉你怎样做。

这个问题,原因明显是⼤量程序员的问题和误解这个API有关信息:尽管运⾏exec()和流程API看起来⾮常简单,那简单是欺骗,因为简单,或者说

是明显,使⽤的API是容易出错。这⾥给API设计师的建议是,为简单的操作保留简单的API。容易产⽣复杂性操作和具有特定平台依赖性应该

准确反映问题域。有可能某个抽象进⾏的太深。这个JConfig库提供了⼀个⽰例的⼀个更完整的API来处理⽂件和流程操作(请参阅下⾯的参

考资料以获得更多信息)

现在,让我们遵循JDK⽂档和处理输出的javac过程。当您运⾏javac不带任何参数,它产⽣⼀组使⽤语句,描述了如何运⾏这个程序及其意义

的所有可⽤的程序的选项。了解这些信息会到stderr(标准错误)流,您可以很容易地编写⼀个程序,在等待进程退出前检测这个输出流。

清单4.3完成这个任务。虽然这种⽅法可以运⾏,但这不是⼀个好的通⽤解决⽅案。因此,清单4.3的程序被命名为MediocreExecJavac;它只

提供了⼀个平庸的解决⽅案。⼀个更好的解决⽅案将不输出或者清空标准错误流和标准输出流。最好的解决⽅案将清空这些流(我之后将证

)

清单 4.3

1 import .*;

2 import .*;

3 public class MediocreExecJavac

4 {

5 public static void main(String args[])

6 {

7 try

8 {

9 Runtime rt = time();

10 Process proc = ("javac");

11 InputStream stderr = orStream();

12 InputStreamReader isr = new InputStreamReader(stderr);

13 BufferedReader br = new BufferedReader(isr);

14 String line = null;

15 n("");

16 while ( (line = ne()) != null)

17 n(line);

18 n("");

19 int exitVal = r();

20 n("Process exitValue: " + exitVal);

21 } catch (Throwable t)

22 {

23 tackTrace();

24 }

25 }

26 }

运⾏

MediocreExecJava产⽣:

1 E:classescomjavaworldjpitfallsarticle2>java MediocreExecJavac

2

3 Usage: javac

4 where includes:

5 -g Generate all debugging info

6 -g:none Generate no debugging info

7 -g:{lines,vars,source} Generate only some debugging info

8 -O Optimize; may hinder debugging or enlarge class files

9 -nowarn Generate no warnings

10 -verbose Output messages about what the compiler is doing

11 -deprecation Output source locations where deprecated APIs are used

12 -classpath Specify where to find user class files

13 -sourcepath Specify where to find input source files

14 -bootclasspath Override location of bootstrap class files

15 -extdirs Override location of installed extensions

16 -d Specify where to place generated class files

17 -encoding Specify character encoding used by source files

18 -target Generate class files for specific VM version

19

20 Process exitValue: 2

所以,MediocreExecJavac运⾏产⽣⼀个退出值2。通常,⼀个退出值0表⽰成功,任何⾮零值表⽰⼀个错误。这些退出值的含义取决于特定的

操作系统。⼀个Win32错误值为2是⼀个未找到⽂件错误。这是有道理的,因为javac期望我们遵循的程序源代码⽂件进⾏编译。

因此,绕过第⼆个陷阱——永远挂在运⾏时exec()——如果你运⾏的程序产⽣输出或期望的输⼊,确保程序的输⼊和输出流。

假设⼀个命令是⼀个可执⾏程序

Windows操作系统,许多新程序员运⾏exec(),当试图使⽤它来完成为⾮执⾏命令,像dir和复制,他们就掉进了运⾏exec()的第三陷阱。

清单4.4展⽰的正是这种情况:

清单 4.4

1 import .*;

2 import .*;

3 public class BadExecWinDir

4 {

5 public static void main(String args[])

6 {

7 try

8 {

9 Runtime rt = time();

10 Process proc = ("dir");

11 InputStream stdin = utStream();

12 InputStreamReader isr = new InputStreamReader(stdin);

13 BufferedReader br = new BufferedReader(isr);

14 String line = null;

15 n("");

16 while ( (line = ne()) != null)

17 n(line);

18 n("");

19 int exitVal = r();

20 n("Process exitValue: " + exitVal);

21 } catch (Throwable t)

22 {

23 tackTrace();

24 }

25 }

26 }

运⾏

BadExecWinDir输出:

1 E:classescomjavaworldjpitfallsarticle2>java BadExecWinDir

2 ption: CreateProcess: dir error=2

3 at (Native Method)

4 at 32Process.(Unknown Source)

5 at ternal(Native Method)

6 at (Unknown Source)

7 at (Unknown Source)

8 at (Unknown Source)

9 at (Unknown Source)

10 at (:12)

如前所述,误差值为2的意思是未找到⽂件”,在这种情况下,意味着可执⾏⽂件名为不能被发现。这是因为⽬录命令是Windows命令解释器的⼀部分,⽽不是⼀个单独的可

执⾏⽂件。要运⾏Windows命令解释器,执⾏或者,这取决于您使⽤的Windows操作系统。清单4.5运⾏的⼀个Windows命令解释器,然后执⾏⽤户提

供的命令(如。,dir)

清单 4.5

1 import .*;

2 import .*;

3 class StreamGobbler extends Thread

4 {

5 InputStream is;

6 String type;

7

8 StreamGobbler(InputStream is, String type)

9 {

10 this.is = is;

11 this.type = type;

12 }

13

14 public void run()

15 {

16 try

17 {

18 InputStreamReader isr = new InputStreamReader(is);

19 BufferedReader br = new BufferedReader(isr);

20 String line=null;

21 while ( (line = ne()) != null)

22 n(type + ">" + line);

23 } catch (IOException ioe)

24 {

25 tackTrace();

26 }

27 }

28 }

29 public class GoodWindowsExec

30 {

31 public static void main(String args[])

32 {

33 if ( < 1)

34 {

35 n("USAGE: java GoodWindowsExec ");

36 (1);

37 }

38

39 try

40 {

41 String osName = perty("" );

42 String[] cmd = new String[3];

43 if( ( "Windows NT" ) )

44 {

45 cmd[0] = "" ;

46 cmd[1] = "/C" ;

47 cmd[2] = args[0];

5 OUTPUT>

6 OUTPUT> Directory of E:classescomjavaworldjpitfallsarticle2

7 OUTPUT>

8 OUTPUT>10/23/00 09:01p 805

9 OUTPUT>10/22/00 09:35a 770

10 OUTPUT>10/24/00 08:45p 488

11 OUTPUT>10/24/00 08:46p 519

12 OUTPUT>10/24/00 09:13p 930

13 OUTPUT>10/22/00 09:21a 2,282

14 OUTPUT>10/22/00 09:20a 2,273

15 ... (some output omitted for brevity)

16 OUTPUT>10/12/00 09:29p 151

17 OUTPUT>10/24/00 09:23p 1,814

18 OUTPUT>10/09/00 05:47p 23,543

19 OUTPUT>10/12/00 08:55p 228

20 OUTPUT> 22 File(s) 46,661 bytes

21 OUTPUT> 19,678,420,992 bytes free

22 ExitValue: 0

GoodWindowsExec运⾏与任何相关的⽂档类型将启动与之关联⽂档类型的应⽤程序。例如,启动Microsoft Word来显⽰⼀个Word⽂档(即⼀

个带doc扩展名的⽂件)

>java GoodWindowsExec ""

注意,GoodWindowsExec使⽤操作系统的系统名字属性来确定,您运⾏的是哪种Windows操作系统,从⽽确定适当的命令解释器。

在执⾏命令解释器,使⽤StreamGobbler类来处理标准错误和标准输⼊流。StreamGobbler通过独⽴线程,来清空任何传递到它的流。这个类使⽤⼀个简单的字符串类型来

表⽰流清空,在当它将打印⾏输出到控制台时起作⽤。

因此,为了避免运⾏exec()的第三个陷阱,⾸先不要认为⼀个命令是⼀个可执⾏程序;其次要了解你是否正在执⾏⼀个独⽴的可执⾏⽂件或⼀种解释命令。在结束这⼀节中,

将演⽰⼀个简单的命令⾏⼯具,可以帮你分析。

值得注意的是,⽤于获取⼀个进程的输出流的⽅法叫做getInputStream()。要记住的是,API看待对象的⾓度,是从Java程序内部,⽽不是外部的过程。因此,外部程序的输出是

Java程序的输⼊。这种逻辑,也表明外部程序的输⼊流,是⼀个Java程序的输出流。

()不是命令⾏

最后⼀个陷阱是,错误的假设命令⾏(shell)能接受的字符串,在()内也能接受。运⾏exec()会受到更多的限制,⽽且不能

跨平台。这个陷阱是由于⽤户试图使⽤exec()⽅法,接受⼀个符合命令⾏可执⾏的字符串。混乱可能是因为commandexec()⽅法的参数

名。因此,程序员错误地将参数command与他可以输⼊命令⾏的东西联系起来,⽽不是将它看做单个程序及其参数。下⾯的清单4.6,⼀个⽤

户尝试执⾏命令和重定向输出在⼀个调⽤exec():

清单 4.6

1 import .*;

2 import .*;

3 // StreamGobbler omitted for brevity

4 public class BadWinRedirect

5 {

6 public static void main(String args[])

7 {

8 try

9 {

10 Runtime rt = time();

11 Process proc = ("java jecho 'Hello World' > ");

12 // any error message?

13 StreamGobbler errorGobbler = new

14 StreamGobbler(orStream(), "ERROR");

15

16 // any output?

17 StreamGobbler outputGobbler = new

18 StreamGobbler(utStream(), "OUTPUT");

19

20 // kick them off

21 ();

22 ();

23

24 // any error

25 int exitVal = r();

26 n("ExitValue: " + exitVal);

27 } catch (Throwable t)

28 {

29 tackTrace();

30 }

31 }

32 }

运⾏BadWinRedirect 产⽣:

E:classescomjavaworldjpitfallsarticle2>java BadWinRedirect

OUTPUT>'Hello World' >

ExitValue: 0

该程序BadWinRedirect试图重定向Java版本的echo程序输出到⽂件 然⽽,我们发现⽂件并不存在。jecho的程序只需要它的

命令⾏参数,并将其写⼊到标准输出流。(你会发现jecho的源代码在源代码可供下载in.)清单4.6,⽤户认为你可以重定向标准输出到⼀个⽂件

中,就像你可能在⼀个DOS命令⾏中执⾏⼀样。然⽽,你不能通过这种⽅法重定向输出。这⾥不正确的假设是,exec()⽅法会像⼀个shell

析器⼀样,但是它不会。相反,exec()执⾏⼀个单⼀可执⾏⽂件(⼀个程序或脚本)。如果你想处理流重定向或管道,要么⽤另⼀个程序实现,你必

须通过编程,使⽤javaio包。清单4.7 完成重定向标准输出流的jecho流程到⼀个⽂件中。

清单4.7

import .*;

import .*;

class StreamGobbler extends Thread

{

InputStream is;

String type;

OutputStream os;

StreamGobbler(InputStream is, String type)

{

this(is, type, null);

}

StreamGobbler(InputStream is, String type, OutputStream redirect)

{

this.is = is;

this.type = type;

this.os = redirect;

}

public void run()

{

try

{

PrintWriter pw = null;

if (os != null)

pw = new PrintWriter(os);

InputStreamReader isr = new InputStreamReader(is);

BufferedReader br = new BufferedReader(isr);

String line=null;

while ( (line = ne()) != null)

{

if (pw != null)

n(line);

n(type + ">" + line);

}

if (pw != null)

();

} catch (IOException ioe)

{

tackTrace();

}

}

}

public class GoodWinRedirect

{

public static void main(String args[])

{

if ( < 1)

{

n("USAGE java GoodWinRedirect ");

(1);

}

try

{

FileOutputStream fos = new FileOutputStream(args[0]);

Runtime rt = time();

Process proc = ("java jecho 'Hello World'");

// any error message?

StreamGobbler errorGobbler = new

StreamGobbler(orStream(), "ERROR");

// any output?

StreamGobbler outputGobbler = new

StreamGobbler(utStream(), "OUTPUT", fos);

// kick them off

();

();

// any error

int exitVal = r();

n("ExitValue: " + exitVal);

();

();

} catch (Throwable t)

{

tackTrace();

}

}

}

运⾏ GoodWinRedirect 产⽣:

1 E:classescomjavaworldjpitfallsarticle2>java GoodWinRedirect

2 OUTPUT>'Hello World'

3 ExitValue: 0

在运⾏GoodWinRedirect确实存在。解决这个陷阱很简单,就是通过外部进程控制重定向的标准输出流,独⽴于exec()⽅法。我

们创建⼀个单独的OutputStream、读取的⽂件名,打开⽂件,我们将从派⽣进程的标准输出写到⽂件。清单4.7通过对StreamGobbler类添加

⼀个新的构造器来完成这个任务。新构造函数有三个参数:读⼊输⼊流,String类型标记我们是否正在使⽤读⼊输出流,重定向输⼊。这个新版

本的StreamGobbler并不违反任何代码之前使⽤的,因为我们没有改变现有的公共API——我们只扩展它。

因为参数来运⾏exec()是依赖于操作系统、不同系统之间的适⽤命令会有所变化。所以,在最终确定()参数和编写代码,快速测

试。清单4.8是⼀个简单的命令⾏⼯具,允许你这样做。

这是⼀个有⽤的练习:尝试修改TestExec重定向标准输⼊和标准输出到⼀个⽂件。当在Windows 95Windows 98上执⾏javac编译器,这将

解决错误消息超出命令⾏有限缓存的问题。

清单4.8

1 import .*;

2 import .*;

3 // class StreamGobbler omitted for brevity

4 public class TestExec

5 {

6 public static void main(String args[])

7 {

8 if ( < 1)

9 {

10 n("USAGE: java TestExec "cmd"");

11 (1);

12 }

13

14 try

15 {

16 String cmd = args[0];

17 Runtime rt = time();

18 Process proc = (cmd);

19

20 // any error message?

21 StreamGobbler errorGobbler = new

22 StreamGobbler(orStream(), "ERR");

23

24 // any output?

25 StreamGobbler outputGobbler = new

26 StreamGobbler(utStream(), "OUT");

27

28 // kick them off

29 ();

30 ();

31

32 // any error

33 int exitVal = r();

34 n("ExitValue: " + exitVal);

35 } catch (Throwable t)

36 {

37 tackTrace();

38 }

39 }

40 }

运⾏TestExec 启动Netscape浏览器和加载Java帮助⽂档 产⽣:

1 E:classescomjavaworldjpitfallsarticle2>java TestExec "e:"

2 ption: CreateProcess: e: error=193

3 at (Native Method)

4 at 32Process.(Unknown Source)

5 at ternal(Native Method)

6 at (Unknown Source)

7 at (Unknown Source)

8 at (Unknown Source)

9 at (Unknown Source)

10 at (:45)

我们的第⼀个测试失败,错误193Win32误差值193“不是⼀个有效的Win32应⽤程序。这个错误告诉我们,没有找到关联的应⽤程序(如⽹

景浏览器)存在,⽽且这⼀流程不能运⾏⼀个HTML⽂件没有关联的应⽤程序。

因此,我们尝试测试,这次⼜给它⼀个完整路径⽹景。(或者,我们可以添加到我们的PATH environment⽹景变量)。第⼆次运⾏的TestExec

:

1 E:classescomjavaworldjpitfallsarticle2>java TestExec

2 "e:program e:"

3 ExitValue: 0

ok!⽹景浏览器的打开,然后装⼊Java帮助⽂档。

⼀个额外的改进包括⼀个命令⾏开关,使TestExec接受从标准输⼊的输⼊流。然后您将使⽤putStream()⽅法通过输⼊派⽣外

部程序。

总之,遵循这些法则,以避免的陷阱在运⾏时执⾏():

你不能从外部过程获得⼀个退出状态,直到它已经退出

你必须从你外部程序⽴即处理输⼊、输出和错误流

您必须使⽤运⾏时exec()来执⾏程序

你不能使⽤运⾏时执⾏()就像⼀个命令⾏