第11章 异常、断言、日志和调试
本章内容:
* 处理错误
* 捕获异常
* 使用异常机制的技巧
* 使用断言
* 日志
* 调试技巧
* GUI程序排错技巧
* 使用调试器
11.1 处理错误
- 如果由于出现错误而使得某些操作没有完成,程序应该:
- 返回到一种安全状态,并能够让用户执行一些其他的命令;
- 允许用户保存所有操作的结果,并以适当的方式终止程序。
- 异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。为了能够在程序中处理异常情况,必须研究程序中可能会出现的错误和问题,以及哪类问题需要关注。
(1) 用户输入错误
(2) 设备错误
(3) 物理限制
(4) 代码错误
11.1.1 异常分类
- 在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例。
- 需要注意的是,所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception。
- Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止之外。再也无能为力了。这种情况很少出现。
- Exception层次结构分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。规划两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
- 派生于RuntimeExceotion的异常包含下面几种情况:
- 错误的类型转换。
- 数组访问越界。
- 访问空指针。
- 不是派生于RuntimeException的异常包括:
- 试图在文件尾部后面读取数据。
- 试图打开一个不存在的文件。
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
- Java语言规范将派生于Error类或RuntimeException类的所有异常称为未检查(unchecked)异常,所以其他的异常称为已检查(checked)异常。
11.1.2 声明已检查异常
- 如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。这个道理很简单:一个方法不仅仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。
- 方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类已检查异常。
- 在自己编写方法时,不必将所有可能抛出的异常都进行声明。至于什么时候需要在方法中用throws子句声明异常,什么异常必须使用throws子句声明,需要记住在遇到下面4种情况时应该抛出异常:
(1)调用一个抛出已检查异常的方法。例如,FileInputStream构造器。
(2)程序运行过程中发现错误,并且利用throw语句抛出一个已检查异常。
(3)程序出现错误,例如,a[-1]=0会抛出一个ArrayIndexOutOfBoundsException这样的未检查异常。
(4)Java虚拟机和运行时库出现的内部错误。
如果出现前两种情况之一,则必须告诉调用这个方法的程序员有可能抛出异常。因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前执行的线程就会结束。 - 对于那些可能被他人使用的Java方法,应该根据异常规范(exception specification),在方法的首部声明这个方法可能抛出的异常。
- 如果一个方法有可能抛出多个已检查异常,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。但是,不需要声明Java的内部错误,即从Error继承的错误。任何程序代码都具有抛出那些异常的潜能,而我们对其没有任何控制能力。同样,也不应该声明从RuntimeException继承的那些未检查异常。
- 一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。如果方法没有声明所有可能发生的已检查异常,编译器就会给出一个错误信息。
- 如果在子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能比超类方法中声明的异常更通用(也就是说,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常)。特别需要说明的是,如果超类方法没有抛出任何已检查异常,子类也不能抛出任何已检查异常。
- 如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常,或者这个类的任意一个子类的异常。
11.1.3 如何抛出异常
- EOFException异常描述的是“在输入过程中,遇到了一个未预期的EOF后的信号”。
- 对于一个已经存在的异常类,将其抛出非常容易。在这种情况下:
(1)找到一个合适的异常类。
(2)创建这个类的一个对象。
(3)将对象抛出。
一旦方法抛出了异常,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。
11.1.4 创建异常类
- java.lang.Throwable 1.0
- Throwable()
构造一个新的Throwable对象,这个对象没有详细的描述信息。 - Throwable(String message)
构造一个新的throwable对象,这个对象带有特定的详细描述信息。习惯上,所有派生的异常类都支持一个默认的构造器和一个带有详细描述信息的构造器。 - String getMessage()
获取Throwable对象的详细描述信息。
- Throwable()
11.2 捕获异常
- 如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。对于图形界面程序(applet和application应用程序),在捕获异常之后,就会打印出堆栈的信息,但程序将返回到用户界面的处理循环中(在调试基于图形界面的程序时,最好保证控制台窗口可见,并且没有被最小化)。
- 如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么
(1)程序将跳过try语句块的其他代码。
(2)程序将执行catch子句中的处理器代码。 - 如果在try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
- 如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立即退出。
- 编译器严格得执行throws说明符。如果调用了一个抛出已检查异常的方法,就必须对它进行处理,或者将它继续进行传递。
- 如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个已检查异常。不允许在子类的throws说明符中出现超过超类方法所列出的异常类的范围。
11.2.1 捕获多个异常
- 在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。
- 异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息,可以试着使用e.getMessage()得到详细的错误信息(如果有的话),或者使用e.getClass().getName()得到异常对象的实际类型。
- 在Java SE 7中,同一个catch子句中可以捕获多个异常类型。
- 捕获多个异常时,异常变量隐含为final变量。
- 捕获多个异常不仅会让代码看起来更简单,还会更高效。生成的字节码只包含一个对应公共catch子句的代码块。
11.2.2 再次抛出异常与异常链
- 在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。
11.2.3 finally子句
- 当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题。一种解决方法是捕获并重新抛出所以的异常。一种更好的解决方案,就是final子句。
- 不管是否有异常被捕获,finally子句中的代码都被执行。
- try语句可以只有finally子句,而没有catch子句。
- 当finally子句包含return语句时,将会出现一种意想不到的结果。假设利用return语句从try语句块中退出。在方法返回前,finally子句的内容将被执行。如果finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。
11.2.4 带资源的try语句
- 带资源的try语句(try-with-resources)的最简形式为:
try(Resource res = ...){work with res}
,try块退出时,会自动调用res.close()。 - 如果try块抛出一个异常,而且close方法也抛出一个异常,这就会带来一个难题。戴子渊的try语句可以很好地处理这种情况。原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常。如果对这些异常感兴趣,可以调用getSuppressed方法,它会得到从close方法抛出并被抑制的异常列表。
- 带资源的try语句自身也可以有catch子句和一个finally子句。这些子句会在关闭资源之后执行。
11.2.5 分析堆栈跟踪元素
- 堆栈跟踪(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。当Java程序正常终止,而没有捕获异常时,这个列表就会显示出来。
- 可以使用Throwable类的printStackTrace方法访问堆栈耿总的文本描述信息。一种更灵活的方法是使用getStackTrace方法,它会得到StackTraceElement对象的一个数组,可以在程序中分析这个对象数组。StackTraceElment类含有能够获得文件名和当前执行的代码行号的方法,同时,还含有能够获得类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获得的信息。静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈跟踪。
- java.lang.Throwable 1.0
- Throwable(Throwable cause) 1.4
- Throwable(String message,Throwable cause) 1.4
用给定的“原因”构造一个Throwable对象。 - Throwable initCause(Throwable cause) 1.4
将这个对象设置为“原因”。如果这个对象已经被设置为“原因”,则抛出一个异常。返回this引用。 - Throwable getCause() 1.4
获得设置为这个对象的“原因”的异常对象。如果没有设置“原因”,则返回null。 - StackTraceElement[] getStackTrace() 1.4
获得构造这个对象时调用堆栈的跟踪。 - void addSuppressed(Throwable t) 7
为这个异常增加一个“抑制”异常。这出现在带资源的try语句中,其中t是close方法抛出的一个异常。 - Throwable[] getSuppressed() 7
得到这个异常的所有“抑制”异常。一般来说,这些是带资源的try语句中close方法抛出的异常。
- java.lang.Exception 1.0
- Exception(Throwable cause) 1.4
- Exception(String message,Throwable cause)
用给定的“原因”构造一个异常对象。
- java.lang.RuntimeException 1.0
- RuntimeException(Throwable cause) 1.4
- RuntimeException(String message,Throwable cause) 1.4
用给定的“原因”构造一个RuntimeException对象。
- java.lang.StackTraceElement 1.4
- String getFileName()
返回这个元素运行时对应的源文件名。如果这个信息不存在,则返回null。 - int getLineNumber()
返回这个元素运行时对应的源文件行数。如果这个信息不存在,则返回-1。 - String getClassName()
返回这个元素运行时对应的类的全名。 - String getMethodName()
返回这个元素运行时对应的方法名。构造器名为;静态初始化器名是。这里无法区分同名的重载方法。 - boolean isNativeMethod()
如果这个元素运行时在一个本地方法中,则返回true。 - String toString()
如果存在的话,返回一个包含类名、方法名、文件名和行数的格式化字符串。
- String getFileName()
11.3 使用异常机制的技巧
- 使用异常机制的几个技巧:
(1)异常处理不能代替简单的测试
与执行简单的测试相比,捕获异常所花费的时间大大超过了前者,因此使用异常的基本规则是:只在异常情况下使用异常机制。
(2)不要过分地细化异常
有必要将整个任务包装在try语句块中,这样,当任何一个操作出现问题时,整个任务都可以取消。
(3)利用异常层次结构
不要只抛出RuntimeException异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获Thowable异常,否则,会使程序代码更难读、更难维护。
考虑已检查异常与未检查异常的区别。已检查异常本来就很庞大,不要为逻辑错误抛出这些异常。
将一种异常转换成另一种更加适合的异常时不要犹豫。
(4)不要压制异常
(5)在检测错误时,“苛刻”要比放任更好
在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException异常更好。
(6)不要羞于传递异常
传递异常要比捕获异常更好,让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。
11.4 使用断言
- 断言机制允许在测试期间向代码中插入一些查询语句。放代码发布时,这些插入的检测语句将会被自动地移走。
- Java语言引入了关键字assert。这个关键字有两种形式:
assert 条件;
和assert 条件:表达式;
。这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。
11.4.1 启用和禁用断言
- 在默认情况下,断言被禁用。可以在运行程序时用-enableassertions或-ea选项启用它:
java -enableassertions MyApp
。需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用断言时类加载器(class loader)的功能。当断言被禁用时,类加载器将跳过断言的代码,因此,不会降低程序运行的速度。 - 也可以在某个类或某个包中使用断言,也可以用选项-disableassertions或-da禁用某个特定类和包的断言。
- 有些类不是由类加载器加载,而是直接由虚拟机加载。可以使用这些开关有选择地启用或禁用那些类中的断言。然而,启用和禁用所有断言的-ea和-da开关不能应用到那些没有类加载器的“系统类”上。对于这些系统类来说,需要使用-enablesystemassertions/-esa开关启用断言。
11.4.2 使用断言完成参数检查
- 在Java语言中,给出了3种处理系统错误的机制:
- 抛出一个异常。
- 日志。
- 使用断言。
- 什么时候应该选择使用断言呢?请记住下面几点:
- 断言失败是致命的、不可恢复的错误。
- 断言检查只用于开发和测试阶段。
- 不应该使用断言向程序的其他部分通告发生了可恢复的错误,或者,不应该作为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。
11.4.3 为文档假设使用断言
- 断言时一种测试和调试阶段所使用的战术性工具;而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。
- java.lang.ClassLoader 1.0
- void setDefaultAssertionStatus(boolean b) 1.4
对于通过类加载器加载的所有类来说,如果没有显式地说明类或包的断言状态,就启用或禁用断言。 - void setClassAssertionStatus(String className,boolean b) 1.4
对于给定的类和它的内部类,启用或禁用断言。 - void setPackageAssertionAstatus(String packageName,boolean b)1.4
对于给定包和其子包中的所有类,启用或禁用断言。 - void clearAssertionStatus() 1.4
移去所有类和包的显式断言状态设置,并禁用所有通过这个类加载器加载的类的断言。
- void setDefaultAssertionStatus(boolean b) 1.4
11.5 记录日志
- 每个Java程序员都很熟悉在有问题的代码中插入一些System.out.println方法调用来帮助程序运行的操作过程。当然,一旦发现问题的根源,就要将这些语句从代码中删去。如果接下来又出现了问题,就需要再插入几个调用System.out.println方法的语句。记录日志API就是为了解决这个问题而设计的。下面先讨论一下这些API的优点。
- 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
- 可以很简单地禁止日志记录的输出,因此,将这些日志代码留在程序中的开销很小。
- 日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等。
- 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器指定的标准丢弃那些不用的记录项。
- 日志记录可以采用不同的方式格式化,例如,纯文本或XML。
- 应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,例如,com.mycompany.myapp。
- 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置。
11.5.1 基本日志
- 日志系统管理着一个名为Logger.global的默认日志记录器,可以用System.out替换它,并通过调用info方法记录日志信息。
- 在修正bug 7184195之前,还需要调用Logger.ghetGlobal().setLevel(Level.INFO)来激活全局日志记录器。
11.5.2 高级日志
- 调用getLogger方法可以创建或检索记录器。
- 与包名类似,日志记录器名也具有层次结构。事实上,与包名相比,日志记录器的层次性更强。对于包来说,一个包的名字与其父包的名字之间没有语义关系,但是日志记录器的父与子之间将共享某些属性。
- 通常,有以下7个日志记录器级别:SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST。在默认情况下,只记录前三个级别。也可以设置其他的级别。
- 还可以使用Level.ALL开启所有级别的记录,或者使用Level.OFF关闭所有级别的记录。
- 默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。但是,如果虚拟机对执行过程做了优化,就得不到准确地调用信息。此时,可以调用logp方法获得调用类和方法的确切位置。
- 记录日志的常见用途是记录那些不可预料的异常。
- 调用throwing可以记录一条FINER级别的记录和一条以THROW开始的信息。
11.5.3 修改日志管理器配置
- 可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于:
e/lib/logging.properties
。要想使用另一个配置文件,就要将java.util.logging.config.file特性设置为配置文件的存储位置,并用下列命令启动应用程序:java -Djava.utll.logging.config.file=configFile MainClass
。 - 要想修改默认的日志记录级别,就需要编辑配置文件,并修改以下命令行
.level=INFO
。可以通过添加以下内容来指定自己的日志记录级别com.mycompany.myapp.level=FINE
。也就是说,在日志记录名后面添加后缀.level。要想在控制台上看到FINE级别的消息,就需要进行下列设置java.util.logging.ConsoleHandler.level=FINE
。 - 在日志管理器配置的属性设置不是系统属性,因此,用-Dcom.myconpany.myapp.level=FINE启动程序不会被日志记录产生任何影响。
- 日志属性文件由java.util.logging.LogManager类处理。可以通过将java.util.logging.manager系统属性设置为某个子类的名字来指定一个不同的日志管理器。另外,在保存标准日志管理器的同时,还可以从日志属性文件跳过初始化。还有一种方式是将java.util.logging.config.class系统属性设置为某个类名,该类再通过其他方式设定日志管理器属性。
- 在运行的程序中,使用jconsole程序也可以改变日志记录的级别。
11.5.4 本地化
- 本地化的应用程序包含资源包(resource bundle)中的本地特定信息。资源包由各个地区的映射集合组成。
- 一个程序可以包含多个资源包,一个用于菜单;其他用于日志消息。每个资源包都有一个名字。要想将映射添加到一个资源包中,需要为每个地区创建一个文件。英文消息映射位于com/mycompany/logmessages_en.properties文件中。德文消息映射位于com/mycompany/logmessages_de.properties文件中。(en和de是语言编码)。可以将这些文件与应用程序的类文件放在一起,以便ResourceBundle类自动地对它们进行定位。
11.5.5 处理器
1.在默认情况下,日志记录器将记录发送到ConsoleHandler中,并由它输出到System,err流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器(命名为“”)有一个ConsoleHandler。
- 与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处理器的日志记录级别为
java.util.logging.ConsoleHandler.level=INFO
。 - 要想记录FINE级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。另外,还可以绕过配置文件,安装自己的处理器。
- 在默认情况下,日志记录器将记录发送到自己的处理器和父处理器。我们的日志记录器是原始日志记录器(命名为“”)的子类,而原始日志记录器将会把所有等于或高于INFO级别的揭露发送到控制台。然而,我们并不想两次看到这些记录。鉴于这个原因,应该将useParentHandlers属性设置为false。
- 要想将日志记录发送到其他地方,就要添加其他的处理器。日志API为此提供了两个有用的处理器,一个是FileHandler;另一个是SocketHandler。SocketHandler将记录发送到特定的主机和端口。而更令人感兴趣的是FileHandler,它可以收集文件中的记录。
- 可以通过设置日志管理器配置文件中的不同参数,或者利用其他的构造器来修改文件处理器的默认行为。
- 也有可能不想使用默认的日志记录文件名,因此,应该使用另一种模式,例如,%h/myapp.log。
- 如果多个应用程序(或者同一个应用程序的多个副本)使用同一个日志文件,就应该开启append标志。另外,应该在文件名模式中使用%u,以便每个应用程序创建日志的唯一副本。
- 开启文件循环功能也是一个不错的注意。日志文件以myapp.log.0,myapp.log.1,myapp.log.2,这种循环序列的形式出现。只要文件超出了大小限制,最旧的文件就会被删除,其他的文件将重新命名,同时创建一个新文件,编号为0。
- 文件处理器配置参数
配置属性 描述 默认 java.util.logging.FileHandler.level 处理器级别 Level.ALL java.util.FileHandler.append 控制处理器应该追加到一个已经存在的文件尾部;还是应该为每个运行的程序打开一个新文件 false java.util.logging.FileHandler.limit 在打开另一个文件之前允许写入一个文件的近似最大字节数(0表示无限制) 在FileHandler类中为0(表示无限制);在默认的日志管理器配置文件中为50000 java.util.logging.FileHandler.pattern 日志文件名的模式。 %h/java%u.log java.util.logging.FileHandler.count 在循环序列中的日志记录数量 1(不循环) java.util.logging.FileHandler.filter 使用的过滤器类 没有使用过滤器 java.util.logging.FileHandler.encoding 使用的字符编码 平台的编码 java.util.logging.FileHandler.formatter 记录格式器 java.util.logging.XMLFormatter - 日志记录文件模式变量
变量 描述 %h 系统属性user.home的值 %t 系统临时目录 %u 用于解决冲突的唯一性编号 %g 为循环日志记录生成的数值。(当使用循环功能且模式不包括%g时,使用后缀%g) %% %字符 - 还可以通过扩展Handler类或StreamHandler类自定义处理器。
11.5.6 过滤器
- 在默认情况下,过滤器根据日志的级别进行过滤。每个日志记录器和处理器都可以有一个可选的过滤器啦完成附加的过滤。另外,可以通过实现Filter接口并定义下列发放来自定义过滤器。
-
boolean isLoggable(LogRecord record)
在这个方法中,可以利用自己喜欢的标准,对日志记录进行分析,返回true表示这些记录应该包含在日志中。 - 要想将一个过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法就可以了。注意,同一时刻最多只能有一个过滤器。
11.5.7 格式化器
- ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。但是,也可以自定义格式。这需要扩展Formatter类并覆盖下面这个方法:
String format(LogRecord record)
。 - 可以根据自己的愿望对记录中的信息进行格式化,并返回结果字符串。在format方法中,有可能会调用下面这个方法
String formatMessage(LogRecord record)
,这个方法对记录中的部分信息进行格式化、参数替换和本地化应用操作。 - 很多文件格式(如XML)需要已格式化的记录的前后加上一个头部和尾部。在这个例子中,要覆盖下面两个方法:
String getHead(Handler h)
和String getTail(Handler h)
。最后,调用setFormatter方法将格式化器安装到处理器中。
11.5.8 日志记录说明
-
一些最常用的操作:
(1)为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名与应用程序包一样的名字,例如,com.mycompany.myprog,这个一种好的编程习惯。另外,可以通过调用下列方法得到日志记录器。Logger logger = Logger.getLogger("com.mycompany.myprog");
。为了方便起见,可能希望利用一些日志操作将下面的静态域添加到类中:private static final Logger logger = Logger.getLogger("com.mycompany.myprog");
。
(2)默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台。用户可以覆盖默认的配置文件。但是正如前面所述,改变配置需要做相当多的工作。因此,最好在应用程序中安装一个更加适宜的默认配置。
下面代码确保将所有的消息记录到应用程序特定的文件中。可以将这段代码防止在应用程序的main方法中。if (System.getProperty("java.util.logging.config.class")==null && System.getProperty("java.util.logging.config.file")==null) { try{ Logger.getLogger("").setLevel(Level.ALL); final int LOG_ROTATION_COUNT = 10; Handler handler = new FileHandler("%h/myapp.log",0,LOG_ROTATION_COUNT); } catch(IOException e) { logger.log(Level.SERVERE,"Can't create log file handler",e); } }
(3)现在,可以记录自己想要的内容了。但需要牢记:所有级别为INFO、WARNIN和SEVERE的消息都将显示到控制台上。因此,最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为FINE是一个很好的选择。
当调用System.out.println时,实际上生成了下面的日志消息:logger.fine("File open dialog canceled");
记录那些不可预料的异常也是一个不错的想法。例如:try { ... } catch(SomeException e) { logger.log(Level.FINE,"explanation",e); }
-
java.util.logging.Logger 1.4
- Logger getLogger(String loggerName)
- Logger getLogger(String loggerName,String bundleName)
获得给定名字的日志记录器。如果这个日志记录器不存在,创建一个日志记录器。
参数:loggerName 具有层次结构的日志记录器名。例如,com.mycompany.myapp。 bundleName 用来查看本地消息的资源包名。 - void servere(String message)
- void warning(String message)
- void info(String message)
- void config(String message)
- void fine(String message)
- void finer(String message)
- void finest(String message)
记录一个由方法名和给定消息指示级别的日志记录。 - void entering(String className,String methodName)
- void entering(String className,String methodName,Object param)
- void entering(String className,String methodName,Object[] param)
- void exiting(String className,String methodName)
- void exiting(String className,String methodName,Object result)
记录一个描述进入/退出方法的日志记录,其中应该包括给定参数和返回值。 - void throwing(String className,String methodName,Throwable t)
记录一个描述抛出给定异常对象的日志记录。 - void log(Level level,String message)
- void log(Level level,String message,Object obj)
- void log(Level level,String message,Object[] objs)
- void log(Level level,String message,Throwable t)
记录一个给定级别和消息的日志记录,其中可以包括对象或者可抛出对象。要想包括对象,消息中必须包含格式化占位符{0}、{1}等。 - void logp(Level level,String className,String methodName,String message)
- void logp(Level level,String className,String methodName,String message,Object obj)
- void logp(Level level,String className,String methodName,String message,Object[] objs)
- void logp(Level level,String className,String methodName,String message,Throwable t)
记录一个给定级别、准确地调用者信息和消息的日志记录,其中可以包括对象或可抛出对象。 - void logb(Level level,String className,String methodName,String bundleName,String message)
- void logb(Level level,String className,String methodName,String bundleName,String message,Object obj)
- void logb(Level level,String className,String methodName,String bundleName,String message,Object[] objs)
- void logb(Level level,String className,String methodName,String bundleName,String message,Throwable t)
记录一个给定级别、准确地调用者信息、资源包名和消息的日志记录,其中可以包括:对象或可抛出对象。 - Level getLevel()
- void setLevel(Level l)
获得或设置这个日志记录器的级别。 - Logger getParent()
- void setParent(Logger l)
获得和设置这个日志记录器的父日志记录器。 - Handler[] getHandlers()
获得这个日志记录器的所有处理器。 - void addHandler(Handler h)
- void removeHandler(Handler h)
增加或删除这个日志记录器中的一个处理器。 - boolean getUseParentHandlers()
- void setUseParentHandlers(boolean b)
获得和设置”use parent handler”属性。如果这个属性是true,则日志记录器会将全部的日志记录转发给它的父处理器。 - Filter getFilter()
- void setFilter(Filter f)
获得和设置这个日志记录器的过滤器。
- java.util.logging.Handler 1.4
- abstract void publish(LogRecord record)
将日志记录发送到希望的目的地。 - abstract void flush()
刷新所有已缓冲的数据。 - abstract void close()
刷新所有已缓冲的数据,并释放所有相关的资源。 - Filter getFilter()
- void setFilter(Filter f)
获得和设置这个处理器的过滤器。 - Formatter getFormatter()
- void setFormatter(Formatter f)
获得和设置这个处理器的格式化器。 - Level getLevel()
- void setLevel(Level l)
获得和设置这个处理器的级别。
- abstract void publish(LogRecord record)
- java.util.logging.ConsoleHandler 1.4
- ConsoleHandler()
构造一个新的控制台处理器。
- ConsoleHandler()
- java.util.logging.FileHandler 1.4
- FileHandler(String pattern)
- FileHandler(String pattern,boolean append)
- FileHandler(String pattern,int limit,int count)
- FileHandler(String pattern,int limit,int count,boolean append)
构造一个文件处理器。
参数:pattern 构造日志文件名的模式。 limit 在打开一个新日志文件之前,日志文件可以包含的近似最大字节数。 count 循环序列的文件数量。 append 新构造的文件处理器对象应该追加在一个已存在的日志文件尾部,则为true。
- java.util.logging.LogRecord 1.4
- Level getLevel()
获得这个日志记录的记录级别。 - String getLggerName()
获得正在记录这个日志记录的日志记录器的名字。 - ResourceBundle getresourceBundle()
- String getresourceBundleName()
获得用于本地化消息的资源包或资源包的名字。如果没有获得,则返回null。 - String getMessage()
获得本地化和格式化之前的原始消息。 - Object[] getParameters()
获得参数对象。如果没有获得,则返回null。 - Throwable getThrown()
获得被抛出的对象。如果不存在,则返回null。 - String getSourceClassName()
- String getSourceMethodName()
获得记录这个日志记录的代码区域。这个信息有可能是由日志记录代码提供的,也有可能是自动从运行时堆栈推测出来的。如果日志记录代码提供的值有误,或者运行时代码由于被优化而无法推测出确切的位置,者两个方法的返回值就有可能不准确。 - long getMillis()
获得创建时间。以毫秒为单位(从1970年开始)。 - long getSequenceNumber()
获得这个日志记录的唯一序列序号。 - int getTreadID()
获得创建这个日志记录的线程的唯一ID。这些ID是由LogRecord类分配的,并且与其他线程的ID无关。
- Level getLevel()
- java.util.logging.Filter 1.4
- boolean isLoggable(LogRecord record)
如果给定日志记录需要记录,则返回true。
- boolean isLoggable(LogRecord record)
- java.util.logging.Formatter 1.4
- abstract String format(LogRecord record)
返回对日志记录格式化后得到的字符串。 - String getHead(Handler h)
- String getTail(Handler h)
返回应该出现在包含日志记录的文档的开头和结尾的字符串。超类Formatter定义了这些方法,它们只返回空字符串。如果必要的话,可以对它们进行覆盖。 - String formatMessage(LogRecord record)
返回经过本地化和格式化后的日志记录的消息内容。
- abstract String format(LogRecord record)
11.6 调试技巧
-
调试技巧:
(1)可以用下面的方法打印或记录任意变量的值:System.out.println("x="+x);
或Logger.getGlobar().info("x="+x);
。
(2)一个不太为人所知但却非常有效的技巧是在每一个类中放置一个main方法,这样就可以对每一个类进行单元测试。public class MyClass { methods and fields ... public static void main(String[] args) { test code } }
利用这种技巧,只需要创建少量的对象,调用所有的方法,并检测每个方法是否能够正常地运行就可以了。另外,可以为每个类保留一个main方法,然后分别为每个文件调用Java虚拟机进行运行测试。在运行applet应用程序的时候,这些main方法不会被调用,而在运行应用程序的时候,Java虚拟机只调用启动类的main方法。
(3)JUnit是一个非常常见的单元测试框架,利用它可以很容易地组织几套测试用例。只要修改类,就需要运行测试。在发现bug时,需要补充一些其他的测试用例。
(4)日志代理(logging proxy)是一个子类的对象,它可以窃取方法调用,并进行日志记录,然后调用超类中的方法。
(5)利用Throwable类提供的printStackTrace方法,可以从任何一个异常对象中获得堆栈情况。
不一定要通过捕获异常来生成堆栈跟踪。只要在代码的任何位置插入下面这条语句就可以获得堆栈跟踪:Thread.dumptack()
。
(6)一般来说,堆栈跟踪显示在System.err上。也可以利用printStackTrace(PrintWriter s)方法将它发送到一个文件中。另外,如果想记录或显示堆栈跟踪,就可以采用下面的方式,将它捕获到一个字符串中:ByteArrayOutputStream out = new ByteArrayOutputStream(); new Throwable().printStackTrace(out); String description = out.toString();
(7)通常,将一个程序中的错误信息保存在一个文件中时非常有用的。然而,错误信息被发送到System.err中,而不是System.out中。因此,不能够通过运行下面的语句获取它们:
java MyProgram 2> errors.txt
,而是采用下面的方式捕获错误流:java MyProgram 2> errors.txt
,这条命令将工作在Windows shell中。
(8)让非捕获异常的堆栈跟踪出现在System.err中并不是一个很理想的方法。如果在客户端偶尔看到这些信息,则会感到迷惑,并且在需要的时候也无法实现诊断目的。比较好的方式是将这些内容记录到一个文件中。可以调用静态的Thread.setDefaultUncaughtExceptionHandler方法改变非捕获异常的处理器:Thread.setDefaultUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t,Throwable t) { save information in log file } } )
(9)要想观察类的加载过程,可以用-verbose标志启动Java虚拟机。
(10)Xlint选项告诉编辑器对一些普通容易出现的代码进行检查。例如,如果使用下面这条命令编译:javac -Xlint:fallthrough
,当switch语句中缺少break语句时,编译器就会给出报告。
下面列出了可以使用的选项:
-Xlint或-Xlint:all 执行所有的检查
-Xlint:deprecation 与-deprecation一样,检查废弃的方法
-Xlint:fallthrough 检查switch语句中是否缺少break语句
-Xlint:finally 警告finally子句不能正常地执行
-Xlint:none 不执行任何检查
-Xlint:path 检查类路径和源代码路径上的所有目录是否存在
-Xlint:serial 警告没有serialVersionUID的串行化类
-Xlint:unchecked 对通用类型与原始类型之间的危险转换给予警告
(11)Java虚拟机增加了对Java应用程序进行监控(monitoring)和管理(management)的支持。它允许利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。这个功能对于像应用程序服务器这样大型的、长时间运行的Java程序来说特别重要。
(12)可以使用jmap实用工具获得一个堆的转储,其中显示了堆中的每个对象。使用命令如下:jmap -dump:format=b,file=dumpFileName processID jhat dumpFileName
然后,通过浏览器进入localhost:7000,将会运行一个网络应用程序,借此探查转储对象时堆的内容。
(13)如果使用-Xprof标志运行Java虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。剖析信息将发送给System.out。输出结果中还会显示哪些方法是由即时编译器编译的。 -
编译器的-X选项并没有正式支持,而且在有些JDK版本中并不存在这个选项。可以运行命令java -X得到所有非标准选项的列表。
11.7 GUI程序排错技巧
- GUI变成的一些特定调试技巧:
(1)如果查看Swing窗口,想知道设计者如何将所有这些组件排列得如此整齐,可以监视其内容、按下Ctrl+Shift+F1,会按照层次结构打印所有组件的信息。
(2)如果定义了你自己的定制Swing组件,但它看起来不能正确显示,对此有一个很好的工具:Swing图形调试工具(Swing graphics debugger)。即使你并没有编写自己的组件类,能看到组件的内容如何绘制也很有意义,而且很有趣。要对一个Swing组件进行调试,可以使用JComponent类的setDebugGraphicsOptions方法。有以下可用选项:
DebugGraphics.FLASH_OPTION 绘制各线段、矩形和文本之前,用红色闪烁显示。
DebugGraphics.LOG_OPTION 为各个绘制操作分别打印一个消息。
DebugGraphics.BUFFERED_OPTION 显示在屏幕外缓冲区上完成的操作。
DebugGraphics.NONE_OPTION 关闭图形调试。
要让闪烁选项工作,必须禁用“双重缓冲”(Swing采用这种策略来缓解更新窗口时的屏幕抖动现象)。要打开闪烁选项,可以使用下面“魔咒”:
主需要将这几行代码放在框架构造函数的最后。程序运行时,就会看到内容窗格将缓慢地填充。或者,如果要完成更多本地化调试,只需要对单个组件调用setDebugGraphicsOptions。要控制闪烁,可以设置持续时间、次数和闪烁颜色。RepaintManager.currentManager(getRootPane()).setDoubleBufferingEnabled(false); ((JComponent)getContentPane()).setDebugGraphicsOptions(DebugFraphics.FLASH_OPTION);
(3)如果想得到GUI应用中生成的各个AWT事件的记录,可以在发出事件的各个组件中安装有个监听器。 - 使用AWT的Robot类
Robot类可以将按钮和点击鼠标的事件发送给AWT程序,并能够对用户界面进行自动地检测。
要想获得Robot对象,首先要得到一个GraphicsDevice对象。
需要在单独的线程中运行robot。
Robot类自身并不适用于进行用户界面的测试。实际上,它是用于构建基本测试工具的基础构件。一个专业的测试工具应该具有捕获、存储和再现用户交互场景的功能,并能够确定组件在屏幕中的位置,以调整鼠标点击位置。 - java.awt.GraphicsEnvironment 1.2
- static GraphicsEnvironment getLocalGraphicsEnvironment()
返回本地的图形环境。 - GraphicsDevice getDefaultScreenDevice()
返回默认的屏幕设备。需要注意的是,使用多台视器的计算机,每一个屏幕有一个图形设备。通过调用getScreenDevices方法可以得到一个保存所有屏幕设备的数组。
- static GraphicsEnvironment getLocalGraphicsEnvironment()
- java.awt.Robot 1.3
- Robot(GraphicsDevice device)
构造一个能够与给定设备交互的robot对象。 - void keyPress(int key)
- void keyRelease(int key)
模拟按下或释放按键。
参数:key 键码。 - void mouseMove(int x,int y)
模拟移动鼠标。
参数:x,y 用绝对像素坐标表示的鼠标位置。 - void mousePress(int eventMask)
- void mouseRelease(int eventMask)
模拟按下或释放鼠标键。
参数:eventMask 描述鼠标键的事件掩码。 - void delay(int milliseconds)
根据给定毫秒数延迟robot。 - BufferedImage createScreenCapture(Rectangle rect)
截取屏幕的一部分。
参数:rect 用绝对像素坐标表示的所截取的矩形。
- Robot(GraphicsDevice device)
11.8 使用调试器
- 可以选择菜单Run->Debug As->Java Application启动调试器。
- “Step Into”命令跟踪到每个方法调用的内部。“Step Over”命令定位到下一行,而并不跟踪到方法调用的内部。
- 要想退出调试器,需要从菜单中选择Run->Terminate。