日志框架
Java中现有的日志框架:
- 日志门面:JCL(Jakarta Common Logging)、SLF
- 日志实现:JUL(Java Util Logging)、log4j、logback、log4j2
logback
比 log4j
功能更加强大,性能更加好;log4j2
与 logback
功能相似,但前者的性能更好
1、JUL
1.1 JUL介绍
JUL:全称 Java Util Logging
,它是 Java 原生的日志框架,使用时不需要引入第三方类库,相对其它日志框架使用方便、学习简单,能够在小型应用中灵活使用。
架构:
- Loggers:记录器。应用程序通过获取 Logger 对象,调用其 API 来发布日志信息。Logger 对象通常是应用程序访问日志系统的入口程序;
- Appenders:也被称为 Handlers。每个 Logger 都会关联一组 Handlers。 Logger 会将日志交给关联的 Handlers 处理,由 Handlers 负责将日志做记录。 Handlers 在此是抽象的。其具体的实现决定了日志记录的位置可以是控制台、文件等;
- Layouts:也被称为 Formatters。它负责对日志事件中的数据进行转换和格式化。Layouts 决定了数据在一条日志记录中的最终形式;
- Level:每条日志消息都有一个关联的日志级别。该级别粗略指导了日志消息的重要性和紧迫。可以将 Level 和 Loggers、Appenders 做关联,以便于我们过滤消息;
- Filters:过滤器。根据需要定制哪些消息会被记录,哪些消息会被放过。
【总结】:用户使用 Logger 来进行日志记录,Logger 持有多个 Handlers,日志的输出操作是由 Handler 完成。在 Handler 输出日志前,会经过 Filters 的过滤,判断哪些日志级别放行,哪些日志级别拦截。Handler 会将日志内容输出到指定位置(控制台/文件)。Handler 在输出日志时会使用 Layout,将输出内容进行排版。
1.2 使用案例
public class JulTest {
public static final String CALSS_NAME = "com.zzc.sbexp.log.jul.JulTest";
@Test
public void testQuick() {
Logger logger = Logger.getLogger(CALSS_NAME);
// 输出日志 日志级别:info
logger.info("hello jul");
// 通过方法输出
logger.log(Level.INFO, "hello jul");
// 通过占位符输出变量值
String name = "zzc";
Integer age = 24;
logger.log(Level.INFO, "用户信息:{0}, {1}", new Object[]{name, age});
}
}
1.3 日志级别
日志级别从高至低:off
、server
、warning
、info
、config
、fine
、finer
、finest
、all
如果是:off
,那么就是关闭了日志级别输出;
如果是:all
,那么就是开启了日志级别输出。
Jul
默认级别是 info
,只要日志级别高于 info
,都会输出。
自定义日志级别
步骤:
- 关闭系统默认配置
- 创建一个
Handler
。我这里是输出到控制台和磁盘,所以是ConsoleHandler
、FileHandler
; - 创建一个
Formatter
; - 将
Handler
与Formatter
进行关联; - 将
Handler
添加到Logger
中。
@Test
public void testLogConfig() throws IOException {
Logger logger = Logger.getLogger(CALSS_NAME);
// 关闭系统默认配置
logger.setUseParentHandlers(false);
// 创建控制台输出Handler
ConsoleHandler consoleHandler = new ConsoleHandler();
SimpleFormatter simpleFormatter = new SimpleFormatter();
// 关联
consoleHandler.setFormatter(simpleFormatter);
// 创建文件输出Handler
FileHandler fileHandler = new FileHandler("logs.log");
fileHandler.setFormatter(simpleFormatter);
logger.addHandler(consoleHandler);
logger.addHandler(fileHandler);
// 配置日志级别
logger.setLevel(Level.ALL);
consoleHandler.setLevel(Level.ALL);
fileHandler.setLevel(Level.WARNING);
logger.severe("server");
logger.warning("warning");
logger.info("info");
}
【注意】:logs.log 的父级路径必须得存在,否则,会报错。
自定义配置文件修改 RootLogger 的默认配置
在 resource 路径下添加一个配置文件 logging.properties
,其内容为:
#RootLogger默认的处理器,可以配置多个,所有非手动解除父日志的子日志都将使用这些处理器
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
#RootLogger的日志级别(默认INFO),所有的Handler都受限于此日志级别,Handler的日志级别可以比RootLogger的日志级别
.level = INFO
java.util.logging.FileHandler.pattern = E:/logs/java%u.log
#单个日志文件大小,单位是bit,1024bit即为1kb
java.util.logging.FileHandler.limit = 1024 * 1024 * 10
#日志文件数量
java.util.logging.FileHandler.count = 1
#是否以追加方式添加日志内容
java.util.logging.FileHandler.append = true
java.util.logging.FileHandler.encoding = UTF-8
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.encoding = UTF-8
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
代码:
@Test
public void testLogProperties() throws IOException {
// 通过类加载加载配置文件
InputStream in = JulTest.class.getClassLoader().getResourceAsStream("logging.properties");
// 创建LogManger加载配置文件
LogManager logManager = LogManager.getLogManager();
logManager.readConfiguration(in);
Logger logger = Logger.getLogger(CALSS_NAME);
logger.severe("server");
logger.warning("warning");
logger.info("info");
logger.finest("finest");
}
自定义配置文件添加自定义的 Logger
在 resource 路径下添加一个配置文件 logging.properties
,其内容为:
...
my.logger.handlers = java.util.logging.ConsoleHandler
my.logger.level = INFO
my.logger.useParentHandlers = false
代码:
@Test
public void testMyLogProperties() throws IOException {
// 通过类加载加载配置文件
InputStream in = JulTest.class.getClassLoader().getResourceAsStream("logging.properties");
// 创建LogManger加载配置文件
LogManager logManager = LogManager.getLogManager();
logManager.readConfiguration(in);
Logger logger = Logger.getLogger("my.logger");
logger.severe("server");
logger.warning("warning");
logger.info("info");
logger.finest("finest");
}
2、Log4j
2.1 Log4j 介绍
Log4j
是 Apache
下的一款开源的日志框架,通过在项目中使用 log4j
,我们可以控制日志信息输出的位置(控制台、文件、数据库);也可以控制每一条日志的输出格式,通过定义日志的输出级别,可以更灵活的控制日志的输出过程,方便项目的调试。
Log4j 组件
Log4j
主要由 Loggers
(日志记录器)、Appenders
(输出端)、Layout
(日志格式化器)组成。
- Loggers:控制日志的输出级别与日志是否输出
Loggers
:日志记录器,负责处理日志记录。实例的命名就是类的全限定名。Logger 的名字大小写敏感。其命名有继承机制。如:org.apache.commons
的 logger 会继承 org.apache
的 logger。
Log4j
中,有一个特殊的 logger
,叫 root
,它是所有的 logger 的根。root logger 可以通过 Logger.getRootLogger()
方法获取
- Appenders:指定日志的输出方式
Appenders 用来指定日志输出到哪个地方,可以同时指定日志的输出目的地(控制台、文件)。常用的目的地有以下几种:
- ConsoleAppender:将日志输出到控制台
- FileAppender:将日志输出到文件中
- DailyRollingFileAppender:将日志输出到一个日志文件,并且每天输出到一个新的文件
- RollingFileAppender:将日志输出到一个日志文件,并且指定文件的尺寸,当文件大小达到指定尺寸时,会自动把文件改名,同时,产生一个新的文件
- JDBCAppender:将日志保存到数据库
- Layout:控制台日志信息的输出格式
Layouts
· 用于控制日志输出内容的格式,让我们可以使用各种需要的格式输出日志。常用的 Layouts:
HTML Layout:格式化日志输出为 HTML 表格形式
Simple Layout:简单的日志输出格式化。打印的日志格式为(info - message)
Pattern Layout:最强大的格式化器。可以根据自定义格式输出日志,如果没有指定转换格式,则使用默认的转换格式
2.2 使用案例
新建一个 SpringBoot 工程,引入 log4j
依赖:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
public class Log4jTest {
@Test
public void testQuick() {
Logger logger = Logger.getLogger(Log4jTest.class);
logger.info("info");
}
}
执行后,查看控制台:
由此可知,需要进行系统配置。一般用配置文件进行配置,这里暂时先用代码进行配置。
@Test
public void testQuick() {
BasicConfigurator.configure();
Logger logger = Logger.getLogger(Log4jTest.class);
logger.info("info");
}
这样,日志信息就能正常输出。
2.3 日志级别
fatal:严重错误。一般会造成系统崩溃并终止运行
error:错误信息。不会影响系统运行
warn:警告信息。可能会发生问题
info:运行信息。数据连接、网络连接、IO操作等等
debug:调试信息。一般在开发中使用,记录程序变量参数传递信息等等
trace:追踪信息。记录程序所有的流程信息
Log4j
默认级别是 debug
,只要日志级别高于 debug
,都会输出。
2.4 使用配置文件
在 resources
下新建一个配置文件 log4j.properties
:
# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=info,使用的 appender 是 console
log4j.rootLogger = info,console
# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.SimpleLayout
在代码中使用:
@Test
public void testProperties() {
Logger logger = Logger.getLogger(Log4jTest.class);
logger.fatal("fatal");
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
}
- 自定义配置 Appender 的格式
在 log4j.properties
配置文件中,我们定义了日志输出级别与输出端,在输出端中分别配置日志的输出格式:
# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=info,使用的 appender 是 console
log4j.rootLogger = info,console
# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
#指定消息格式的内容
log4j.appender.console.layout.conversionPattern = [%p]%r %l %d{yyyy-MM-dd HH:mm:ss} %m%n
log4j
采用类似 C 语言的 printf()
函数的打印格式格式化日志信息,具体的占位符及其含义如下:
%m=输出代码中指定的信息
%p=输出优先级
%n=换行符
%r=输出自应用启动到输出该 log 信息耗费的毫秒数
%c=输出打印语句所属的类的全限定名
%t=输出产生该日志的线程全名
%d=输出服务器当前时间。默认为 ISO8601。可指定格式:%d{yyyy年MM月dd日 HH:mm:ss}
%l=输出日志发生位置。包括:类名、线程名及在代码中的行数
%F=输出日志消息产生时所在的文件名称
%L=输出代码中的行号
%%=输出一个 % 字符
可以在 % 与字符之间加上修饰符来控制最小宽度,最大宽度和文本的对其方式。如:
%5c=输出类的全限定名,最小宽度是5,类名小于5,默认情况下右对齐
%-5c=输出类的全限定名,最小宽度是5,类名小于5,“-” 指定左对齐,会有空格
%.5c=输出类的全限定名,最大宽度是5,类名大于5,就会将左边多出的字符截掉;小于5,不会有空格
%20.30c%=类名小于20,补空格、右对齐;大于30,就会将左边多出的字符截掉
- 自定义配置文件的 FileAppender
log4j.properties:将日志信息输出到文件
FileAppender
# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=info,使用的 appender 是 console
log4j.rootLogger = info,console,rollFile
# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.console.layout.conversionPattern = [%p]%r %l %d{yyyy-MM-dd HH:mm:ss} %m%n
# 指定控制台日志输出的 appender
log4j.appender.file = org.apache.log4j.FileAppender
# 指定消息格式 layout
log4j.appender.file.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.file.layout.conversionPattern = [%p]%r %l %d{yyyy-MM-dd HH:mm:ss} %m%n
# 指定日志文件的保存路径
log4j.appender.file.file = E:/temp/logs.log
# 指定日志文件的字符集
log4j.appender.file.encoding = UTF-8
log4j.properties
:按照文件大小进行拆分
# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=info,使用的 appender 是 console
log4j.rootLogger = info,console,rollFile
# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.console.layout.conversionPattern = [%p]%r %l %d{yyyy-MM-dd HH:mm:ss} %m%n
# 按照文件大小进行拆分的 appender 对象
log4j.appender.rollFile = org.apache.log4j.RollingFileAppender
# 指定消息格式 layout
log4j.appender.rollFile.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.rollFile.layout.conversionPattern = [%p]%r %l %d{yyyy-MM-dd HH:mm:ss} %m%n
# 指定日志文件的保存路径
log4j.appender.rollFile.file = E:/temp/logs.log
# 指定日志文件的字符集
log4j.appender.rollFile.encoding = UTF-8
# 指定日志文件内容的大小
log4j.appender.rollFile.maxFileSize = 1MB
# 指定日志文件的数量
log4j.appender.rollFile.maxBackupIndex = 10
log4j.properties
:按照时间大小进行拆分
# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=info,使用的 appender 是 console
log4j.rootLogger = info,dailyFile
# 按照时间大小进行拆分的 appender 对象
log4j.appender.dailyFile = org.apache.log4j.DailyRollingFileAppender
# 指定消息格式 layout
log4j.appender.dailyFile.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.dailyFile.layout.conversionPattern = [%p]%r %l %d{yyyy-MM-dd HH:mm:ss} %m%n
# 指定日志文件的保存路径
log4j.appender.dailyFile.file = E:/temp/logs.log
# 指定日志文件的字符集
log4j.appender.dailyFile.encoding = UTF-8
# 指定日志文件拆分的规则
log4j.appender.dailyFile.datePattern = '.'yyyy-MM-dd-HH-mm-ss
- 自定义 Logger
log4j.properties
:
# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=info,使用的 appender 是 console
log4j.rootLogger = trace,console
# 自定义 logger,也会继承RootLogger,所以也会输出到控制台
log4j.logger.com.zzc.sbexp.log.log4j = info,file
# apache 的logger,只有error级别及以上才会输出到控制台,并不会输出到文件
log4j.logger.org.apache = error
...
运行后:
前言
JUL 用起来简单、方便,不需要引入第三方依赖,适合小型应用。Log4j 同样适用起来简单,但它配置更为强大。那么,我们该如何选择这两个日志框架呢?其实,这根据我们项目需求而定的。
举个简单例子:在项目初期,功能简单,我们可以使用比较简单的日志框架 JUL。因为,它使用起来灵活,不需要导入第三方依赖。随着功能的日益完善,当初的日志框架无法满足现在的需求,那么就得进行日志的升级了,如:切换成 Log4j。
如果从
JUL
切换成Log4j
,那么代码一定会受到影响,且之前记录的日志信息也得修改(改动大),这并不是我们程序员希望看到的。为了解决这一问题,Apache 组织站出来了,它将当时主流的日志框架(JUL
、Log4j
)统一一套 API。那么,后期在软件开发阶段需要关联一套统一的 API 就可以操作某一个日志实现框架的具体日志记录了。即:日志框架修改了,但这套 API 是没变的。而这套 API 就是指JCL
。
3、JCL
3.1 JCL介绍
JCL
:全称 Jakarta Common Logging ,是 Apache 提供的一个通用日志 API。
它为 “所有的 Java 日志实现”提供了一个统一的接口,它自身也提供了一个日志的实现,但功能非常弱(impleLog
)。所以,一般不会单独使用它。它允许开发者使用不同的具体日志实现工具:Log4j、JUL
。
JCL
有两个基本的抽象类:Log、LogFactory(负责创建 Log)
3.2 使用案例
新建一个 Maven 工程,引入一个 commons
依赖,POM 文件:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
public class JclTest {
@Test
public void testQuick() {
Log log = LogFactory.getLog(JclTest.class);
log.info("info");
log.error("error");
}
}
运行后:
这种格式,由于没有导入日志的实现依赖,就使用默认的 JDK14 Logger
(JUL)
导入Log4j
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
运行后:
弹出警告:Log4j
没有系统配置。来,咱们加上 log4j.properties
配置文件
# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=info,使用的 appender 是 console
log4j.rootLogger = trace,console
# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.console.layout.conversionPattern = [%p]%r %l %d{yyyy-MM-dd HH:mm:ss} %m%n
运行后:
这种日志格式,就是使用了 log4j
看,修改日志的实现,不需要修改代码。这就是面向接口设计的理念。
3.3 为什么使用日志门面
- 面向接口开发,不再依赖具体的实现类,减少代码的耦合
- 项目通过导入不同的日志实现类,可以灵活地切换日志框架
- 统一 API,方便开发者学习
- 统一配置,便于项目日志的管理
3.4 JCL原理
根据上面案例我们知道:当我们没有引入日志的实现的依赖时,它会使用日志的默认实现 JUL
;但当我们引入了 log4j
后,自然就使用 log4j
日志。那么,jcl
到底是如何做到的呢?
1、 通过 LogFactory
动态地加载 Log 实现
接口 Log
有 4 个实现类:JDK14Logger
、Log4jLogger
、Jdk13LumberjackLogger
、SimpleLog
通过 LogFactory
动态地获取 Log
(哪个实现类)
2、日志门面支持的日志实现数组
private static final String[] classesToDiscover = {
"org.apache.commons.logging.impl.Log4JLogger",
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};
注意:这个数组中的元素的顺序!!第一个是: Log4JLogger
3、获取具体日志的实现
for(int i=0; i<classesToDiscover.length && result == null; ++i) {
result = createLogFromClass(classesToDiscover[i], logCategory, true);
}
循环 jcl
中已经实现了的 4 个类,然后调用方法 createLogFromClass()
。
重点看方法 createLogFromClass()
:
// 只看重点逻辑
private Log createLogFromClass(String logAdapterClassName,
String logCategory,
boolean affectState)
throws LogConfigurationException {
Class c;
Log logAdapter = null;
for (;;) {
try {
c = Class.forName(logAdapterClassName, true, currentCL);
} catch (NoClassDefFoundError e) {
...
break;
}
constructor = c.getConstructor(logConstructorSignature);
Object o = constructor.newInstance(params);
if (o instanceof Log) {
logAdapterClass = c;
logAdapter = (Log) o;
break;
}
}
return logAdapter;
}
这个方法的逻辑:通过 Class.forName(String className)
来加载 Log
的实现类(来自于 classesToDiscover
数组),如果此类存在,则通过反射创建其实例并返回;如果不存在,则直接返回。
再看看 for
循环中,如果返回的结果不为 null,则跳出循环,继续往下执行;否则,继续循环。
那么,当我们没有引入 log4j 的依赖时,也是存在 Log4JLogger 的(jcl 的实现类)。并且,它是位于要循环的数组中的第一个元素,但是,它并不会加载成功。因为 Log4JLogger 类中引入了 log4j 的依赖,依赖于 log4j。如:import org.apache.log4j.Logger; 所以,在调用 Class.forName(String className) 时会失败。然后继续循环,尝试加载第二个元素 Jdk14Logger,这个依赖就存在 jcl 依赖中,所以,它就会加载成功。
没有引入 log4j
依赖时,org.apache.commons.logging.impl.Log4JLogger
类:
那么,当我们引入 log4j
依赖时,org.apache.commons.logging.impl.Log4JLogger
类就显得可用了,所以,它会一次加载成功,不用循环数组
JCL
只支持以上4种实现类,如果还想支持其它三方日志框架,就得修改 jcl
源码了。难道使用起来这么困难吗?其实不然,早在2014年,jcl
就被 apache 组织给淘汰了,后期就推出了更加优秀的日志门面框架。
4、SLF4j
4.1 SLF介绍
SLF
主要是为了给 java 日志访问提供一套标准、规范的 API 框架。其主要意义在于提供接口,具体的实现可以交由其它日志框架。如:logback
、log4j
、log4j2
。
当然,SLF 自己也提供了较为简单的实现,但一般很少用。对于一般的 java 项目而言,日志框架会选择 slf4j-api 作为门面,配上具体的实现框架(log4j、logback 等。即: slf + logback、slf + log4j2),中间使用桥接器完成桥接。SLF 是目前市面上最流行的日志门面。现在的项目中,基本上都是使用 SLF 作为日志系统。
SLF
日志门面主要提供两大功能:
- 日志框架的绑定
- 日志框架的桥接
4.2 使用案例
新建一个 Maven 工程,引入一个 slf4j-api
依赖,POM 文件:
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<!--slf内置简单的实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
public class SlfTest {
public static final Logger LOGGER = LoggerFactory.getLogger(SlfTest.class);
@Test
public void testQuick() {
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
// 使用占位符
String name = "zzc";
Integer age = 24;
LOGGER.info("用户信息:{}, {}", name, age);
}
}
运行后,结果如下:
4.3 SLF的日志绑定
上面的案例是使用了 SLF
绑定了 简单的日志实现
。那么,SLF
是如何绑定其它的主流日志框架呢?SLF4J 官网的用户手册 上有详细的介绍:
上面这副图就表明了 SLF
是如何绑定其它的主流日志框架。
如果 Java 应用中需要使用日志记录的话,则首先需要引入 slf4j-api.jar
的依赖,统一日志接口。并且,它需要引入具体的实现,共有三种情况:
- 没有引入具体的实现:那么,日志功能将不能起作用
- 引入了一类实现(上图的蓝色框:
slf4j-logback
、slf4j-simple
、slf4j-nop
),由于它们的设计比SLF
要晚,所以默认遵循SLF
的规范,只需要导入它们的依赖就可使用 - 引入了另一类实现(
log4j
、jdk14
),它们的设计比SLF
要早,在设计之初,并没有遵循SLF
的规范,无法直接进行绑定。所以,需要添加一个适配层Adaptation layer
。通过适配器进行适配,从而间接地遵循了SLF
的规范。
使用 SLF 的日志绑定流程:
1、添加 slf4j-api
的依赖
2、使用 slf4j
的 API 在项目中进行统一的日志记录
3、 绑定具体的日志实现
- 绑定了已经实现sfl4j的日志框架,直接添加对应的依赖
- 绑定了没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
4、slf4j
有且仅有一个日志实现框架的绑定(如果出现多个,默认使用第一个依赖日志实现)
接下来就对剩下的日志实现进行案例演示
logback:
<!--logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
logback-classic
包含了 logback-core
的依赖。
【注意】:引入了 logback-classic
的依赖后,得去掉的 slf4j-simple
的依赖(入门案例时引入的)
入门案例代码不变,运行后:
nop:
slf4j-nop
是日志的开关,引入它后,表示系统关闭日志系统
<!-- nop 日志的开关-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.2</version>
</dependency>
运行后,控制台并不会输出日志信息
log4j:
<!--绑定log4j实现,需要导入适配器-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
需要引入 log4j.properties
,这里就拿之前的就行。
运行结果如下:
jdk14:
<!--绑定jdk14实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.25</version>
</dependency>
运行结果如下:
4.4 SLF的日志绑定原理
LoggerFactory
:Logger
是根据 LoggerFactory
获取的实例
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
LoggerFactory#getILoggerFactory()
:获取具体的工厂实例
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == 0) {
//...
performInitialization();
}
switch(INITIALIZATION_STATE) {
//...
case 3:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
//...
}
LoggerFactory#bind()
:绑定所有的实现类
private static final void bind() {
String msg;
try {
Set<URL> staticLoggerBinderPathSet = null;
if (!isAndroid()) {
// 1.找出所有的日志实现
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
// 2.如果实现类大于1,则会记录下来
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// 3.进行日志实现类org.slf4j.impl.StaticLoggerBinder.class加载
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = 3;
reportActualBinding(staticLoggerBinderPathSet);
//...
} catch (NoClassDefFoundError var2) {
// ...
}
}
说明:
-
findPossibleStaticLoggerBinderPathSet();
:通过类加载器在类路径下(包括 jar 包)查找包含org.slf4j.impl.StaticLoggerBinder.class
类的路径。即:日志实现的 jar 就包含 -
reportMultipleBindingAmbiguity()
:如果引入了多于1个的日志实现,则会记录下来 -
StaticLoggerBinder.getSingleton();
:调用此方法时,会导致类加载,并返回日志实现的单例实例(即使引入了多个日志实现的类,那也只会返回同一个实例)。
4.5 SLF 的日志桥接器
Java 工程中使用的日志框架依赖于 SLF 以外的 API。为了使日志框架看上去是 SLF API 的实现,将使用 SLF 附带了几个桥接模块。这些模块将对 Log4j、jcl 的调用重定向,就好像它们是对 SLF API 一样。
桥接解决的项目中日志的遗留问题,当系统中存在之前的日志 API,可以通过桥接转换到 SLF 的实现。
步骤:
- 先去除之前老的日志框架的依赖
- 添加 SLF 提供的桥接组件
- 为项目添加 SLF 的具体实现
案例 ------ log4j:
假若之前,工程中使用的是 log4j
:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
然后,在类路径下添加:log4j.properties
添加测试代码
public class Log4jTest {
@Test
public void testQuick() {
Logger logger = Logger.getLogger(com.zzc.log.log4j.Log4jTest.class);
logger.fatal("fatal");
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
}
}
运行结果后:
现在,我需要在工程中使用 SLF
API(SLF + logback),而不使用 log4j
。
引入依赖:
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<!--logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!--配置log4j的桥接器-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.24</version>
</dependency>
测试代码不变
运行结果如下:
输出格式:logback
(用的它的实现)
【注意】:桥接器和适配器不能一起使用。否则,就是出现 栈溢出。
5、logback
5.1 logback介绍
logback
是由 log4j 创始人设计的另一个开源日志组件,性能比 log4j
好
logback 主要分为 3 个模块:
-
logback-core
:其它两个模块的基础模块 -
logback-classic
:它是log4j
的一个改良版本,同时它完整实现了slf4j
API -
logback-access
:访问模块与Servlet
容器集成提供通过 http 来访问日志的功能
5.2 使用案例
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<!--logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
public class LogbackTest {
public static final Logger LOGGER = LoggerFactory.getLogger(LogbackTest.class);
@Test
public void testQuick() {
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
}
}
运行结果如下:
表明:logback
的默认级别是 debug
。
5.3 配置文件
logback
依次读取以下类型的配置文件:
- logback.groovy
- logback-test.xml
- logback.xml
如果均不存在,则采用默认配置
- logback 组件之间的关系
- Logger:日志记录器。把它关联到应用的对应的 context 上后,主要用于存放日志对象,也可以定义日志类型、级别
- Appender:用于指定日志输出的目的地。目的地可以是控制台、文件、数据库等
- Layout:负责把事件转换成字符串,格式化日志信息输出。在 logback 中,Layout 对象被封装成 encoder 中
- 控制台 Appender
控制台 Appender:将日志信息输出到控制台,并且可以设置一些日志信息。
在 resources 下新建一个 logback.xml
配置文件
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!--配置集中管理属性-->
<property name="pattern" value="[%-5level] %d{yyyy-mm-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n"></property>
<!--
%-5level:日志级别。向左对齐
%d{yyyy-mm-dd HH:mm:ss.SSS}:日期(格式化)
%c:方法名称
%M:类名
%thread:线程名
%m:日志信息
%n:换行
-->
<!--控制台日志输出的 Appender-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--控制输出流对象,默认为 System.out,可修改为 System.err-->
<target>System.err</target>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--root logger 配置-->
<root level="ALL">
<appender-ref ref="console"></appender-ref>
</root>
</configuration>
配置了配置文件后,运行结果如下:
- 文件 Appender
文件 Appender:将日志信息存放到日志文件中
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!--配置集中管理属性-->
<property name="pattern" value="[%-5level] %d{yyyy-mm-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n"></property>
<property name="log_dir" value="E:/temp/logs"></property>
<!--控制台日志输出的 Appender-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--控制输出流对象,默认为 System.out,可修改为 System.err-->
<target>System.err</target>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--文件日志输出的 Appender-->
<appender name="file" class="ch.qos.logback.core.FileAppender">
<!--日志保存路径-->
<file>${log_dir}/logback.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--root logger 配置-->
<root level="ALL">
<appender-ref ref="console"></appender-ref>
<appender-ref ref="file" />
</root>
</configuration>
当然,如果更想直观地看日志,可将日志文件设置成 html 格式:
<!--html 文件格式-->
<appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
<!--日志保存路径-->
<file>${log_dir}/logback.html</file>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<pattern>%level%d{yyyy-mm-dd HH:mm:ss.SSS}%c%M%L%thread%m</pattern>
</layout>
</encoder>
</appender>
- 日志拆分和归档压缩的 Appender
日志文件较大时,我们不能让日志信息都存储在同一个日志文件里面,所以,我们要将日志信息进行拆分。
<!--日志拆分和归档压缩的 Appender-->
<appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志保存路径-->
<file>${log_dir}/roll_logback.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!--指定拆分规则-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--按照时间和压缩格式声明拆分的文件名-->
<fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd-HH-mm-ss}.log%i.gz</fileNamePattern>
<!--按照文件大小拆分-->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
</appender>
运行结果如下:
日志文件拆分规则:日志压缩文件按照秒拆分(精确到秒),当日志文件超过 1 MB 时,日志文件会再次进行拆分,且日志文件名中的 .log%i.gz
中的 i
就会自加。
- 日志级别过滤器
<!--日志拆分和归档压缩的 Appender-->
<appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志保存路径-->
<file>${log_dir}/roll_logback.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!--指定拆分规则-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--按照时间和压缩格式声明拆分的文件名-->
<fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd-HH-mm-ss}.log%i.gz</fileNamePattern>
<!--按照文件大小拆分-->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
其中,
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
表示:只记录 ERROR
级别的日志信息
- 异步日志
默认情况下,程序运行时,程序和日志记录是同步的。使用异步日志的话,就可以使程序和日志记录不是同步。
<!--异步日志-->
<appender name="asyc" class="ch.qos.logback.classic.AsyncAppender">
<!--指定某个具体的 appender-->
<appender-ref ref="rollFile"></appender-ref>
</appender>
<!--root logger 配置-->
<root level="ALL">
<appender-ref ref="console"></appender-ref>
<appender-ref ref="asyc" />
</root>
- 自定义 logger
<!--root logger 配置-->
<root level="ALL">
<appender-ref ref="console" />
<appender-ref ref="async" />
</root>
<!--自定义Logger对象-->
<logger name="com.zzc.log.logback" level="info" additivity="false">
<appender-ref ref="console" />
</logger>
上述标签含义:自定义 Logger 对象没有继承 rootLogger 对象,但它用到了 console appender,并且只输出 info 级别以上的日志信息。
logger 标签的属性说明:
- name:测试类
LogbackTest
所在的类名 - level:控制日志级别
- additivity:自定义 logger 对象是否继承 rootLogger 对象的功能。为 false,表示不继承 root logger 的功能
6、log4j2
6.1 log4j2介绍
log4j2
是 log4j
的升级版,参考了 logback
的优秀的设计,修复了一些问题,因此,带来了一些重大的提升
6.2 使用案例
目前市面上主流的日志门面是 slf4j
。虽然 log4j2
也是日志门面,但它的实现功能非常强大,性能优越。所以,大家一般还是将 log4j2
看做是日志的实现。slf4j + log4j2
是未来趋势。
新建一个 Maven 工程,引入依赖,POM 文件:
<!--log4j2的日志门面-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<!--log4j2的日志实现-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.10.0</version>
</dependency>
public class Log4j2Test {
public static final Logger LOGGER = LogManager.getLogger(Log4j2Test.class);
@Test
public void testQuick() {
LOGGER.fatal("fatal");
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
}
}
运行结果后:
表明:log4j2
的日志默认级别为 error
。
slf4j + log4j2
:
<!--slf日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<!--使用log4j2的适配器进行绑定-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.10.0</version>
</dependency>
<!--log4j2的日志门面-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<!--log4j2的日志实现-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.10.0</version>
</dependency>
6.3 配置文件
log4j2
默认加载 classpath 下的 log4j2.xml
文件中的配置
<?xml version="1.0" encoding="utf-8" ?>
<!--
status=warn:日志框架本身的输出日志级别
monitorInterval="5":自动加载配置文件的间隔时间,不低于5秒
-->
<Configuration status="warn" monitorInterval="5">
<!--配置集中管理属性-->
<properties>
<property name="LOG_HOME" value="E:/temp/logs"></property>
</properties>
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n"></PatternLayout>
</Console>
<File name="file" fileName="${LOG_HOME}/myfile.log">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n"></PatternLayout>
</File>
<!--使用随机读写流-->
<RandomAccessFile name="accessFile" fileName="${LOG_HOME}/myAcclog.log">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n"></PatternLayout>
</RandomAccessFile>
<!--按照一定规则拆分-->
<RollingFile name="rollingFile" fileName="${LOG_HOME}/myRolllog.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/myRolllog-%d{yyyy-MM-dd-HH-mm}-%i.log">
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n"></PatternLayout>
<!--拆分规则-->
<Policies>
<!--系统启动时,触发并启动拆分规则,生成新的日志文件-->
<OnStartupTriggeringPolicy />
<!--按照文件大小拆分-->
<SizeBasedTriggeringPolicy size="10 MB" />
<!--按照时间节点拆分,规则由filePattern定义-->
<TimeBasedTriggeringPolicy />
</Policies>
<!--在同一个目录下,文件的个数限定30个,超过后覆盖旧的日志信息-->
<DefaultRolloverStrategy max="30" />
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="console" />
</Root>
</Loggers>
</Configuration>
6.4 异步日志
log4j2
最大的特点就是 异步日志,其性能的提升主要也是从异步日志中受益。
log4j2
提供了两种异步方式:AsyncAppender
、AsyncLogger
(常用)
【注意】:配置异步日志,需要引入依赖:
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.4</version>
</dependency>
- AsyncAppender
<Configuration>
...
<File name="file" fileName="${LOG_HOME}/myfile.log">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n"></PatternLayout>
</File>
<!--异步日志配置-->
<Async name="Async">
<AppenderRef ref="file" />
</Async>
<Loggers>
<Root level="info">
<AppenderRef ref="console" />
<AppenderRef ref="Async" />
</Root>
</Loggers>
</Configuration>
运行代码后,结果如下:
- AsyncAppender
AsyncAppender
才是log4j2
的重头戏,也是官方推荐的方式。有两种方式:全局异步、混合异步。
全局异步
全局异步:所有的日志都是异步地记录,在配置文件上不做任何修改。只需要添加一个 log4j2.component.properties
配置文件。
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
混合异步
混合异步:可以在应用中同时使用同步日志和异步日志,这使得日志的配置更加灵活
修改 log4j2.xml
:
<Configuration>
...
<Loggers>
<!--自定义Logger对象 includeLocation="false" 关闭行号信息-->
<AsyncLogger name="com.zzc.log.log4j2" level="info" includeLocation="false" additivity="false">
<AppenderRef ref="console" />
</AsyncLogger>
<Root level="info">
<AppenderRef ref="console" />
<AppenderRef ref="Async" />
</Root>
</Loggers>
</Configuration>
运行代码,结果如下:
表明:没有显示行号。
7. SpringBoot 中的日志
SpringBoot 日志也是开发中常用的日志系统。SpringBoot 默认就是使用 SLF4J
作为日志门面,logback
作为日志实现来记录日志。
7.1 SpringBoot 中的日志设计
SpringBoot 中的日志:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
依赖结构图:
总结:
- SpringBoot 底层使用
logback
作为日志实现 - 使用了
SLF4J
作为日志门面 - 将
JUL
也转换为了SLF4J
- 也可以使用
Log4j2
作为日志门面,但是最终也是通过SLF4J
调用logback
7.2 SpringBoot 中使用日志
1.测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationTests.class);
@Test
public void contextLoads() {
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
}
}
运行结果:
SpringBoot 中的默认日志级别:info
2.配置文件:
application.properties
:
# 指定自定义 Logger 对象日志级别
logging.level.com.zzc.sbexp=debug
# 指定控制台输出信息格式
logging.pattern.console=[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c [%thread] === %m%n
# 指定日志文件存放的目录,文件名默认为 spring.log
logging.file=E://temp/springboot.log
# 指定日志文件消息格式
logging.pattern.file=[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c [%thread] === %m%n
运行结果如下:
3.指定日志配置文件:
给类路径下放上每个日志框架自己的配置文件;SpringBoot 就不使用默认配置的了。
日志框架 | 配置文件 |
logback | logback.xml、logback-spring.xml |
log4j2 | log4j2.xml、log4j2-spring.xml |
jul | logging.properties |
logback.xml
直接就被 SpringBoot 识别了。
logback.xml
:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!--配置集中管理属性-->
<property name="pattern" value="[%-5level] %d{yyyy-mm-dd HH:mm:ss.SSS} %c %M %L [%thread] --- %m%n"></property>
<property name="log_dir" value="E:/temp/logs"></property>
<!--控制台日志输出的 Appender-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--控制输出流对象,默认为 System.out,可修改为 System.err-->
<target>System.err</target>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--自定义Logger对象-->
<logger name="com.zzc.sbexp" level="info" additivity="false">
<appender-ref ref="console" />
</logger>
</configuration>
运行代码后:
4.切换日志配置文件:
将日志框架切换为 log4j2
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--排除logback-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
log4j2.xml
:
<?xml version="1.0" encoding="utf-8" ?>
<Configuration status="warn" monitorInterval="5">
<!--配置集中管理属性-->
<properties>
<property name="LOG_HOME" value="E:/temp/logs"></property>
</properties>
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n"></PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="console" />
</Root>
</Loggers>
</Configuration>
8.使用logback输出mybaits日志
在配置文件种增加自定义logger
<!--mybatis 日志-->
<logger name="com.apache.ibatis" level="debug"/>