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进程⼀直没有完成?
为什么 () 挂起
JDK的Javadoc⽂档提供了这个问题的答案:
因为⼀些本机平台只提供有限的缓冲区⼤⼩为标准输⼊和输出流,未能及时写输⼊流或读取输出流的⼦流程可能会导致⼦流程阻⽌,甚⾄死
锁。
这只是程序员不阅读⽂档的⼀个案例。隐含常听到的建议:读好⼿册(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
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
13 -sourcepath
14 -bootclasspath
15 -extdirs
16 -d
17 -encoding
18 -target
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.
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()⽅法,接受⼀个符合命令⾏可执⾏的字符串。混乱可能是因为command是exec()⽅法的参数
名。因此,程序员错误地将参数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()执⾏⼀个单⼀可执⾏⽂件(⼀个程序或脚本)。如果你想处理流重定向或管道,要么⽤另⼀个程序实现,你必
须通过编程,使⽤java。io包。清单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 95或Windows 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.
5 at ternal(Native Method)
6 at (Unknown Source)
7 at (Unknown Source)
8 at (Unknown Source)
9 at (Unknown Source)
10 at (:45)
我们的第⼀个测试失败,错误193。Win32误差值193“不是⼀个有效的Win32应⽤程序。“这个错误告诉我们,没有找到关联的应⽤程序(如⽹
景浏览器)存在,⽽且这⼀流程不能运⾏⼀个HTML⽂件没有关联的应⽤程序。
因此,我们尝试测试,这次⼜给它⼀个完整路径⽹景。(或者,我们可以添加到我们的PATH environment⽹景变量)。第⼆次运⾏的TestExec产
⽣:
1 E:classescomjavaworldjpitfallsarticle2>java TestExec
2 "e:program e:"
3 ExitValue: 0
ok!⽹景浏览器的打开,然后装⼊Java帮助⽂档。
⼀个额外的改进包括⼀个命令⾏开关,使TestExec接受从标准输⼊的输⼊流。然后您将使⽤putStream()⽅法通过输⼊派⽣外
部程序。
总之,遵循这些法则,以避免的陷阱在运⾏时执⾏():
你不能从外部过程获得⼀个退出状态,直到它已经退出
你必须从你外部程序⽴即处理输⼊、输出和错误流
您必须使⽤运⾏时exec()来执⾏程序
你不能使⽤运⾏时执⾏()就像⼀个命令⾏


发布评论