Java核心技术1-断言、日志与调试

  |  

摘要: 本文是《Java核心技术 10th》中关于断言、日志与调试的要点总结

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文是《Java核心技术1》第10版 Chap7 中关于断言、日志与调试的要点总结。

当用户运行程序期间,可能出现程序错误、外部环境影响造成用户数据丢失等情况。此时至少应该做到以下几点:

  • 向用户通告错误
  • 保存所有的工作结果
  • 允许用户以妥善形式退出程序

本文主要涉及以下三个方面:

(1) 异常

对于异常情况,Java 用异常处理的【错误捕获机制】来处理。Java 中的异常处理与 C++ 中的异常处理很类似。

(2) 断言

测试期间需要大量的检测已验证程序的正确性,这些代码如果用复制粘贴的方式维护,就比较累了,而断言是一种相对优雅的方式。

(3) 日志

当程序出现错误时,并不总是能与用户或终端进行交互。此时需要记下问题,以备后续分析。Java 中有标准日志框架可以做这件事。

在文章 Java核心技术1-异常 中我们介绍了异常,本文我们介绍断言、日志和调试。


断言

断言机制允许测试期间想代码中插入一些检查语句,当代码发布时,这些插入的检测语句会被自动移走。

  • assert 条件: 如果结果为 false,抛出 AssertionError 异常。
  • assert 条件: 表达式: 表达式传入 AssertionError 构造器,并转换成一个消息字符串。

C++ 与 Java 关于断言的区别

C 语言中的 assert 宏将段严重的条件转换成一个字符串。当断言失败,这个字符串会被打印出来。

Java 中,条件并不会自动成为错误报告的一部分。

断言的启用与禁用

默认情况断言是禁用的。运行时用 -enableassertions-ea 选项启用。

启用或禁用断言是类加载器的功能。

也可以在某个类或真个包中使用断言,例如

1
java -ea:MyClass -ea:com.mycompany.mylib MyApp

也可以用 -disableassertions-da 选项禁用某个特定类和包的断言。

断言的适用场景

Java 中有 3 中处理系统错误的机制:

  • 抛出一个异常
  • 日志
  • 断言

其中断言的适用场景如下:

  • 断言失败是致命的,不可恢复的错误
  • 断言检查只用于开发和测试阶段

断言是一种测试和调试阶段所使用的战术性工具;而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。


日志

记录日志 API 的优点

  • 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
  • 可以很简单地禁止日志记录的输出,因此,将这些日志代码留在程序中的开销很小。
  • 日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等。
  • 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项。
  • 日志记录可以采用不同的方式格式化,例如,纯文本或 XML。
  • 应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,例如,com.mycompany.myapp。
  • 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置。

基本日志

要生成简单的日志记录,可以用了全局日志记录器,并调用 info 方法。

1
Logger.getGlobal().info("File-Open menu item selected");

默认情况下会输出以下内容:

1
2
Jun 05, 2022 2:13:25 PM logging.LoggingImageViewer main
INFO: File-Open menu item selected

在适当地方调用以下代码,此后会取消所有日志。

1
Logger.getGlobal().setLevel(Level.OFF);

高级日志

自定义日志记录器:

1
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");

注意:未被任何变量引用的日志记录器可能会被垃圾回收。为了防止这种情况发生,用一个静态变量存储日志记录器的一个引用。

日志记录器的父与子之间将共享某些属性。例如,如果对 com.mycompany 日志记录器设置了日志级别,它的子记录器也会继承这个级别。

有 7 个日志记录器级别

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

默认只记录前三个级别。也可以设置其它级别:Level.FINE,此外 Level.ALL 开启所有级别的记录,Level.OFF 关闭所有级别的记录。

记录的方法

  • logger.warning(message)
  • logger.fine(message)
  • logger.log(Level.FINE, message)

默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。但是,如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。此时,可以调用 logp 方法获得调用类和方法的确切位置,这个方法的签名为:

1
void logp(Level l, String className, String methodName, String Message)

此案外还有一些用来跟踪执行流的方法:

1
2
3
4
5
void entering(String className, String methodName);
void entering(String className, String methodName, Object param);
void entering(String className, String methodName, Object[] params);
void exiting(String className, String methodName);
void exiting(String className, String methodName, Object result);

这些调用将生成 FINER 级别和以字符串 ENTRY 和 RETURN 开始的日志记录。

例如:

1
2
3
4
5
6
int read(String file, String pattern) {
logger.entering("com.mycompany.mylib.Reader", "read"i, new Object[] {file, pattern});
...
logger.exiting("com.mycompany.mylib.Reader", "read", count);
return count;
}

记录日志的常见用途是记录那些不可预料的异常。可以使用下面两个方法提供日志记录中包含的异常描述内容。

1
2
void throwing(String className, String methodName, Throwable t)
void log(Level l, String message, Throwable t)

以下是两种典型用法:

1
2
3
4
5
6
7
8
9
10
11
if(...) {
IOException = new IOException("...");
logger.throwing("com.mycompany.mylib.Reader", "read", exception);
throw exception;
}

try {
...
} catch (IOException e) {
Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e);
}

日志管理器配置

默认情况下,日志系统的配置文件存在于:

1
jre/lib/logging.properties

要想使用另一个配置文件,就要将 java.util.logging.config.file 特性设置为配置文件的存储位置,并用下列命令启动应用程序:

1
java -Djava.util.lgging.config.file=configFile MainClass

日志管理器在 VM 启动过程中初始化,在 main 执行之前完成。如果在 main 中调用 System.setProperty("java.util.logging.config.file", file),也会调用 LogManager.readConfiguration() 来重新初始化日志管理器。

日志记录的其它问题和选项

日志记录的可选项非常多,例如:

  • 本地化:解决让全球用户可以阅读的问题
  • 处理器:解决日志记录发送到哪里的问题
  • 过滤器:除了根据级别进行过滤外的附加过滤
  • 格式化器:对记录中的信息格式化并返回结果字符串

日志记录的一些经验

(1) 为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字,例如,com.mycompany.myprog,这是一种好的编程习惯。

可以通过调用下列方法得到日志记录器:

1
Logger logger = Logger.getLogger("com.mycompany.myprog");

(2) 默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台。用户可以覆盖默认的配置文件。

但注意,改变配置文件需要做很多工作。

下面代码将所有消息记录到应用程序特定的文件中:

1
2
3
4
5
6
7
8
9
10
11
if (System.getProperty("java.util.logging.config.class") == null
&& System.getProperty("java.util.logging.config.file") == null) {
try {
Logger.getLogger("").getLevel(Level.ALL);
final int LOG_ROTATION_COUNT = 10;
Handler handler = new FileHandler("%h/myapp.log", 0, LOG_ROTATION_COUNT);
Logger.getLogger("").addHandler(handler);
} catch (IOException e) {
logger.log(Level.SEVERE, "Can't create log file handler", e);
}
}

(3) 所有级别为 INFO、WARNING 和 SEVERE 的消息都将显示到控制台上。因此,最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为 FINE 是一个很好的选择。


调试

启动调试器前的一些建议:

(1) 打印或记录任意变量的值

1
2
System.out.println("x=" + x);
Logger.getGlobal().info("x=" + x);

获得隐式参数对象的状态

1
Logger.getGlobal().info("this=" + this);

(2) 在每一个类中单独放置 main 方法,这样可以对每一个类进行单元测试:

1
2
3
4
5
6
7
public class MyClass {
// methods and fields
...
public static void main(String[] args) {
// 测试代码
}
}

(3) 单元测试框架 JUnit:http://junit.org

(4) 日志代理是一个子类的对象,它可以截获方法调用,并进行日志记录,然后调用超类中的方法。

例如,在调用 Random 类的 nextDouble 方法时出现问题,就可以按以下方式,以匿名子类实例的形式创建一个代理对象

1
2
3
4
5
6
7
Random generator = new Random() {
public double nextDouble() {
double result = super.nextDouble();
Logger.getGlobal().info("nextDouble: " + result);
return result;
}
};

(5) 利用 Throwable 类的 printStackTrace 方法,可以从任何一个异常对象中获得堆栈情况。

1
2
3
4
5
6
try {
...
} catch (Throwable t) {
t.printStackTrace();
throw t;
}

不一定通过捕获异常来生成堆栈轨迹,在任何位置插入下面代码就可以获得堆栈轨迹:

1
Thread.dumpStack();

(6) 一般堆栈轨迹显示在 System.err 上,也可以通过 printStackTrace(PrintWriter s) 发送到一个文件中。

也可以捕获到一个字符串中:

1
2
3
StringWriter out = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(out));
String description = out.toString();

(7) 由于错误信息发送到 System.err 中,因此子啊命令行可以通过以下方法获取

1
java MyApp 2> errors.txt

(8) 让非捕获异常的轨迹出现在 System.err 中并不好。记录到文件中更好。

1
2
3
4
5
6
Thread.setDefaultUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
// 在 log 文件中保存信息。
}
});

(9) 要观察类加载的过程,可以用 -verbose 标志启动 Java 虚拟机

(10) -Xlint 选项告诉编译器对一些普遍容易出现的代码进行检查,可以使用的选项如下:

选项 含义
-Xlint 或 -Xlint:all 执行所有检查
-Xlint:deprecation 与 -deprecation 一样,检查废弃的方法
-Xlint:fallthrouth 检查 switch 中是否缺少 break
-Xlint:finally 警告 finally 不能正常执行
-Xlint:none 不执行任何检查
-Xlint:path 检查类路径和源代码路径上的所有目录是否存在
-Xlint:serial 警告没有 serialVersionUID 的串行化类 (参考卷 2 Chap 1)
-Xlint:unchecked 对通用类型与原始类型之间的危险转换给予警告 (参考 Chap8)

(11) Java 虚拟机增加了对 Java 应用程序进行监控和管理的支持。利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。JDK 有一个 jconsole 的图形工具,可以用于显示虚拟机性能的统计结果。

1
jconsole

(12) 可以用 jmap 实用工具获得一个堆的转储,其中显示了堆中的每个对象

1
2
jmap -dump:format=b,file=dumpFileName processID
jhat dumpFileName

(13) 如果使用 -Xprof 运行 Java 虚拟机,会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法,剖析信息将发送给 System.out。


Share