Java控制台程序调试方法

来源:m.fanwen118.com时间:2022.7.20

第1章 Java基础知识

1.1 正确认识Java

作为一名程序设计人员,常常希望有一种编程语言,它具有令人赏心悦目的语法和易于理解的语意。Java恰恰满足了这种需求,它集多种优势于一身,对程序设计人员来说有着不可抗拒的吸引力。原因只有一个,那就是Java不仅仅是一门程序设计语言,而是一个完整的程序设计平台。

程序设计语言与程序设计平台的区别完全可以从现实生活中找到形象的注解。单凭程序设计语言本身想要独步江湖,成为独孤求败那样令人仰望的存在,它就必须是全职高手;而程序设计平台若要力压群雄,则需要打造高效而有力的团队,能在各种环境中利用团队多年的积累而左右逢源。在程序语言家族中不乏功能很好很强大的语言,比如说神级语言——C语言,但是如果要编写一个或具有绘图功能,或具有数据库连接功能,或具有网络数据处理功能的程序时,就需要程序员手工编写大量的代码;而Java语言则可以利用Java平台所提供的各种“工具”,程序员只需要编制几行代码即可。

至此,我们应该开始逐步确定一个概念:学习Java程序设计或软件开发,不仅仅是学习程序语言本身的灵活运用,而是学会使用Java平台提供的工具来完成工作。

1.2 Java的前世今生

Java的历史可以追溯到19xx年,在Sun Microsystem公司里,由Patrick Naughton和James Gosling率领的一个团队在为一个名为“Green”的项目而努力工作,他们在实施这个项目的过程中无意地设计了一种新的程序设计语言,当时该语言被命名为“Oak”,后来他们发现“Oak”已经是另一种程序语言的名字了,遂将其改名为“Java”。可以说Java从被发明开始就是被当成“工具”来使用的,就像后来Gosling在专访中谈到的:“毕竟,语言只是实现目标的工具,而不是目标本身。”后来该项目的产品没有被市场所接受,但是Java语言作为附属产品

则无心插柳地打开了市场。

19xx年5月23日,由纯Java语言编写的浏览器HotJava问世,其目的在于向人们展示Java技术;

19xx年,Java语言的第一版正式发布,即Java1.0。但是该版本的Java语言其教学研究意义大于实际工作运用的作用,它的开发者们都戏称其为玩具语言;

19xx年,Java1.2版发布,该版本基本实现了目前所使用的Java的大多数功能,同时被细分为用于手机等嵌入式设备开发的微型版——J2ME,用于服务器处理的企业版——J2EE和广泛使用的标准版——J2SE;

表1.1展示了Java语言及其类库的发展状况,可以发现其应用程序接口——API的规模发生了惊人的变换。这些API就是Java平台提供的各种工具。

表1.1 Java语言发展状况

Java控制台程序调试方法

1.3 Java与Internet

Java之所以大行其道,从某种程度说与Internet有着密切的关系。Java有着出色的网络编程性能,无论是处理在网络中传输的数据,还是在安全性等各个方面都有着卓越的表现。Internet将Java带到了世界各地的软件公司和程序设计师手中,其市场占有率至今处于第一的位置。图1.1是TIOBE开发语言排行榜20xx年1月的数据。就连比尔盖茨也承认“Java语言是长时间以来最卓越的程序设计语言”。因而Java平台“催生”了微软的.net平台,C#语言基本上就是借鉴Java

语言的思路而开发出来的编程语言。可以说到目前为止,Java平台技术可以说是“一直被追赶,从未被超越”。

Java控制台程序调试方法

图1.1 20xx年1月TIOBE开发语言排行榜

Java与Internet另一个紧密的联系在于Java语言的部署方式。如果经过精心的配置,可以说完成任何工作的Java程序都可以在浏览器中执行。在B/S模式大行其道的今天,Java可以通过网络将其触角伸进生活工作中的任何一个角落。

1.4 扩展阅读

Java学习中遇到的基础性问题和常见问题可以从下面的网站中得到解答。

1. 官方Java白皮书

2. Java 常见FAQ

第2章 Java开发环境

工欲善其事,边先利其器。本章主要介绍Java在工作中的开发环境以及各种编译和运行类型的Java程序的方法。

2.1 安装JDK

Java平台是指安装了JDK的计算机系统。JDK是Java Development Kit的缩写,JDK此概念之前使用的是Java SDK的说法,现在以及不再使用了。JDK可以从Java的官方网站下载,由于Sun公司在20xx年被Oracle公司收购,最新版本的JDK的下载地址是/technetwork/java/javase/downloads/index.html。在下载JDK时必须根据自己所使用的操作系统及其版本来选择适合自己的JDK。由于JDK7的版本还在不断更新中,以及其他配套Java开发工具对JDK7的支持也还不成熟等原因,所以在企业开发中目前任然使用JDK6这一稳定版。目前本书中所有的代码都是在JDK6中进行编译和运行的。

Java控制台程序调试方法

图2.1 下载JDK要注意选择版本

在Windows操作系统中安装JDK非常简单,只需要根据自己设定好安装目录后一直点击“Next”按钮直至结束安装即可。但是安装完成之后并不是大功告成,还需要对操作系统的环境变量进行设置,否则将无法正确编译和运行Java程序。

Java控制台程序调试方法

Java控制台程序调试方法

图2.2(a) 图2.2(b)

Java控制台程序调试方法

Java控制台程序调试方法

图2.2(c) 图2.2(d)

第一步,在“我的电脑”图标上点击鼠标右键,选择“属性”菜单,弹出“系统属性”对话框,如图2.2(a);

第二步,点击“环境变量”按钮,选择“系统变量”中的“path”再点击编辑按钮,如图2.2(b);

第三步,在“编辑环境变量”对话框中的“变量值”的最后一个字母的后面输入“;C:\Program Files\Java\jdk1.6.0_24\bin”(笔者的JDK安装目录是C:\Program Files\Java\jdk1.6.0_24),然后点击“确定”按钮。注意,一定要在英文输入法状态中在“C:”之前输入“;”(分号,用于分隔各种软件的环境变量),如图2.2(c)。

第四步,在如图2.2(b)所示的对话框中,选择“系统变量”的“新建”按钮,出现如图2.2(d)所示对话框。在“变量名”的文本框中输入“CLASSPAT

H”,在“变量值”的文本框中输入“.;C:\Program Files\Java\jdk1.6.0_24\lib;C:\Program Files\Java\jdk1.6.0_24\lib\tools.jar;C:\Program Files\Java\jdk1.6.0_24\lib\dt.jar;”。注意,一定要在最前面输入“.;”(英文输入法的句号和分号),否则该变量值是错误的,将无法对Java程序正确进行编译和运行。

第五步,至此环境变量设置完毕,需要将操纵系统重新启动以使环境变量生效。重启系统后在DOS环境下输入“java -version”,如果出现如图2.3所示的版本信息信息,表示JDK安装配置正确。

Java控制台程序调试方法

图2.3 Java版本信息

2.2 选择开发环境

JDK安装完成之后,所有的Java程序的编译和运行都是在命令行(控制台)环境中通过输入命令实现的。这看起来很麻烦,但是这是一个基本技能,日后可能会用到。在掌握这基本技能之后就需要选择专业的开发工具软件了。目前主流的企业级开发工具有NetBeans、JBuilder和MyEclipse。

2.2.1 Java程序的执行过程

最原始的Java程序设计流程是:在文本编译器中编写Java源代码,然后在命令行中输入“javac”命令进行编译,编译正确后输入“java”命令运行程序。

Java控制台程序调试方法

图2.4 Java程序执行过程 在命令行中编译运行Java程序之所以是必须掌握的基本技能,因为它阐述了程序的执行过程,并与JDK的运行原理有相当紧密的联系。

2.2.2 使用命令行

编写源代码:文件命名为HelloWorld.java。编写完成后文件保存在D盘根目录下。

Java控制台程序调试方法

Java控制台程序调试方法

在命令行中进入D盘后,输入编译Java程序的命令“javac”,接着输入一个空格,然后输入源文件名“HelloWorld.java”,表示用“javac”命令编译该源文件。编译时,必须输入完整的源文件文件名,包括扩展名“java”。

注意:源文件的扩展名是java;命令javac必须全部小写。如果程序编译没有错误,则不会有任何提示信息,结果如图2.5所示。

图2.5 编译成功

编译成功后,输入命令“java”,接着输入一个空格,然后输入字节码文件名“HelloWorld”,表示运行字节码文件。运行Java程序时,输入的字节码文件名不需要包含字节码文件名的扩展名“class”。

该程序运行结果如图2.6所示。

Java控制台程序调试方法

图2.6 运行Java程序

在可视化编程流行的年代,许多程序设计人员对控制台中用命令编译运行程序已经很生疏了,结果就产生了很多令人沮丧错误。现将一些要点例举在此。

如果手工输入文件名,必须注意大小写,必须是HelloWorld,因为Java程序是区分大小写的;

如果看到诸如“Bad command or file name”或“javac:command not found”这类错误提示,则要检查JDK的安装配置,特别是环境变量;

如果javac命令提示“cannot read: HelloWorld.java”,则需要检查是否是在正确的目录下执行的“javac”命令。在本例中如果在C盘根目录或其他目录下执行javac命令,就会出现这种错误;

如果编译时错误太多,以至于命令行窗口一屏显示不了,不便于检查,则可以用“2>”操作符将错误提出保存到特定文件中。例如本例,将源文件中println修改成为错误的写法“pritln”,在输入命令:

javac HelloWorld.java 2> error.txt

此时,将在D盘根目录下生成error.txt的文件,该文件的内容如下:

Java控制台程序调试方法


第二篇:在Java程序中截获控制台输出 15900字

在Java程序中截获控制台输出

在Java开发中,控制台输出仍是一个重要的工具,但默认的控制台输出有着各种各样的局限。本文介绍如何用Java管道流截取控制台输出,分析管道流应用中应该注意的问题,提供了截取Java程序和非Java程序控制台输出的实例。

即使在图形用户界面占统治地位的今天,控制台输出仍旧在Java程序中占有重要地位。控制台不仅是Java程序默认的堆栈跟踪和错误信息输出窗口,而且还是一种实用的调试工具(特别是对习惯于使用println()的人来说)。然而,控制台窗口有着许多局限。例如在Windows 9x平台上,DOS控制台只能容纳50行输出。如果Java程序一次性向控制台输出大量内容,要查看这些内容就很困难了。

对于使用javaw这个启动程序的开发者来说,控制台窗口尤其宝贵。因为用javaw启动java程序时,根本不会有控制台窗口出现。如果程序遇到了问题并抛出异常,根本无法查看Java运行时环境写入到System.out或System.err的调用堆栈跟踪信息。为了捕获堆栈信息,一些人采取了用 try/catch()块封装main()的方式,但这种方式不一定总是有效,在Java运行时的某些时刻,一些描述性错误信息会在抛出异常之前被写入 System.out和System.err;除非能够监测这两个控制台流,否则这些信息就无法看到。

因此,有些时候检查Java运行时环境(或第三方程序)写入到控制台流的数据并采取合适的操作是十分必要的。本文讨论的主题之一就是创建这样一个输入流,从这个输入流中可以读入以前写入Java控制台流(或任何其他程序的输出流)的数据。我们可以想象写入到输出流的数据立即以输入的形式“回流”到了 Java程序。

本文的目标是设计一个基于Swing的文本窗口显示控制台输出。在此期间,我们还将讨论一些和Java管道流(PipedInputStream和 PipedOutputStream)有关的重要注意事项。图一显示了用来截取和显示控制台文本输出的Java程序,用户界面的核心是一个 JTextArea。最后,我们还要创建一个能够捕获和显示其他程序(可以是非Java的程序)控制台输出的简单程序。

一、Java管道流

要在文本框中显示控制台输出,我们必须用某种方法“截取”控制台流。换句话说,我们要有一种高效地读取写入到System.out和System.err 所有内容的方法。如果你熟悉Java的管道流PipedInputStream和PipedOutputStream,就会相信我们已经拥有最有效的工具。

写入到PipedOutputStream输出流的数据可以从对应的PipedInputStream输入流读取。Java的管道流极大地方便了我们截取控制台输出。Listing 1显示了一种非常简单的截取控制台输出方案。

【Listing 1:用管道流截取控制台输出】

PipedInputStream pipedIS = new PipedInputStream();

PipedOutputStream pipedOS = new PipedOutputStream();

try {

pipedOS.connect(pipedIS);

}

catch(IOException e) {

System.err.println("连接失败");

System.exit(1);

}

PrintStream ps = new PrintStream(pipedOS);

System.setOut(ps);

System.setErr(ps);

可以看到,这里的代码极其简单。我们只是建立了一个PipedInputStream,把它设置为所有写入控制台流的数据的最终目的地。所有写入到控制台流的数据都被转到PipedOutputStream,这样,从相应的PipedInputStream读取就可以迅速地截获所有写入控制台流的数据。接下来的事情似乎只剩下在Swing JTextArea中显示从pipedIS流读取的数据,得到一个能够在文本框中显示控制台输出的程序。遗憾的是,在使用Java管道流时有一些重要的注意事项。只有认真对待所有这些注意事项才能保证Listing 1的代码稳定地运行。下面我们来看第一个注意事项。

1.1 注意事项一

PipedInputStream运用的是一个1024字节固定大小的循环缓冲区。写入PipedOutputStream的数据实际上保存到对应的 PipedInputStream的内部缓冲区。从PipedInputStream执行读操作时,读取的数据实际上来自这个内部缓冲区。如果对应的 PipedInputStream输入缓冲区已满,任何企图写入PipedOutputStream的线程都将被阻塞。而且这个写操作线程将一直阻塞,直至出现读取PipedInputStream的操作从缓冲区删除数据。

这意味着,向PipedOutputStream写数据的线程不应该是负责从对应PipedInputStream读取数据的唯一线程。假设线程t是负责从PipedInputStream读取数据的唯一线程;另外,假定t企图在一次对 PipedOutputStream的write()方法的调用中向对应的PipedOutputStream写入2000字节的数据。在t线程阻塞之前,它最多能够写入1024字节的数据(PipedInputStream内部缓冲区的大小)。然而,一旦t被阻塞,读取 PipedInputStream的操作就再也不会出现,因为t是唯一读取PipedInputStream的线程。这样,t线程已经完全被阻塞,同时,所有其他试图向PipedOutputStream写入数据的线程也将遇到同样的情形。

这并不意味着在一次write()调用中不能写入多于1024字节的数据。但应当保证,在写入数据的同时,有另一个线程从PipedInputStream读取数据。

Listing 2示范了这个问题。这个程序用一个线程交替地读取PipedInputStream和写

在Java程序中截获控制台输出

PipedOutputStream。每次调用write()向 PipedInputStream的缓冲区写入20字节,每次调用read()只从缓冲区读取并删除10个字节。内部缓冲区最终会被写满,导致写操作阻塞。由于我们用同一个线程执行读、写操作,一旦写操作被阻塞,就不能再从PipedInputStream读取数据。

【Listing 2:用同一个线程执行读/写操作导致线程阻塞】

import java.io.*;

public class Listing2 {

static PipedInputStream pipedIS = new PipedInputStream();

static PipedOutputStream pipedOS =

new PipedOutputStream();

public static void main(String[] a){

try {

pipedIS.connect(pipedOS);

}

catch(IOException e) {

System.err.println("连接失败");

System.exit(1);

}

byte[] inArray = new byte[10];

byte[] outArray = new byte[20];

int bytesRead = 0;

try {

// 向pipedOS发送20字节数据

pipedOS.write(outArray, 0, 20);

System.out.println(" 已发送20字节...");

// 在每一次循环迭代中,读入10字节

// 发送20字节

bytesRead = pipedIS.read(inArray, 0, 10);

int i=0;

while(bytesRead != -1) {

pipedOS.write(outArray, 0, 20);

System.out.println(" 已发送20字节..."+i);

i++;

bytesRead = pipedIS.read(inArray, 0, 10);

}

}

catch(IOException e) {

System.err.println("读取pipedIS时出现错误: " + e);

System.exit(1);

}

} // main()

}

只要把读/写操作分开到不同的线程,Listing 2的问题就可以轻松地解决。Listing 3是Listing 2经过修

改后的版本,它在一个单独的线程中执行写入PipedOutputStream的操作(和读取线程不同的线程)。为证明一次写入的数据可以超过 1024字节,我们让写操作线程每次调用PipedOutputStream的write()方法时写入2000字节。那么,在 startWriterThread()方法中创建的线程是否会阻塞呢?按照Java运行时线程调度机制,它当然会阻塞。写操作在阻塞之前实际上最多只能写入1024字节的有效载荷(即PipedInputStream缓冲区的大小)。但这并不会成为问题,因为主线程(main)很快就会从 PipedInputStream的循环缓冲区读取数据,空出缓冲区空间。最终,写操作线程会从上一次中止的地方重新开始,写入2000字节有效载荷中的剩余部分。

【Listing 3:把读/写操作分开到不同的线程】

import java.io.*;

public class Listing3 {

static PipedInputStream pipedIS =

new PipedInputStream();

static PipedOutputStream pipedOS =

new PipedOutputStream();

public static void main(String[] args) {

try {

pipedIS.connect(pipedOS);

}

catch(IOException e) {

System.err.println("连接失败");

System.exit(1);

}

byte[] inArray = new byte[10];

int bytesRead = 0;

// 启动写操作线程

startWriterThread();

try {

bytesRead = pipedIS.read(inArray, 0, 10);

while(bytesRead != -1) {

System.out.println("已经读取" +

bytesRead + "字节...");

bytesRead = pipedIS.read(inArray, 0, 10);

}

}

catch(IOException e) {

System.err.println("读取输入错误.");

System.exit(1);

}

} // main()

// 创建一个独立的线程

// 执行写入PipedOutputStream的操作

private static void startWriterThread() {

new Thread(new Runnable() {

public void run() {

byte[] outArray = new byte[2000];

while(true) { // 无终止条件的循环

try {

// 在该线程阻塞之前,有最多1024字节的数据被写入

pipedOS.write(outArray, 0, 2000);

}

catch(IOException e) {

System.err.println("写操作错误");

System.exit(1);

}

System.out.println(" 已经发送2000字节...");

}

}

}).start();

} // startWriterThread()

} // Listing3

也许我们不能说这个问题是Java管道流设计上的缺陷,但在应用管道流时,它是一个必须密切注意的问题。下面我们来看看第二个更重要(更危险的)问题。

1.2 注意事项二

从PipedInputStream读取数据时,如果符合下面三个条件,就会出现IOException异常:

1. 试图从PipedInputStream读取数据,

2. PipedInputStream的缓冲区为“空”(即不存在可读取的数据),

3. 最后一个向PipedOutputStream写数据的线程不再活动(通过Thread.isAlive()检测)。

这是一个很微妙的时刻,同时也是一个极其重要的时刻。假定有一个线程w向PipedOutputStream写入数据;另一个线程r从对应的 PipedInputStream读取数据。下面一系列的事件将导致r线程在试图读取PipedInputStream时遇到IOException异常:

1. w向PipedOutputStream写入数据。

2. w结束(w.isAlive()返回false)。

3. r从PipedInputStream读取w写入的数据,清空PipedInputStream的缓冲区。

4. r试图再次从PipedInputStream读取数据。这时PipedInputStream的缓冲区已经为空,而且w已经结束,从而导致在读操作执行时出现IOException异常。

构造一个程序示范这个问题并不困难,只需从Listing 3的startWriterThread()方法中,删除while(true)条件。这个改动阻止了执行写操作的方法循环执行,使得执行写操作的方法在一次写入操作之后就结束运行。如前所述,此时主线程试图读取PipedInputStraem时,就会遇到一个IOException异常。

这是一种比较少见的情况,而且不存在直接修正它的方法。请不要通过从管道流派生子类的方法修正

该问题――在这里使用继承是完全不合适的。而且,如果Sun以后改变了管道流的实现方法,现在所作的修改将不再有效。

最后一个问题和第二个问题很相似,不同之处在于,它在读线程(而不是写线程)结束时产生IOException异常。

1.3 注意事项三

如果一个写操作在PipedOutputStream上执行,同时最近从对应PipedInputStream读取的线程已经不再活动(通过 Thread.isAlive()检测),则写操作将抛出一个IOException异常。假定有两个线程w和r,w向 PipedOutputStream写入数据,而r则从对应的PipedInputStream读取。下面一系列的事件将导致w线程在试图写入 PipedOutputStream时遇到IOException异常:

1. 写操作线程w已经创建,但r线程还不存在。

2. w向PipedOutputStream写入数据。

3. 读线程r被创建,并从PipedInputStream读取数据。

4. r线程结束。

5. w企图向PipedOutputStream写入数据,发现r已经结束,抛出IOException异常。

实际上,这个问题不象第二个问题那样棘手。和多个读线程/单个写线程的情况相比,也许在应用中有一个读线程(作为响应请求的服务器)和多个写线程(发出请求)的情况更为常见。

1.4 解决问题

要防止管道流前两个局限所带来的问题,方法之一是用一个ByteArrayOutputStream作为代理或替代PipedOutputStream。 Listing 4显示了一个LoopedStreams类,它用一个ByteArrayOutputStream提供和Java管道流类似的功能,但不会出现死锁和 IOException异常。这个类的内部仍旧使用管道流,但隔离了本文介绍的前两个问题。我们先来看看这个类的公用方法。构造函数很简单,它连接管道流,然后调用startByteArrayReaderThread()方法(稍后再讨论该方法)。getOutputStream()方法返回一个OutputStream(具体地说,是一个ByteArrayOutputStream)用以替代PipedOutputStream。写入该 OutputStream的数据最终将在getInputStream()方法返回的流中作为输入出现。和使用PipedOutputStream的情形不同,向ByteArrayOutputStream写入数据的线程的激活、写数据、结束不会带来负面效果。

【Listing 4:防止管道流应用中出现的常见问题】 import java.io.*;

public class LoopedStreams {

private PipedOutputStream pipedOS = new PipedOutputStream();

private boolean keepRunning = true;

private ByteArrayOutputStream byteArrayOS = new ByteArrayOutputStream() { public void close() {

keepRunning = false;

try {

super.close();

pipedOS.close();

}

catch(IOException e) {

// 记录错误或其他处理

// 为简单计,此处我们直接结束 System.exit(1);

}

}

};

在Java程序中截获控制台输出

private PipedInputStream pipedIS = new PipedInputStream() {

public void close() {

keepRunning = false;

try {

super.close();

}

catch(IOException e) {

// 记录错误或其他处理

// 为简单计,此处我们直接结束

System.exit(1);

}

}

};

public LoopedStreams() throws IOException {

pipedOS.connect(pipedIS);

startByteArrayReaderThread();

} // LoopedStreams()

public InputStream getInputStream() {

return pipedIS;

} // getInputStream()

public OutputStream getOutputStream() {

return byteArrayOS;

} // getOutputStream()

private void startByteArrayReaderThread() {

new Thread(new Runnable() {

public void run() {

while(keepRunning) {

// 检查流里面的字节数

if(byteArrayOS.size() > 0) {

byte[] buffer = null;

synchronized(byteArrayOS) {

buffer = byteArrayOS.toByteArray(); byteArrayOS.reset(); // 清除缓冲区 }

try {

// 把提取到的数据发送给PipedOutputStream pipedOS.write(buffer, 0, buffer.length); }

catch(IOException e) {

// 记录错误或其他处理

// 为简单计,此处我们直接结束

System.exit(1);

}

}

else // 没有数据可用,线程进入睡眠状态

try {

// 每隔1秒查看ByteArrayOutputStream检查新数据

Thread.sleep(1000);

}

catch(InterruptedException e) {}

}

}

}).start();

} // startByteArrayReaderThread()

} // LoopedStreams

startByteArrayReaderThread()方法是整个类真正的关键所在。这个方法的目标很简单,就是创建一个定期地检查 ByteArrayOutputStream缓冲区的线程。缓冲区中找到的所有数据都被提取到一个byte数组,然后写入到 PipedOutputStream。由于PipedOutputStream对应的PipedInputStream由 getInputStream()返回,从该输入流读取数据的线程都将读取到原先发送给ByteArrayOutputStream的数据。前面提 到,LoopedStreams类解决了管道流存在的前二个问题,我们来看看这是如何实现的。

ByteArrayOutputStream具有根据需要扩展其内部缓冲区的能力。由于存在“完全缓冲”,线程向getOutputStream()返回 的流写入数据时不会被阻塞。因而,第一个问题不会再给我们带来麻烦。另外还要顺便说一句,ByteArrayOutputStream的缓冲区永远不会缩 减。例如,假设在能够提取数据之前,有一块500 K的数据被写入到流,缓冲区将永远保持至少500 K的容量。如果这个类有一个方法能够在数据被提取之后修正缓冲区的大小,它就会更完善。

第二个问题得以解决的原因在于,实际上任何时候 只有一个线程向PipedOutputStream写入数据,这个线程就是由startByteArrayReaderThread()创建的线程。由于 这个线程完全由LoopedStreams类控制,我们不必担心它会产生IOException异常。

LoopedStreams类还 有一些细节值得提及。首先,我们可以看到byteArrayOS和pipedIS实际上分别是ByteArrayOutputStream和 PipedInputStream的派生类的实例,也即在它们的close()方法中加入了特殊的行为。如果一个LoopedStreams对象的用户关 闭了输入或输出流,在startByteArrayReaderThread()中创建的线程必须关闭。覆盖后的close()方法把 keepRunning标记设置成false以关闭线程。另外,请注意startByteArrayReaderThread()中的同步块。要确保在 toByteArray()调用和reset()调用之间ByteArrayOutputStream缓冲区不被写入流的线程修改,这是必不可少的。由于 ByteArrayOutputStream的write()方法的所有版本都在该流上同步,我们保证了ByteArrayOutputStream的内 部缓冲区不被意外地修改。

注意LoopedStreams类并不涉及管道流的第三个问题。该类的getInputStream()方法返回 PipedInputStream。如果一个线程从该流读取,一段时间后终止,下次数据从ByteArrayOutputStream缓冲区传输到 PipedOutputStream时就会出现IOException异常。

二、捕获Java控制台输出

Listing 5的ConsoleTextArea类扩展Swing JTextArea捕获控制台输出。不要对这个类有这么多代码感到惊讶,必须指出的是,ConsoleTextArea类有超过50%的代码用来进行测试。

【Listing 5:截获Java控制台输出】

import java.io.*;

import java.util.*;

import javax.swing.*;

import javax.swing.text.*;

public class ConsoleTextArea extends JTextArea {

public ConsoleTextArea(InputStream[] inStreams) {

for(int i = 0; i < inStreams.length; ++i)

startConsoleReaderThread(inStreams[i]);

} // ConsoleTextArea()

public ConsoleTextArea() throws IOException {

final LoopedStreams ls = new LoopedStreams();

// 重定向System.out和System.err

PrintStream ps = new PrintStream(ls.getOutputStream());

System.setOut(ps);

System.setErr(ps);

startConsoleReaderThread(ls.getInputStream());

} // ConsoleTextArea()

private void startConsoleReaderThread(

InputStream inStream) {

final BufferedReader br =

new BufferedReader(new InputStreamReader(inStream));

new Thread(new Runnable() { public void run() { StringBuffer sb = new StringBuffer(); try {

String s;

Document doc = getDocument();

while((s = br.readLine()) != null) {

boolean caretAtEnd = false;

caretAtEnd = getCaretPosition() == doc.getLength() ? true : false;

sb.setLength(0);

append(sb.append(s).append('\n').toString()); if(caretAtEnd)

setCaretPosition(doc.getLength());

}

}

catch(IOException e) {

JOptionPane.showMessageDialog(null,

"从BufferedReader读取错误:" + e);

System.exit(1);

}

}

}).start();

} // startConsoleReaderThread()

// 该类剩余部分的功能是进行测试 public static void main(String[] args) { JFrame f = new JFrame("ConsoleTextArea测试"); ConsoleTextArea consoleTextArea = null;

try {

consoleTextArea = new ConsoleTextArea();

}

catch(IOException e) {

System.err.println(

"不能创建LoopedStreams:" + e);

System.exit(1);

}

consoleTextArea.setFont(java.awt.Font.decode("monospaced")); f.getContentPane().add(new JScrollPane(consoleTextArea), java.awt.BorderLayout.CENTER);

f.setBounds(50, 50, 300, 300);

f.setVisible(true);

f.addWindowListener(new java.awt.event.WindowAdapter() { public void windowClosing(

java.awt.event.WindowEvent evt) {

System.exit(0); } }); // 启动几个写操作线程向

// System.out和System.err输出

startWriterTestThread(

"写操作线程 #1", System.err, 920, 50);

startWriterTestThread(

"写操作线程 #2", System.out, 500, 50);

startWriterTestThread(

"写操作线程 #3", System.out, 200, 50);

startWriterTestThread(

"写操作线程 #4", System.out, 1000, 50);

startWriterTestThread(

"写操作线程 #5", System.err, 850, 50);

} // main()

private static void startWriterTestThread(

final String name, final PrintStream ps,

final int delay, final int count) {

new Thread(new Runnable() {

public void run() {

for(int i = 1; i <= count; ++i) {

ps.println("***" + name + ", hello !, i=" + i);

try {

Thread.sleep(delay); } catch(InterruptedException e) {} }

}

}).start();

} // startWriterTestThread()

} // ConsoleTextArea

main()方法创建了一个JFrame,JFrame包含一个ConsoleTextArea的实例。这些代码并没有什么特别之处。Frame显 示出来之后,main()方法启动一系列的写操作线程,写操作线程向控制台流输出大量信息。ConsoleTextArea捕获并显示这些信息,如图一所 示。

ConsoleTextArea提供了两个构造函数。没有参数的构造函数用来捕获和显示所有写入到控制台流的数据,有一个 InputStream[]参数的构造函数转发所有从各个数组元素读取的数据到JTextArea。稍后将有一个例子显示这个构造函数的用处。首先我们来 看看没有参数的ConsoleTextArea构造函数。这个函数首先创建一个LoopedStreams对象;然后请求Java运行时环境把控制台输出 转发到LoopedStreams提供的OutputStream;最后,构造函数调用startConsoleReaderThread(),创建一个 不断地把文本行追加到JTextArea的线程。注意,把文本追加到JTextArea之后,程序小心地保证了插入点的正确位置。

一般 来说,Swing部件的更新不应该在AWT事件分派线程(AWT Event Dispatch Thread,AEDT)之外进行。对于本例来说,这意味着所有把文本追加到JTextArea的操作应该在AEDT中进行,而不是在 startConsoleReaderThread()方法创建的线程中进行。然而,事实上在Swing中向JTextArea追加文本是一个线程安全的 操作。读取一行文本之后,我们只需调用JText.append()就可以把文本追加到JTextArea的末尾。

三、捕获其他程序的控制台输出

在JTextArea中捕获Java程序自己的控制台输出是一回事,去捕获其他程序(甚至包括一些非Java程序)的控制台数据又是另一回事。 ConsoleTextArea提供了捕获其他应用的输出时需要的基础功能,Listing 6的AppOutputCapture利用ConsoleTextArea,截取其他应用的输出信息然后显示在ConsoleTextArea中。

【Listing 6:截获其他程序的控制台输出】

import java.awt.*;

import java.awt.event.*;

import java.io.*;

import javax.swing.*;

public class AppOutputCapture {

private static Process process;

public static void main(String[] args) {

if(args.length == 0) {

System.err.println("用法:java AppOutputCapture " +

"<程序名字> {参数1 参数2 ...}");

System.exit(0);

}

try {

// 启动命令行指定程序的新进程

process = Runtime.getRuntime().exec(args);

}

catch(IOException e) {

System.err.println("创建进程时出错...\n" + e);

System.exit(1);

}

// 获得新进程所写入的流

InputStream[] inStreams =

new InputStream[] {

process.getInputStream(),process.getErrorStream()};

ConsoleTextArea cta = new

ConsoleTextArea(inStreams);

cta.setFont(java.awt.Font.decode("monospaced"));

JFrame frame = new JFrame(args[0] +

"控制台输出");

frame.getContentPane().add(new JScrollPane(cta),

BorderLayout.CENTER);

frame.setBounds(50, 50, 400, 400);

frame.setVisible(true);

frame.addWindowListener(new WindowAdapter() {

public void windowClosing(WindowEvent evt) {

process.destroy();

try {

process.waitFor(); // 在Win98下可能被挂起 }

catch(InterruptedException e) {}

System.exit(0);

}

});

} // main()

} // AppOutputCapture

AppOutputCapture的工作过程如下:首先利用Runtime.exec()方法启动指定程序的一个新进程。启动新进程之后,从结果 Process对象得到它的控制台流。之后,把这些控制台流传入ConsoleTextArea(InputStream[])构造函数(这就是带参数 ConsoleTextArea构造函数的用处)。使用AppOutputCapture时,在命令行上指定待截取其输出的程序名字。例如,如果在 Windows 2000下执行javaw.exe AppOutputCapture ping.exe ,则结果如图四所示。

使用AppOutputCapture时应该注意,被截取输出的应用程序最初输出的一些文本可能无法截取。因为在调用Runtime.exec()和 ConsoleTextArea初始化完成之间存在一小段时间差。在这个时间差内,应用程序输出的文本会丢失。当AppOutputCapture窗口被 关闭,process.destory()调用试图关闭Java程序开始时创建的进程。测试结果显示出,destroy()方法不一定总是有效(至少在 Windows 98上是这样的)。似乎当待关闭的进程启动了额外的进程时,则那些进程不会被关闭。此外,在这种情况下AppOutputCapture程序看起来未能正 常结束。但在Windows NT下,一切正常。如果用JDK v1.1.x运行AppOutputCapture,关闭窗口时会出现一个NullPointerException。这是一个JDK的Bug,JDK 1.2.x和JDK 1.3.x下就不会出现问题。

在Java程序中截获控制台输出

更多类似范文
┣ 学习JAVA程序课程心得 800字
┣ JAVA基础练习程序(1) 1700字
┣ ADRMDR报告反馈工作程序 700字
┣ AppStore程序发布过程总结 1200字
┣ 更多avr程序范例
┗ 搜索类似范文