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

上传人:b****5 文档编号:4189763 上传时间:2022-11-28 格式:DOCX 页数:13 大小:73.76KB
下载 相关 举报
在Java程序中截获控制台输出.docx_第1页
第1页 / 共13页
在Java程序中截获控制台输出.docx_第2页
第2页 / 共13页
在Java程序中截获控制台输出.docx_第3页
第3页 / 共13页
在Java程序中截获控制台输出.docx_第4页
第4页 / 共13页
在Java程序中截获控制台输出.docx_第5页
第5页 / 共13页
点击查看更多>>
下载资源
资源描述

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

《在Java程序中截获控制台输出.docx》由会员分享,可在线阅读,更多相关《在Java程序中截获控制台输出.docx(13页珍藏版)》请在冰豆网上搜索。

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

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

在程序中截获控制台输出

在开发中,控制台输出仍是一个重要的工具,但默认的控制台输出有着各种各样的局限。

本文介绍如何用管道流截取控制台输出,分析管道流应用中应该注意的问题,提供了截取程序和非程序控制台输出的实例。

  即使在图形用户界面占统治地位的今天,控制台输出仍旧在程序中占有重要地位。

控制台不仅是程序默认的堆栈跟踪和错误信息输出窗口,而且还是一种实用的调试工具(特别是对习惯于使用()的人来说)。

然而,控制台窗口有着许多局限。

例如在平台上,控制台只能容纳行输出。

如果程序一次性向控制台输出大量内容,要查看这些内容就很困难了。

  对于使用这个启动程序的开发者来说,控制台窗口尤其宝贵。

因为用启动程序时,根本不会有控制台窗口出现。

如果程序遇到了问题并抛出异常,根本无法查看运行时环境写入到或的调用堆栈跟踪信息。

为了捕获堆栈信息,一些人采取了用()块封装()的方式,但这种方式不一定总是有效,在运行时的某些时刻,一些描述性错误信息会在抛出异常之前被写入和;除非能够监测这两个控制台流,否则这些信息就无法看到。

  因此,有些时候检查运行时环境(或第三方程序)写入到控制台流的数据并采取合适的操作是十分必要的。

本文讨论的主题之一就是创建这样一个输入流,从这个输入流中可以读入以前写入控制台流(或任何其他程序的输出流)的数据。

我们可以想象写入到输出流的数据立即以输入的形式“回流”到了程序。

  本文的目标是设计一个基于的文本窗口显示控制台输出。

在此期间,我们还将讨论一些和管道流(和)有关的重要注意事项。

图一显示了用来截取和显示控制台文本输出的程序,用户界面的核心是一个。

最后,我们还要创建一个能够捕获和显示其他程序(可以是非的程序)控制台输出的简单程序。

  一、管道流

  要在文本框中显示控制台输出,我们必须用某种方法“截取”控制台流。

换句话说,我们要有一种高效地读取写入到和所有内容的方法。

如果你熟悉的管道流和,就会相信我们已经拥有最有效的工具。

  写入到输出流的数据可以从对应的输入流读取。

的管道流极大地方便了我们截取控制台输出。

显示了一种非常简单的截取控制台输出方案。

【:

用管道流截取控制台输出】

();

();

{

();

}

(){

("连接失败");

();

}

();

();

();

  可以看到,这里的代码极其简单。

我们只是建立了一个,把它设置为所有写入控制台流的数据的最终目的地。

所有写入到控制台流的数据都被转到,这样,从相应的读取就可以迅速地截获所有写入控制台流的数据。

接下来的事情似乎只剩下在中显示从流读取的数据,得到一个能够在文本框中显示控制台输出的程序。

遗憾的是,在使用管道流时有一些重要的注意事项。

只有认真对待所有这些注意事项才能保证的代码稳定地运行。

下面我们来看第一个注意事项。

  注意事项一

  运用的是一个字节固定大小的循环缓冲区。

写入的数据实际上保存到对应的的内部缓冲区。

从执行读操作时,读取的数据实际上来自这个内部缓冲区。

如果对应的输入缓冲区已满,任何企图写入的线程都将被阻塞。

而且这个写操作线程将一直阻塞,直至出现读取的操作从缓冲区删除数据。

  这意味着,向写数据的线程不应该是负责从对应读取数据的唯一线程。

假设线程是负责从读取数据的唯一线程;另外,假定企图在一次对的()方法的调用中向对应的写入字节的数据。

在线程阻塞之前,它最多能够写入字节的数据(内部缓冲区的大小)。

然而,一旦被阻塞,读取的操作就再也不会出现,因为是唯一读取的线程。

这样,线程已经完全被阻塞,同时,所有其他试图向写入数据的线程也将遇到同样的情形。

  这并不意味着在一次()调用中不能写入多于字节的数据。

但应当保证,在写入数据的同时,有另一个线程从读取数据。

  示范了这个问题。

这个程序用一个线程交替地读取和写入。

每次调用()向的缓冲区写入字节,每次调用()只从缓冲区读取并删除个字节。

内部缓冲区最终会被写满,导致写操作阻塞。

由于我们用同一个线程执行读、写操作,一旦写操作被阻塞,就不能再从读取数据。

【:

用同一个线程执行读写操作导致线程阻塞】

.*;

{

();

();

([]){

{

();

}

(){

("连接失败");

();

}

[][];

[][];

;

{

向发送字节数据

(,,);

("已发送字节...");

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

发送字节

(,,);

;

(){

(,,);

("已发送字节...");

;

(,,);

}

}

(){

("读取时出现错误:

");

();

}

}()

}

  只要把读写操作分开到不同的线程,的问题就可以轻松地解决。

是经过修改后的版本,它在一个单独的线程中执行写入的操作(和读取线程不同的线程)。

为证明一次写入的数据可以超过字节,我们让写操作线程每次调用的()方法时写入字节。

那么,在()方法中创建的线程是否会阻塞呢?

按照运行时线程调度机制,它当然会阻塞。

写操作在阻塞之前实际上最多只能写入字节的有效载荷(即缓冲区的大小)。

但这并不会成为问题,因为主线程()很快就会从的循环缓冲区读取数据,空出缓冲区空间。

最终,写操作线程会从上一次中止的地方重新开始,写入字节有效载荷中的剩余部分。

【:

把读写操作分开到不同的线程】

.*;

{

();

();

([]){

{

();

}

(){

("连接失败");

();

}

[][];

;

启动写操作线程

();

{

(,,);

(){

("已经读取"

"字节...");

(,,);

}

}

(){

("读取输入错误.");

();

}

}()

创建一个独立的线程

执行写入的操作

(){

((){

(){

[][];

(){无终止条件的循环

{

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

(,,);

}

(){

("写操作错误");

();

}

("已经发送字节...");

}

}

})();

}()

}

  也许我们不能说这个问题是管道流设计上的缺陷,但在应用管道流时,它是一个必须密切注意的问题。

下面我们来看看第二个更重要(更危险的)问题。

  注意事项二

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

   .试图从读取数据,

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

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

  这是一个很微妙的时刻,同时也是一个极其重要的时刻。

假定有一个线程向写入数据;另一个线程从对应的读取数据。

下面一系列的事件将导致线程在试图读取时遇到异常:

   .向写入数据。

   .结束(()返回)。

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

   .试图再次从读取数据。

这时的缓冲区已经为空,而且已经结束,从而导致在读操作执行时出现异常。

  构造一个程序示范这个问题并不困难,只需从的()方法中,删除()条件。

这个改动阻止了执行写操作的方法循环执行,使得执行写操作的方法在一次写入操作之后就结束运行。

如前所述,此时主线程试图读取时,就会遇到一个异常。

  这是一种比较少见的情况,而且不存在直接修正它的方法。

请不要通过从管道流派生子类的方法修正该问题――在这里使用继承是完全不合适的。

而且,如果以后改变了管道流的实现方法,现在所作的修改将不再有效。

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

  注意事项三

  如果一个写操作在上执行,同时最近从对应读取的线程已经不再活动(通过()检测),则写操作将抛出一个异常。

假定有两个线程和,向写入数据,而则从对应的读取。

下面一系列的事件将导致线程在试图写入时遇到异常:

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

   .向写入数据。

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

   .线程结束。

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

  实际上,这个问题不象第二个问题那样棘手。

和多个读线程单个写线程的情况相比,也许在应用中有一个读线程(作为响应请求的服务器)和多个写线程(发出请求)的情况更为常见。

  解决问题

  要防止管道流前两个局限所带来的问题,方法之一是用一个作为代理或替代。

显示了一个类,它用一个提供和管道流类似的功能,但不会出现死锁和异常。

这个类的内部仍旧使用管道流,但隔离了本文介绍的前两个问题。

我们先来看看这个类的公用方法。

构造函数很简单,它连接管道流,然后调用()方法(稍后再讨论该方法)。

()方法返回一个(具体地说,是一个)用以替代。

写入该的数据最终将在()方法返回的流中作为输入出现。

和使用的情形不同,向写入数据的线程的激活、写数据、结束不会带来负面效果。

【:

防止管道流应用中出现的常见问题】

.*;

{

  

    ();

  ;

  

    (){

    (){

      ;

      {

        ();

        ();

      }

      (){

        记录错误或其他处理

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

        ();

      }

    }

  };

  (){

    (){

      ;

        {

        ();

      }

      (){

        记录错误或其他处理

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

        ();

      }

    }

  };

  (){

    ();

    ();

  }()

  (){

    ;

  }()

  (){

    ;

  }()

  (){

    ((){

      (){

        (){

          检查流里面的字节数

          (()>){

            [];

            (){

              ();

              ();清除缓冲区

            }

            {

              把提取到的数据发送给

              (,,);

            }

            (){

              记录错误或其他处理

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

              ();

            }

          }

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

            {

              每隔秒查看检查新数据

              ();

            }

            (){}

          }

      }

    })();

  }()

}

()方法是整个类真正的关键所在。

这个方法的目标很简单,就是创建一个定期地检查缓冲区的线程。

缓冲区中找到的所有数据都被提取到一个数组,然后写入到。

由于对应的由()返回,从该输入流读取数据的线程都将读取到原先发送给的数据。

前面提到,类解决了管道流存在的前二个问题,我们来看看这是如何实现的。

  具有根据需要扩展其内部缓冲区的能力。

由于存在“完全缓冲”,线程向()返回的流写入数据时不会被阻塞。

因而,第一个问题不会再给我们带来麻烦。

另外还要顺便说一句,的缓冲区永远不会缩减。

例如,假设在能够提取数据之前,有一块的数据被写入到流,缓冲区将永远保持至少的容量。

如果这个类有一个方法能够在数据被提取之后修正缓冲区的大小,它就会更完善。

  第二个问题得以解决的原因在于,实际上任何时候只有一个线程向写入数据,这个线程就是由()创建的线程。

由于这个线程完全由类控制,我们不必担心它会产生异常。

  类还有一些细节值得提及。

首先,我们可以看到和实际上分别是和的派生类的实例,也即在它们的()方法中加入了特殊的行为。

如果一个对象的用户关闭了输入或输出流,在()中创建的线程必须关闭。

覆盖后的()方法把标记设置成以关闭线程。

另外,请注意()中的同步块。

要确保在()调用和()调用之间缓冲区不被写入流的线程修改,这是必不可少的。

由于的()方法的所有版本都在该流上同步,我们保证了的内部缓冲区不被意外地修改。

  注意类并不涉及管道流的第三个问题。

该类的()方法返回。

如果一个线程从该流读取,一段时间后终止,下次数据从缓冲区传输到时就会出现异常。

  二、捕获控制台输出

  的类扩展捕获控制台输出。

不要对这个类有这么多代码感到惊讶,必须指出的是,类有超过的代码用来进行测试。

  【:

截获控制台输出】

.*;

.*;

.*;

.*;

{

  ([]){

    (;<;)

      ([]);

  }()

  (){

    ();

    重定向和

    (());

    ();

    ();

    (());

  }()

  (

    ){

    

      (());

    ((){

      (){

        ();

        {

          ;

          ();

          ((())){

            ;

            ()()?

              :

;

            ();

            (()('\')());

            ()

              (());

          }

        }

        (){

          (,

            "从读取错误:

");

          ();

        }

      }

    })();

  }()

  该类剩余部分的功能是进行测试

  ([]){

    ("测试");

    ;

    {

      ();

    }

    (){

      (

        "不能创建:

");

      ();

    }

    ((""));

    ()((),

      );

    (,,,);

    ();

    ((){

      (

        ){

        ();

      }

    });

    启动几个写操作线程向

    和输出

    (

      "写操作线程",,,);

    (

      "写操作线程",,,);

    (

      "写操作线程",,,);

    (

      "写操作线程",,,);

    (

      "写操作线程",,,  );

  }()

  (

    ,,

    ,){

    ((){

      (){

        (;<;){

          ("***"",!

");

          {

            ();

          }

          (){}

        }

      }

    })();

  }()

}

()方法创建了一个,包含一个的实例。

这些代码并没有什么特别之处。

显示出来之后,()方法启动一系列的写操作线程,写操作线程向控制台流输出大量信息。

捕获并显示这些信息,如图一所示。

  提供了两个构造函数。

没有参数的构造函数用来捕获和显示所有写入到控制台流的数据,有一个[]参数的构造函数转发所有从各个数组元素读取的数据到。

稍后将有一个例子显示这个构造函数的用处。

首先我们来看看没有参数的构造函数。

这个函数首先创建一个对象;然后请求运行时环境把控制台输出转发到提供的;最后,构造函数调用(),创建一个不断地把文本行追加到的线程。

注意,把文本追加到之后,程序小心地保证了插入点的正确位置。

  一般来说,部件的更新不应该在事件分派线程(,)之外进行。

对于本例来说,这意味着所有把文本追加到的操作应该在中进行,而不是在()方法创建的线程中进行。

然而,事实上在中向追加文本是一个线程安全的操作。

读取一行文本之后,我们只需调用()就可以把文本追加到的末尾。

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

  在中捕获程序自己的控制台输出是一回事,去捕获其他程序(甚至包括一些非程序)的控制台数据又是另一回事。

提供了捕获其他应用的输出时需要的基础功能,的利用,截取其他应用的输出信息然后显示在中。

【:

截获其他程序的控制台输出】

.*;

.*;

.*;

.*;

{

    ;

    ([]){

        (){

      ("用法:

"

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

      ();

        }

        {

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

            ()();

        }

        (){

            ("创建进程时出错...\");

            ();

        }

        获得新进程所写入的流

        []

            []{

          ()()};

        

  ();

        ((""));

        ([]

            "控制台输出");

        ()((),

          );

        (,,,);

        ();

        ((){

            (){

                ();

                {

                    ();在下可能被挂起

                }

                (){}

                    ();

                }

            });

    }()

}

  的工作过程如下:

首先利用()方法启动指定程序的一个新进程。

启动新进程之后,从结果对象得到它的控制台流。

之后,把这些控制台流传入([])构造函数(这就是带参数构造函数的用处)。

使用时,在命令行上指定待截取其输出的程序名字。

例如,如果在下执行,则结果如图四所示。

使用时应该注意,被截取输出的应用程序最初输出的一些文本可能无法截取。

因为在调用()和初始化完成之间存在一小段时间差。

在这个时间差内,应用程序输出的文本会丢失。

当窗口被关闭,()调用试图关闭程序开始时创建的进程。

测试结果显示出,()方法不一定总是有效(至少在上是这样的)。

似乎当待关闭的进程启动了额外的进程时,则那些进程不会被关闭。

此外,在这种情况下程序看起来未能正常结束。

但在下,一切正常。

如果用运行,关闭窗口时会出现一个。

这是一个的,和下就不会出现问题。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 人文社科 > 设计艺术

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1