4.使用断言
断言的概念
如果一个数为负值,抛出一个异常:
if (x < 0) {
throw new IllegalAccessException("x < 0");
}
但这段代码会一直保留在程序中,即使测试完毕也不会自动删除。
断言机制允许在测试期间想代码插入一些检查语句,当代码发布时,这些插入的检测语句将会被自动地移走。
Java引入关键字assert:
assert 条件;
assert 条件 : 表达式;
这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。第二种形式,表达式将被传入AssertionError构造器,并转换成一个消息字符串。
启动和禁用断言
在默认情况下断言被禁止。IDEA开启断言,在VM options中添加-ea:
在启用或禁止断言时不必重新编译程序,启用或禁止断言是==类加载器(class loader)==的功能。当断言被禁用时,类加载器将会跳过断言代码,因此不会降低程序的运行速度。
-da或者删除-ea可以关闭断言。
使用断言完成参数检查
在Java中给出了3种处理系统错误的机制:
1.抛出异常
2.日志
3.断言
断言失败是致命的、不可恢复的错误,断言检查只用于开发和测试阶段。因此不应该使用断言向程序员的其他部分通告发生了可恢复性的错误,或者不应该做为程序向用户通告问题的手段。断言只应用于在测试阶段确定程序内部的错误位置。
假设方法的约定:
@param a the array to be sorted(must not be null)
那这个方法的调用者就必须注意:不允许用null数组调用这个方法,并在这个方法开头使用断言:
assert a != null;
计算机科学家将这种约定成为前置条件(Precondition)。调用者在调用方法时没有提供满足这个前置条件的参数,断言就会失败,将会出现难以预料的结果,有时会抛出一个断言错误,有时会产生一个null指针异常,这完全取决于类加载器的配置。
断言是一种测试和调试阶段所使用的战术性工具;而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。
5.记录日志
基本日志
全局日志记录器(global logger):
Logger.getGlobal().info("File->Open menu item selected");
// 十一月 26, 2019 1:33:13 下午 LoggerTest main
// 信息: File->Open menu item selected
在适当的地方调用:
Logger.getGlobal().setLevel(Level.OFF);
将会取消所有日志。
高级日志
企业级(industrial-strength)日志,在专业的应用程序中,不要讲所有的日志都记录到一个全局记录器中,而是可以自定义记录器。
private static final Logger myLogger = Logger.getLogger("chapter7.section5");
日志记录器级别:
SEVERE
WARNING
INFO
CONFIG
FINE
FINER
FINEST
默认情况下,只记录前三个级别。也可以设置其他级别:
logger.setLevel(Level.FINE);
现在,FINE和更高级别的记录都可以记录下来。还可以使用Level.ALL开启所有级别的记录。或者用Level.OFF关闭所有级别的记录。
还有:
logger.warning(message);
logger.fine(message);
logger.log(Level.FINE, message);
默认的日志记录显示包含日志调用类的类名和方法。但是如果虚拟机对执行过程进行优化,就得不到精确的调用信息。可以调用logp方法获得调用类和方法的确切位置:
void logp(Level l, String className, String methodName, String message)
void entering,将生成FINER级别和以字符串ENTRY和RETURN开始的日志记录。
void throwing,以记录一条FINER级别的记录和一条以THROW开始的信息。
修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种配置。默认情况下,配置文件存在于:jre/lib/logging.properties
使用其他配置文件,就要将java.util.logging.config.file特性设置为配置文件的存储位置:
java -Djava.util.logging.config.file=configFile MainClass
IDEA配置方式:
日志管理器在VM启动过程中初始化,在main之前完成。如果在main中调用System.setProperty(“java.util.logging.config.file”, file)也会调用LogManager.readConfiguration()来重新初始化日志管理器。
# 修改默认日志记录级别
.level = INFO
# 指定自己的日志记录
com.mycompany.myapp.level = FINE
# 指定控制台级别
java.util.logging.ConsoleHandler.level = FINE
本地化
本地化的应用程序包含资源包中的本地特定信息。资源包由各个地区的映射集组成。如某个资源包将字符串“readingFile”映射出英文的“Reading file”或中文的“读取文件”。
想要将映射添加到资源包中,需要为每个地区创建一个文件,logmessages_en.propertise,logmessages_cn.propertise(en和zh是语言编码)。
logmessages.propertise文件内容:
readingFile=读取文件
moreMessage=更多{0}信息{1}
注意设置本地语言编码:
/**
* 在请求日志记录器时,可以指定资源包
*/
private static final Logger myLogger = Logger.getLogger("loggerName", "chapter7.section5.logmessages");
public static void main(String[] args) {
// 为日志指定资源包的关键字,而不是实际的日志消息字符串
myLogger.info("readingFile");
// 通常需要在本地化的消息中增加一些参数,可以包含占位符{0},{1}等
myLogger.log(Level.INFO, "moreMessage", new Object[] { "的", "." });
}
//十一月 27, 2019 9:36:31 下午 chapter7.section5.LoggerTest main
//信息: 读取文件
//十一月 27, 2019 9:36:31 下午 chapter7.section5.LoggerTest main
//信息: 更多的信息.
处理器
在默认情况下,日志记录器将记录发送到ConsoleHandler中,并由它输出到System.err流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器有一个ConsoleHandler。
与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。
java.util.logging.ConsoleHandler.level = FINE
还可以绕过配置文件,安装自己的处理器:
Logger logger = Logger.getLogger("com.mycompany.myapp");
logger.setLevel(Level.FINE);
logger.setUseParentHandlers(false);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
logger.addHandler(handler);
日志API为此提供了两个很有用的处理器,一个是FileHandler,可以收集文件中的记录;另一个是SocketHandler。SocketHandler将记录发送到特定的主机和端口。
FileHandler handler = new FileHandler();
logger.addHandler(handler );
logger.info("读取文件");
这些记录被发送到用户主目录(Windows95/98/Me,C:\Window)的javan.log文件中,n是文件名的唯一编号。
在默认情况下,记录被格式化为XML。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2019-11-27T21:41:12</date>
<millis>1574862072789</millis>
<sequence>4</sequence>
<logger>loggerName</logger>
<level>INFO</level>
<class>chapter7.section5.LoggerTest</class>
<method>main</method>
<thread>1</thread>
<message>读取文件</message>
<key>readingFile</key>
<catalog>chapter7.section5.logmessages</catalog>
</record>
</log>
文件处理器配置参数:
配置属性 | 描述 | 默认 |
---|---|---|
java.util.logging.FileHandler.level | 处理器级别 | Level.ALL |
java.util.logging.FileHandler.append | 控制处理器应该追到一个已经存在的文件尾部;还是应该为每个运行的程序打开一个新文件 | false |
java.util.logging.FileHandler.limit | 在打开另一个文件之前允许写入一个文件的近似最大字节数(0表示无限制) | FileHandler类中为0,在默认的日志管理器配置文件中为5000 |
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 | 为循环日志记录生成的数值 |
%% | %字符 |
开启文件循环,日志文件以myapp.log.0,myapp.log.1,这种循环序列的形式出现。只要文件超出了大小限制,最旧的文件会被删除,其他的文件将重新命名,同时创建一个文件,其编号为0。
还可以通过扩展Handler类或StreamHandler类自定义处理器,例如下述程序中的WindowHandler 。
过滤器
在默认情况下,过滤器根据日志记录的级别进行过滤。每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的过滤。可以通过实现Filter接口并定义下列方法来自定义过滤器。
boolean isLoggable(LogRecord record)
可以利用自己喜欢的标准,对日志记录进行分析,返回true表示这些记录应该包含在日志中。同一时刻最多只能有一个过滤器。
格式化器
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。但是也可以自定义格式,这需要扩展Formatter类并覆盖下面方法:
String format(LogRecord record)
String formatMessage(LogRecord record)对记录中的部分信息进行格式化、参数替换和本地化应用操作。
String getHead(Handler h)和String getTail(Handler h)可以在已格式化的记录前后加一个头部和尾部。最后调用setFormatter方法将格式化器安装到处理器中。
日志记录说明
1.为一个简单的程序选择一个日志记录器。并把日志记录器命名为与主程序包一样的名字。为了方便起见,可以将静态域添加到类中:
private static final Logger logger = Logger.getLogger("com.mycompany.myprog");
2.默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台。用户可以覆盖默认的配置文件。
3.可以记录自己想要的内容。所有级别为INFO、WARNING和SEVERE的消息都将显示在控制台上。最好只将对程序用户有意义的消息设置为这几个级别。程序员想要的日志记录,设置为FINE是一个很好的选择。
public class LoggingImageViewer {
public static void main(String[] args) {
if (System.getProperty("java.util.logging.config.class") == null
&& System.getProperty("java.util.logging.config.file") == null) {
try {
Logger.getLogger("com.spring.corejava").setLevel(Level.ALL);
final int LOG_ROTATION_COUNT = 10;
Handler handler = new FileHandler("%h/LoggingImageViewer.log", 0, LOG_ROTATION_COUNT);
Logger.getLogger("com.spring.corejava").addHandler(handler);
} catch (IOException e) {
Logger.getLogger("com.spring.corejava").log(Level.SEVERE, "不能创建日志文件处理器", e);
}
}
EventQueue.invokeLater(() -> {
Handler windowHandler = new WindowHandler();
windowHandler.setLevel(Level.ALL);
Logger.getLogger("com.spring.corejava").addHandler(windowHandler);
JFrame frame = new ImageViewerFrame();
frame.setTitle("LoggingImageViewer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Logger.getLogger("com.spring.corejava").fine("显示帧");
frame.setVisible(true);
});
}
}
class ImageViewerFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 400;
private JLabel label;
private static Logger logger = Logger.getLogger("com.spring.corejava");
public ImageViewerFrame() {
logger.entering("ImageViewerFrame", "<init>");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu menu = new JMenu("File");
menu.add(menu);
JMenuItem openItem = new JMenuItem("Open");
menu.add(openItem);
openItem.addActionListener(new FileOpenListener());
}
private class FileOpenListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
logger.entering("ImageViewerFrame.FileOpenListener", "actionPerformed", e);
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
chooser.setFileFilter(new javax.swing.filechooser.FileFilter(){
@Override
public boolean accept(File f) {
return f.getName().toLowerCase().endsWith(".gif") || f.isDirectory();
}
@Override
public String getDescription() {
return "GIF图片";
}
});
int r = chooser.showOpenDialog(ImageViewerFrame.this);
if (r == JFileChooser.APPROVE_OPTION) {
String name = chooser.getSelectedFile().getPath();
logger.log(Level.FINE, "读取文件{0}", name);
label.setIcon(new ImageIcon(name));
} else {
logger.fine("文件打开对话框已取消");
}
logger.exiting("ImageViewerFrame.FileOpenListener", "actionPerformed");
}
}
}
class WindowHandler extends StreamHandler {
private JFrame frame;
public WindowHandler() {
frame = new JFrame();
final JTextArea output = new JTextArea();
output.setEditable(false);
frame.setSize(200, 200);
frame.add(new JScrollPane(output));
frame.setFocusableWindowState(false);
frame.setVisible(true);
setOutputStream(new OutputStream() {
@Override
public void write(int b) throws IOException {
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
output.append(new String(b, off, len));
}
});
}
@Override
public void publish(LogRecord record) {
if (!frame.isVisible()) {
return;
}
super.publish(record);
flush();
}
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2019-11-27T21:50:39</date>
<millis>1574862639844</millis>
<sequence>0</sequence>
<logger>com.spring.corejava</logger>
<level>FINER</level>
<class>ImageViewerFrame</class>
<method><init></method>
<thread>17</thread>
<message>ENTRY</message>
</record>
<record>
<date>2019-11-27T21:50:39</date>
<millis>1574862639890</millis>
<sequence>1</sequence>
<logger>com.spring.corejava</logger>
<level>FINE</level>
<class>chapter7.section5.LoggingImageViewer</class>
<method>lambda$main$0</method>
<thread>17</thread>
<message>显示帧</message>
</record>
</log>
6.调试技巧
1.打印或记录任意变量的值
2.每个类但需放置main方法,这样可以对每个类进行单元测试
3.JUnit非常常见的单元测试框架
4.日志代理(logging proxy)是一个子类的对象,它可以截取方法调用,并进行日志记录,然后调用超类中的方法。
Random generator = new Random() {
@Override
public double nextDouble() {
double result = super.nextDouble();
Logger.getGlobal().info("nextDouble: " + result);
return result;
}
};
5.利用Throwable类提供的printStackTrace方法,可以从任何一个异常对象中获得堆栈情况。
6.堆栈轨迹一般显示在System.err上,也可以利用printStackTrace(PrintWriter s)方法将它发送到一个文件中。