使用断言
断言(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。