小结
- 日志是为了替代System.out.println(),可以定义格式,重定向到文件等;
- 日志可以存档,便于追踪问题;
- 日志记录可以按级别分类,便于打开或关闭某些级别;
- 可以根据配置文件调整日志,无需修改代码;
- Java标准库提供了java.util.logging来实现日志功能。
- Commons Logging是使用最广泛的日志模块;
- Commons Logging的API非常简单;
- Commons Logging可以自动检测并使用其他日志模块。
- 通过Commons Logging实现日志,不需要修改代码即可使用Log4j;
- 使用Log4j只需要把log4j2.xml和相关jar放入classpath;
- 如果要更换Log4j,只需要移除log4j2.xml和相关jar;
- 只有扩展Log4j时,才需要引用Log4j的接口(例如,将日志加密写入数据库的功能,需要自己开发)。
- SLF4J和Logback可以取代Commons Logging和Log4j;
- 始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。
使用JDK Logging
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...");
}
}
运行上述代码,得到类似如下的输出:
Mar 02, 2019 6:32:13 PM Hello main
INFO: start process...
Mar 02, 2019 6:32:13 PM Hello main
WARNING: memory is running out...
Mar 02, 2019 6:32:13 PM 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>
。
使用Commons Logging
和Java标准库提供的日志不同,Commons Logging
是一个第三方日志库,它是由Apache
创建的日志模块。
Commons Logging
的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Logging
自动搜索并使用Log4j
(Log4j
是另一个流行的日志系统),如果没有找到Log4j
,再使用JDK Logging
。
使用Commons Logging
只需要和两个类打交道,并且只有两步:
- 第一步,通过
LogFactory
获取Log
类的实例; - 第二步,使用
Log
实例的方法打日志。
示例代码如下:
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.");
}
}
运行上述代码,肯定会得到编译错误,类似error: package org.apache.commons.logging does not exist(找不到org.apache.commons.logging这个包)
。因为Commons Logging
是一个第三方提供的库,所以,必须先把它下载下来。下载后,解压,找到commons-logging-1.2.jar
这个文件,再把Java
源码Main.java
放到一个目录下,例如work
目录:
然后用javac编译Main.java
,编译的时候要指定classpath
,不然编译器找不到我们引用的org.apache.commons.logging
包。编译命令如下:
javac -cp commons-logging-1.2.jar Main.java
如果编译成功,那么当前目录下就会多出一个Main.class
文件:
现在可以执行这个Main.class
,使用java
命令,也必须指定classpath
,命令如下:
java -cp .;commons-logging-1.2.jar Main
注意到传入的classpath
有两部分:一个是.
,一个是commons-logging-1.2.jar
,用;
分割。.
表示当前目录,如果没有这个.
,JVM不会在当前目录搜索Main.class
,就会报错。
如果在Linux
或macOS
下运行,注意classpath
的分隔符不是;
,而是:
:
java -cp .:commons-logging-1.2.jar Main
运行结果如下:
Mar 02, 2019 7:15:31 PM Main main
INFO: start...
Mar 02, 2019 7:15:31 PM 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()
,除了标准的info(String)
外,还提供了一个非常有用的重载方法:info(String, Throwable)
,这使得记录异常更加简单:
try {
...
} catch (Exception e) {
log.error("got exception!", e);
}
使用Log4j
前面介绍了Commons Logging
,可以作为“日志接口”来使用。而真正的“日志实现”可以使用Log4j
。
Log4j
是一种非常流行的日志框架,最新版本是2.x。
Log4j
是一个组件化设计的日志系统,它的架构大致如下:
当我们使用Log4j
输出一条日志时,Log4j
自动通过不同的Appender
把同一条日志输出到不同的目的地。例如:
- console:输出到屏幕;
- file:输出到文件;
- socket:通过网络输出到远程计算机;
- jdbc:输出到数据库
在输出日志的过程中,通过Filter
来过滤哪些log
需要被输出,哪些log
不需要被输出。例如,仅输出ERROR
级别的日志。
最后,通过Layout
来格式化日志信息,例如,自动添加日期、时间、方法名称等信息。
上述结构虽然复杂,但我们在实际使用的时候,并不需要关心Log4j
的API,而是通过配置文件来配置它。
以XML
配置为例,使用Log4j
的时候,我们把一个log4j2.xml
的文件放到classpath
下就可以让Log4j
读取配置文件并按照我们的配置来输出日志。下面是一个配置文件的例子:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<!-- 定义日志格式 -->
<Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property>
<!-- 定义文件名变量 -->
<Property name="file.err.filename">log/err.log</Property>
<Property name="file.err.pattern">log/err.%i.log.gz</Property>
</Properties>
<!-- 定义Appender,即目的地 -->
<Appenders>
<!-- 定义输出到屏幕 -->
<Console name="console" target="SYSTEM_OUT">
<!-- 日志格式引用上面定义的log.pattern -->
<PatternLayout pattern="${log.pattern}" />
</Console>
<!-- 定义输出到文件,文件名引用上面定义的file.err.filename -->
<RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}">
<PatternLayout pattern="${log.pattern}" />
<Policies>
<!-- 根据文件大小自动切割日志 -->
<SizeBasedTriggeringPolicy size="1 MB" />
</Policies>
<!-- 保留最近10份 -->
<DefaultRolloverStrategy max="10" />
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<!-- 对info级别的日志,输出到console -->
<AppenderRef ref="console" level="info" />
<!-- 对error级别的日志,输出到err,即上面定义的RollingFile -->
<AppenderRef ref="err" level="error" />
</Root>
</Loggers>
</Configuration>
虽然配置Log4j
比较繁琐,但一旦配置完成,使用起来就非常方便。对上面的配置文件,凡是INFO
级别的日志,会自动输出到屏幕,而ERROR
级别的日志,不但会输出到屏幕,还会同时输出到文件。并且,一旦日志文件达到指定大小(1MB),Log4j
就会自动切割新的日志文件,并最多保留10份。
有了配置文件还不够,因为Log4j
也是一个第三方库,我们需要从这里下载Log4j
,解压后,把以下3个jar
包放到classpath
中:
log4j-api-2.x.jar
log4j-core-2.x.jar
log4j-jcl-2.x.jar
因为Commons Logging
会自动发现并使用Log4j
,所以,把上一节下载的commons-logging-1.2.jar
也放到classpath中。
要打印日志,只需要按Commons Logging
的写法写,不需要改动任何代码,就可以得到Log4j
的日志输出,类似:
03-03 12:09:45.880 [main] INFO com.itranswarp.learnjava.Main
Start process...
在开发阶段,始终使用Commons Logging
接口来写入日志,并且开发阶段无需引入Log4j
。如果需要把日志写入文件, 只需要把正确的配置文件
和Log4j
相关的jar包
放入classpath
,就可以自动把日志切换成使用Log4j
写入,无需修改任何代码。
使用SLF4J和Logback
前面介绍了Commons Logging
和Log4j
这一对好基友,它们一个负责充当日志API
,一个负责实现日志底层,搭配使用非常便于开发。
有的童鞋可能还听说过SLF4J
和Logback
。这两个东东看上去也像日志,它们又是啥?
其实SLF4J
类似于Commons Logging
,也是一个日志接口,而Logback
类似于Log4j
,是一个日志的实现。
为什么有了Commons Logging
和Log4j
,又会蹦出来SLF4J
和Logback
?
这是因为Java
有着非常悠久的开源历史,不但OpenJDK
本身是开源的,而且我们用到的第三方库,几乎全部都是开源的。开源生态丰富的一个特定就是,同一个功能,可以找到若干种互相竞争的开源库。
因为对Commons Logging
的接口不满意,有人就搞了SLF4J
。因为对Log4j
的性能不满意,有人就搞了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
的接口:
Commons Logging | SLF4J |
---|---|
org.apache.commons.logging.Log | org.slf4j.Logger |
org.apache.commons.logging.LogFactory | org.slf4j.LoggerFactory |
不同之处就是
Log
变成了Logger
,LogFactory
变成了LoggerFactory
。
使用SLF4J
和Logback
和前面讲到的使用Commons Logging
加Log4j
是类似的,先分别下载SLF4J
和Logback
,然后把以下jar包
放到classpath
下:
slf4j-api-1.7.x.jar
logback-classic-1.2.x.jar
logback-core-1.2.x.jar
然后使用SLF4J
的Logger
和LoggerFactory
即可。和Log4j
类似,我们仍然需要一个Logback
的配置文件,把logback.xml
放到classpath
下,配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<file>log/output.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>log/output.log.%i</fileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>1MB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
运行即可获得类似如下的输出:
13:15:25.328 [main] INFO com.itranswarp.learnjava.Main - Start process...
从目前的趋势来看,越来越多的开源项目从Commons Logging
加Log4j
转向了SLF4J
加Logback
。