使用断言
断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。
我们先看一个例子:
public static void main(String[] args) {
double x = Math.abs(-123.45);
assert x >= 0;
System.out.println(x);
}
语句assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError。
使用assert语句时,还可以添加一个可选的断言消息:
assert x >= 0 : "x must >= 0";
这样,断言失败的时候,AssertionError会带上消息x must >= 0,更加便于调试。
Java断言的特点是:断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。
对于可恢复的程序错误,不应该使用断言,应该抛出异常并在上层捕获:
void sort(int[] arr) {
if (x == null) {
throw new IllegalArgumentException("array cannot be null");
}
}
JVM默认关闭断言指令,即遇到assert语句就自动忽略了,不执行。
要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言。所以,上述程序必须在命令行下运行才有效果:
$ java -ea Main.java
Exception in thread "main" java.lang.AssertionError
at Main.main(Main.java:5)
还可以有选择地对特定地类启用断言,命令行参数是:-ea:com.itranswarp.sample.Main,表示只对com.itranswarp.sample.Main这个类启用断言。
或者对特定地包启用断言,命令行参数是:-ea:com.itranswarp.sample...(注意结尾有3个.),表示对com.itranswarp.sample这个包启动断言。
实际开发中,很少使用断言。更好的方法是编写单元测试,后续我们会讲解JUnit的使用。
小结
断言是一种调试方式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言;
对可恢复的错误不能使用断言,而应该抛出异常;
断言很少被使用,更好的方法是编写单元测试。
使用JDK Logging
输出日志,而不是用System.out.println(),有以下几个好处:
1.可以设置输出样式,避免自己每次都写"ERROR: " + var;
2.可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
3.可以被重定向到文件,这样可以在程序运行结束后查看日志;
4.可以按包名控制日志级别,只输出某些包打的日志;
5.可以……
如何使用日志?
Java标准库内置了日志包java.util.logging,可以直接用。
简单的例子:
// logging
import java.util.logging.Level;
import java.util.logging.Logger;
public class Hello {
public static void main(String[] args) {
Logger logger = Logger.getGlobal();
logger.info("start process...");
logger.warning("memory is running out...");
logger.fine("ignored.");
logger.severe("process will be terminated...");
}
}
运行上述代码,得到类似如下的输出:
Aug 12, 2020 8:43:40 AM Hello main
INFO: start process...
Aug 12, 2020 8:43:40 AM Hello main
WARNING: memory is running out...
Aug 12, 2020 8:43:40 AM Hello main
SEVERE: process will be terminated...
对比可见,使用日志最大的好处是,它自动打印了时间、调用类、调用方法等很多有用的信息。
再仔细观察发现,4条日志,只打印了3条,logger.fine()没有打印。这是因为,日志的输出可以设定级别。JDK的Logging定义了7个日志级别,从严重到普通:
SEVERE
WARNING
INFO
CONFIG
FINE
FINER
FINEST
因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。
使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
使用Java标准库内置的Logging有以下局限:
Logging系统在JVM启动时读取配置文件并完成初始化,一旦开始运行main()方法,就无法修改配置;
配置不太方便,需要在JVM启动时传递参数-Djava.util.logging.config.file=<config-file-name>。
因此,Java标准库内置的Logging使用并不是非常广泛。
练习:使用logger.severe()打印异常:
import java.io.UnsupportedEncodingException;
import java.util.logging.Logger;public class Main {
public static void main(String[] args) {
Logger logger = Logger.getLogger(Main.class.getName());
logger.info("Start process...");
try {
"".getBytes("invalidCharsetName");
} catch (UnsupportedEncodingException e) {
// TODO: 使用logger.severe()打印异常
logger.severe(e.toString());
}
logger.info("Process end.");
}
}
小结
日志是为了替代System.out.println(),可以定义格式,重定向到文件等;
日志可以存档,便于追踪问题;
日志记录可以按级别分类,便于打开或关闭某些级别;
可以根据配置文件调整日志,无需修改代码;
Java标准库提供了java.util.logging来实现日志功能。
使用Commons Logging
Commons Logging可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。
默认情况下,Commons Loggin自动搜索并使用Log4j,如果没有找到Log4j,再使用JDK Logging。
使用Commons Logging只需要和两个类打交道,并且只有两步:
第一步,通过LogFactory获取Log类的实例; 第二步,使用Log实例的方法打日志。
示例代码如下:
//需要添加commons-logging-1.2.jar的依赖
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class Main {
public static void main(String[] args) {
Log log = LogFactory.getLog(Main.class);
log.info("start...");
log.warn("end.");
}
}
运行结果如下:
August 13, 2020 9:53:35 下午 Test1.Main main
INFO: start...
August 13, 2020 9:53:35 下午 Test1.Main main
WARNING: end.Commons Logging定义了6个日志级别:
FATAL
ERROR
WARNING
INFO
DEBUG
TRACE
默认级别是INFO。
使用Commons Logging时,如果在静态方法中引用Log,通常直接定义一个静态类型变量:
// 在静态方法中引用Log:
public class Main {
static final Log log = LogFactory.getLog(Main.class); static void foo() {
log.info("foo");
}
}
在实例方法中引用Log,通常定义一个实例变量:
// 在实例方法中引用Log:
public class Person {
protected final Log log = LogFactory.getLog(getClass()); void foo() {
log.info("foo");
}
}
注意到实例变量log的获取方式是LogFactory.getLog(getClass())。
虽然也可以用LogFactory.getLog(Person.class),但是前一种方式有个非常大的好处,就是子类可以直接使用该log实例。
例如:
// 在子类中使用父类实例化的log:
public class Student extends Person {
void bar() {
log.info("bar");
}
}
由于Java类的动态特性,子类获取的log字段实际上相当于LogFactory.getLog(Student.class),虽然是从父类继承而来,但无需改动代码。
此外,Commons Logging的日志方法,除了标准的info(String)外,还提供了一个非常有用的重载方法:info(String, Throwable),这使得记录异常更加简单:
try {
...
} catch (Exception e) {
log.error("got exception!", e);
}
小结
Commons Logging是使用最广泛的日志模块;
Commons Logging的API非常简单;
Commons Logging可以自动检测并使用其他日志模块。
使用Log4j
前面介绍了Commons Logging,可以作为“日志接口”来使用。而真正的“日志实现”可以使用Log4j。
Log4j是一个组件化设计的日志系统,它的架构大致如下:
log.info("User signed in.");
├──>│ Appender │───>│ Filter │───>│ Layout │───>│ Console │
├──>│ Appender │───>│ Filter │───>│ Layout │───>│ File │
├──>│ Appender │───>│ Filter │───>│ Layout │───>│ Socket │
└──>│ Appender │───>│ Filter │───>│ Layout │───>│ jdbc │
当我们使用Log4j输出一条日志时,Log4j自动通过不同的Appender(附加器)把同一条日志输出到不同的目的地。
例如:
console:输出到屏幕;
file:输出到文件;
socket:通过网络输出到远程计算机;
jdbc:输出到数据库
在输出日志的过程中,通过Filter(过滤器)来过滤哪些log需要被输出,哪些log不需要被输出。例如,仅输出ERROR级别的日志。
最后,通过Layout(布局)来格式化日志信息,例如,自动添加日期、时间、方法名称等信息。
上述结构虽然复杂,但我们在实际使用的时候,并不需要关心Log4j的API,而是通过配置文件来配置它。
以XML配置为例,使用Log4j的时候,我们把一个log4j2.xml的文件放到classpath下(src文件夹下)就可以让Log4j读取配置文件并按照我们的配置来输出日志。
Log4j也是一个第三方库,使用前需要把以下3个jar包放到classpath中:
log4j-api-2.x.jar
log4j-core-2.x.jar
log4j-jcl-2.x.jar
使用SLF4J和Logback
前面介绍了Commons Logging和Log4j一个负责充当日志API,一个负责实现日志底层,搭配使用非常便于开发。
而SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。
同一个功能,可以找到若干种互相竞争的开源库,所以有了SLF4J和Logback。
SLF4J对Commons Logging的接口有何改进?
在Commons Logging中,我们要打印日志,有时候得这么写:
int score = 99;
p.setScore(score);
log.info("Set score " + score + " for Person " + p.getName() + " ok.");
SLF4J的日志接口改进成这样了:
int score = 99;
p.setScore(score);
logger.info("Set score {} for Person {} ok.", score, p.getName());
SLF4J的日志接口通过{}实现占位符的功能,用参数自动替换占位符。
SLF4J的接口和Commons Logging几乎一模一样:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Main {
final Logger logger = LoggerFactory.getLogger(getClass());
}
对比一下Commons Logging和SLF4J的接口,不同之处就是Log变成了Logger,LogFactory变成了LoggerFactory。