本文全面梳理在Java开发中常用的日志框架,注重实战(即将功能实现)。主要面向的读者是架构师养成中的初/中级开发者。由于演示的项目全部采用Maven的方式,所以本文需要的背景知识是了解Maven。所以,如果你想全面认识了解Java日志框架,这篇文章也许很适合你!
本文的主要内容:
为什么需要日志:
目录
日志门面
JCL
slf4j
绑定日志框架
桥接旧的日志框架
日志实现
JUL
Logger之间的父子关系
日志级别Level
配置文件
log4j
三大组件
配置文件
自定义Logger
logback
配置输出到控制台
配置输出到控制台
配置输出到HTML
日志拆分&过滤器
异步日志
自定义Logger
log4j2
slf4j和log4j2组合
log4j2配置文件
异步日志
什么是门面:日志的规范,不做具体的实现,类似于JDBC中的规范
我们为什么要使用日志门面:
Jakarta Commons Logging,是Apache提供的一个通用日志API。
它是为 "所有的Java日志实现"提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常弱(SimpleLog)。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk自带的日志(JUL)
总结:
简单日志门面(Simple Logging Facade For Java)
官方网站: https://www.slf4j.org/
SLF4J是目前市面上最流行的日志门面。现在的项目中,基本上都是使用SLF4J作为我们的日志系统。
SLF4J日志门面主要提供两大功能:
为什么要使用SLF4J作为日志门面?
第一个slf4j实例
创建Maven,添加依赖:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger("net.hackyle"); //看源码知绑定原理
//打印日志信息
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
// 使用占位符输出日志信息
String name = "jack";
Integer age = 18;
logger.info("用户:{},{}", name, age);
// 将系统异常信息写入日志
try {
int i = 1 / 0;
} catch (Exception e) {
// e.printStackTrace();
logger.info("出现异常:", e);
}
}
}
//输出:
// [main] ERROR net.hackyle - error
// [main] WARN net.hackyle - warn
// [main] INFO net.hackyle - info
// [main] INFO net.hackyle - 用户:jack,18
// [main] INFO net.hackyle - 出现异常:
// java.lang.ArithmeticException: / by zero
// at net.hackyle.Main.main(Main.java:23)
org.slf4j
slf4j-api
1.7.27
org.slf4j
slf4j-simple
1.7.27
测试:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger("net.hackyle"); //看源码知绑定原理
//打印日志信息
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
// 使用占位符输出日志信息
String name = "jack";
Integer age = 18;
logger.info("用户:{},{}", name, age);
// 将系统异常信息写入日志
try {
int i = 1 / 0;
} catch (Exception e) {
// e.printStackTrace();
logger.info("出现异常:", e);
}
}
}
//输出:
// [main] ERROR net.hackyle - error
// [main] WARN net.hackyle - warn
// [main] INFO net.hackyle - info
// [main] INFO net.hackyle - 用户:jack,18
// [main] INFO net.hackyle - 出现异常:
// java.lang.ArithmeticException: / by zero
// at net.hackyle.Main.main(Main.java:23)
slf4j可以绑定的日志框架:
注意点:
绑定logback:
org.slf4j
slf4j-api
1.7.27
ch.qos.logback
logback-classic
1.2.3
绑定NOP:
org.slf4j
slf4j-api
1.7.27
org.slf4j
slf4j-nop
1.7.25
绑定log4j:需要导入适配器
org.slf4j
slf4j-api
1.7.27
org.slf4j
slf4j-log4j12
1.7.12
log4j
log4j
1.2.17
绑定JDK自带的日志实现:需要导入适配器即可
org.slf4j
slf4j-api
1.7.27
org.slf4j
slf4j-jdk14
1.7.25
背景:
桥接过程示意图:
桥接操作步骤:
注意:桥接器(使slf4j关联不同)和适配器不能同时部署
总结:
日志门面负责规范API,统一接口。真正实现日志记录的是具体的实现框架们。
日志框架出现的历史顺序:log4j -->JUL--> logback --> log4j2
Java util Logging:
工作过程:
第一个JUL实例
import java.util.logging.Level;
import java.util.logging.Logger;
public class Main {
public static void main(String[] args) throws Exception {
//1.获取日志对象记录器
Logger logger = Logger.getLogger("kyle.Main"); //静态工厂构造器需要传入一个唯一标识,可以采用类全名
//2.日志记录并输出(如果没有指定输出目标,则默认向控制台输出)
logger.log(Level.INFO,"第一个日志记录输出");
//logger.info("第一个日志记录输出"); 等价上一行代码,指定输出级别为INFO
//通过占位符的形式输出
String name = "kyle";
Integer age = 22;
logger.log(Level.INFO, "用户信息:{0},{1}", new Object[]{name,age});//数组
}
}
/*
* 控制台输出:
* 1月 26, 2021 11:41:59 上午 kyle.Main main
* 信息: 第一个日志记录输出
* 1月 26, 2021 11:41:59 上午 kyle.Main main
* 信息: 用户信息:kyle,22
*/
这种父子关系通过树状结构存储,JUL在初始化时会创建一个顶层RootLogger作为所有Logger父Logger,存储上作为树状结构的根节点。
父子结构实例
import java.util.logging.Logger;
public class Main {
public static void main(String[] args) throws Exception {
//Logger的父子关系在代码中是根据静态构造器的传入包结构决定的
Logger logger01 = Logger.getLogger("net.hackyle"); //父Logger
Logger logger02 = Logger.getLogger("net.hackyle.package01"); //子Logger
Logger logger03 = Logger.getLogger("net.hackyle.package02"); //子Logger
//所有日志记录器对象的顶级父元素 class为java.util.logging.LogManager$RootLogger,name为空字符串
System.out.println(logger01.getParent());
System.out.println(logger02.getParent() == logger03.getParent()); //true
System.out.println(logger02.getParent() == logger01); //true
}
}
日志级别由java.util.logging.Level类中的七个静态常量确定:
还有两个特殊的级别:
输出不同的级别日志
import java.util.logging.Logger;
public class Main {
public static void main(String[] args) throws Exception {
//1.获取日志对象记录器
Logger logger = Logger.getLogger("kyle.Main"); //静态工厂构造器需要传入一个唯一标识,可以采用类全名
//2.输出不同级别的日志
logger.severe("severe");
logger.warning("warning");
logger.info("info"); //默认情况下,只会输出INFO以及更高级别的日志信息
logger.config("cofnig");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
}
/*
* 控制台输出:
* 1月 26, 2021 11:52:10 上午 kyle.Main main
* 严重: severe
* 1月 26, 2021 11:52:11 上午 kyle.Main main
* 警告: warning
* 1月 26, 2021 11:52:11 上午 kyle.Main main
* 信息: info
*/
自定义日志输出级别
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
public class Main {
public static void main(String[] args) throws Exception {
//获取日志对象记录器
Logger logger = Logger.getLogger("kyle.Main"); //静态工厂构造器需要传入一个唯一标识,可以采用类全名
//关闭默认的日志级别
logger.setUseParentHandlers(false);
//Handler负责将日志做记录,因为是在我们是将日志输出到控制台,所以使用ConsoleHandler
ConsoleHandler consoleHandler = new ConsoleHandler();
SimpleFormatter simpleFormatter = new SimpleFormatter(); //格式转换
consoleHandler.setFormatter(simpleFormatter);
logger.addHandler(consoleHandler);
//配置级别
logger.setLevel(Level.ALL);
consoleHandler.setLevel(Level.ALL);
//输出到日志文件
FileHandler fileHandler = new FileHandler("C:/users/kyle/desktop/jul.log");
fileHandler.setFormatter(simpleFormatter);
logger.addHandler(fileHandler); //控制台、文件都有日志输出
//输出不同级别的日志
logger.severe("severe");
logger.warning("warning");
logger.info("info"); //默认情况下,只会输出INFO以及更高级别的日志信息
logger.config("cofnig");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
}
/*
* 控制台输出:
* 1月 26, 2021 12:01:20 下午 kyle.Main main
* 严重: severe
* 1月 26, 2021 12:01:20 下午 kyle.Main main
* 警告: warning
* 1月 26, 2021 12:01:20 下午 kyle.Main main
* 信息: info
* 1月 26, 2021 12:01:20 下午 kyle.Main main
* 配置: cofnig
* 1月 26, 2021 12:01:20 下午 kyle.Main main
* 详细: fine
* 1月 26, 2021 12:01:20 下午 kyle.Main main
* 较详细: finer
* 1月 26, 2021 12:01:20 下午 kyle.Main main
* 非常详细: finest
*/
默认日志配置文件:
建立resources目录,将其指定为“资源目录”;新建myLogging文件,格式上述的默认配置文件:
handlers= java.util.logging.ConsoleHandler
.level= ALL
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.maxLocks = 100
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
按照配置文件进行日志记录:
import java.io.InputStream;
import java.util.logging.LogManager;
import java.util.logging.Logger;
public class Main {
public static void main(String[] args) throws Exception {
//读取日志配置文件
InputStream in = Main.class.getClassLoader().getResourceAsStream("myLogging.properties");
//获取日志管理器对象
LogManager logManager = LogManager.getLogManager();
//通过日志管理器加载配置文件
logManager.readConfiguration(in);
//进行日志记录
Logger logger = Logger.getLogger("net.hackyle");
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
}
//输出:
1月 26, 2021 12:47:06 下午 kyle.Main main
严重: severe
1月 26, 2021 12:47:06 下午 kyle.Main main
警告: warning
1月 26, 2021 12:47:06 下午 kyle.Main main
信息: info
1月 26, 2021 12:47:06 下午 kyle.Main main
配置: config
1月 26, 2021 12:47:06 下午 kyle.Main main
详细: fine
1月 26, 2021 12:47:06 下午 kyle.Main main
较详细: finer
1月 26, 2021 12:47:06 下午 kyle.Main main
非常详细: finest
总结:
第一个log4j程序(版本2.14)
导包(缺一不可):
log4j-1.2-api-2.14.0.jar
log4j-api-2.14.0.jar
log4j-core-2.14.0.jar
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
public class Main {
public static void main(String[] args) throws Exception {
// 初始化配置信息,在入门案例中暂不使用配置文件
BasicConfigurator.configure();
// 获取日志记录器对象
Logger logger = Logger.getLogger(Main.class); //注意包的位置
logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃并终止运行
logger.error("error"); //错误信息,不会影响系统运行
logger.warn("warn"); //警告信息,可能会发生问题
logger.info("info"); //运行信息,数据连接、网络连接、IO 操作等等
logger.debug("debug"); //默认:调试信息,一般在开发中使用,记录程序变量参数传递信息等等
logger.trace("trace"); //追踪信息,记录程序所有的流程信息
}
}
//输出:
// 14:49:27.021 [main] FATAL kyle.Main - fatal
// 14:49:27.031 [main] ERROR kyle.Main - error
第一个log4j程序(版本1.2.17)
新建Maven项目,导入依赖
log4j
log4j
1.2.17
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
public class Main {
public static void main(String[] args) {
BasicConfigurator.configure();
// 获取日志记录器对象
Logger logger = Logger.getLogger(Main.class); //注意包的位置
logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃并终止运行
logger.error("error"); //错误信息,不会影响系统运行
logger.warn("warn"); //警告信息,可能会发生问题
logger.info("info"); //运行信息,数据连接、网络连接、IO 操作等等
logger.debug("debug"); //默认:调试信息,一般在开发中使用,记录程序变量参数传递信息等等
logger.trace("trace"); //追踪信息,记录程序所有的流程信息
//输出:
//0 [main] FATAL net.hackyle.Main - fatal
//1 [main] ERROR net.hackyle.Main - error
//1 [main] WARN net.hackyle.Main - warn
//1 [main] INFO net.hackyle.Main - info
//1 [main] DEBUG net.hackyle.Main - debug
}
}
Loggers(记录器)
记录器功能:
自log4j 1.2版以来, Logger 类已经取代了Category 类。对于熟悉早期版本的log4j的人来说,Logger 类可以被视为Category 类的别名。
日志级别:
还有两个特殊的级别:
特性:
输出源Appenders
布局:
Log4j支持两种配置文件格式:
第一个Properities配置文件实例
新建Maven项目,导入依赖(版本1.2.17),在resources目录下建立一个log4j.properties:
# rootLogger:顶级父元素配置
# 指定日志级别,指定日志的输出位置,用逗号分隔
log4j.rootLogger = trace,console
# 指定朝输出控制台输出
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定日志的输出格式
log4j.appender.console.layout = org.apache.log4j.PatternLayout
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
public class Main {
public static void main(String[] args) {
BasicConfigurator.configure();
// 获取日志记录器对象
Logger logger = Logger.getLogger(Main.class); //注意包的位置
logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃并终止运行
logger.error("error"); //错误信息,不会影响系统运行
logger.warn("warn"); //警告信息,可能会发生问题
logger.info("info"); //运行信息,数据连接、网络连接、IO 操作等等
logger.debug("debug"); //默认:调试信息,一般在开发中使用,记录程序变量参数传递信息等等
logger.trace("trace"); //追踪信息,记录程序所有的流程信息
//输出:
//fatal
//0 [main] FATAL net.hackyle.Main - fatal
//error
//1 [main] ERROR net.hackyle.Main - error
//warn
//1 [main] WARN net.hackyle.Main - warn
//info
//1 [main] INFO net.hackyle.Main - info
//debug
//1 [main] DEBUG net.hackyle.Main - debug
//trace
//1 [main] TRACE net.hackyle.Main - trace
}
}
log4j.properties
# rootLogger:顶级父元素配置
# 指定日志级别,指定日志的输出位置,用逗号分隔
# log#4j.rootLogger = trace,console
# 自定义Logger
# 格式:log4j.logger.自己定义名字 = 日志级别,输出位置
# 在继承过程中,日志级别会覆盖,输出位置会继承
# 在名字中,可以是使用点来区分父子关系:
# log4j.logger.Aa = trace,console 是Bb,Cc的父,Aa的父是rootLogger
# log4j.logger.Aa.Bb = trace,console 父是Aa
# log4j.logger.Aa.Cc = trace,console 父是Aa
log4j.logger.Aa = info,file
# 日志文件输出的 appender 对象
log4j.appender.file = org.apache.log4j.FileAppender
# 指定消息格式 layout
log4j.appender.file.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.file.layout.conversionPattern = [%-10p]%r %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n
# 指定日志文件保存路径
log4j.appender.file.file = C:/users/kyle/desktop/aa.log
# 指定日志文件的字符集
log4j.appender.file.encoding = UTF-8
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
public class Main {
public static void main(String[] args) {
// 获取日志记录器对象
Logger logger = Logger.getLogger("Aa"); //这里需要传入在配置文件中自定义Logger的名字
logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃并终止运行
logger.error("error"); //错误信息,不会影响系统运行
logger.warn("warn"); //警告信息,可能会发生问题
logger.info("info"); //运行信息,数据连接、网络连接、IO 操作等等
logger.debug("debug"); //默认:调试信息,一般在开发中使用,记录程序变量参数传递信息等等
logger.trace("trace"); //追踪信息,记录程序所有的流程信息
}
}
执行结果:
Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好。
官方网站:https://logback.qos.ch/index.html
Logback主要分为三个模块:
logback组件
logback会依次读取以下类型配置文件:
如果以上三种配置文件均不存在,则会采用默认配置。
官方提供的log4j.properties转换成logback.xml:https://logback.qos.ch/translator/
第一个logback程序
创建Maven项目,导入依赖:
org.slf4j
slf4j-api
1.7.25
ch.qos.logback
logback-classic
1.2.3
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
//slf4j为日志门面,logback为日志实现
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger("net.hackyle");
//打印日志信息
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
// 使用占位符输出日志信息
String name = "jack";
Integer age = 18;
logger.info("用户:{},{}", name, age);
// 将系统异常信息写入日志
try {
int i = 1 / 0;
} catch (Exception e) {
// e.printStackTrace();
logger.info("出现异常:", e);
}
}
}
//输出:
//1月 27, 2021 9:56:21 上午 net.hackyle.Main main
//严重: error
//1月 27, 2021 9:56:21 上午 net.hackyle.Main main
//警告: warn
//1月 27, 2021 9:56:21 上午 net.hackyle.Main main
//信息: info
//1月 27, 2021 9:56:21 上午 net.hackyle.Main main
//信息: 用户:jack,18
//1月 27, 2021 9:56:21 上午 net.hackyle.Main main
//信息: 出现异常:
//java.lang.ArithmeticException: / by zero
//at net.hackyle.Main.main(Main.java:23)
新建Maven项目,导入依赖上文中的slf4j和logback依赖
在resources资源文件夹下新建logback.xml配置文件:
System.err
${outputPattern}
测试:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
//slf4j为日志门面,logback为日志实现
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger("net.hackyle");
//打印日志信息
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
}
}
//输出:(检查是否为配置文件中指定的输出格式)
//[ERROR] 2021-01-27 10:26:37.171 net.hackyle main 11 [main] error
//[WARN ] 2021-01-27 10:26:37.178 net.hackyle main 12 [main] warn
//[INFO ] 2021-01-27 10:26:37.179 net.hackyle main 13 [main] info
//[DEBUG] 2021-01-27 10:26:37.181 net.hackyle main 14 [main] debug
//[TRACE] 2021-01-27 10:26:37.181 net.hackyle main 15 [main] trace
新建Maven项目,导入依赖上文中的slf4j和logback依赖
在resources资源文件夹下新建logback.xml配置文件:
${log_dir}/logback.log
${outputPattern}
测试方式同上一个。
logback.xml
${log_dir}/logback.html
%-5level%d{yyyy-MM-dd HH:mm:ss.SSS}%c%M%L%thread%m
logback.xml
${log_dir}/roll_logback.log
${outputPattern}
${log_dir}/rolling.%d{yyyy-MM-dd}.log%i.gz
1MB
ERROR
ACCEPT
DENY
背景:
logback.xml实现异步日志
System.err
${outputPattern}
logback.xml
第一个log4j2程序
导入依赖:
org.apache.logging.log4j
log4j-api
2.11.1
org.apache.logging.log4j
log4j-core
2.11.1
测试:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
//log4j2为日志门面,log4j2为日志实现
public static void main(String[] args) {
Logger logger = LogManager.getLogger(Main.class);
logger.fatal("fatal");
logger.error("error"); //默认级别
logger.warn("warn");
logger.info("info");
logger.debug("debug");
logger.trace("trace");
}
}
//输出:
//ERROR StatusLogger No Log4j 2 configuration file found. 提示没有找到配置文件
// Using default configuration (logging only errors to the console),
// or user programmatically provided configurations.
// Set system property 'log4j2.debug' to show Log4j 2 internal initialization logging.
// See https://logging.apache.org/log4j/2.x/manual/configuration.html for instructions on how to configure Log4j 2
//11:21:49.214 [main] FATAL net.hackyle.Main - fatal
//11:21:49.219 [main] ERROR net.hackyle.Main - error
导入依赖:
org.slf4j
slf4j-api
1.7.25
org.apache.logging.log4j
log4j-core
2.11.1
由于log4j2是参考这logback来进行设计的,所以在配置文件上有相似的地方。
在同步的日志中,日志的操作需要主线程去执行,日志后的业务逻辑需要等到日志操作完成后才会去执行。而在异步日志中,一旦需要日志操作,则新开辟一些线程去处理日志,主线程可以继续执行接下来的业务逻辑代码,不影响整体速度。
Log4j2最牛的地方在于异步输出日志时的性能表现
Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger(推荐使用),分别对应前面我们说的Appender组件和Logger组件。
配置异步日志需要添加依赖
com.lmax
disruptor
3.3.4
AsyncAppender方式
log4j2.xml配置文件
D:/logs
%d %p %c{1.} [%t] %m%n
AsyncLogger方式
AsyncLogger才是log4j2 的重头戏,也是官方推荐的异步方式。它可以使得调用Logger.log返回的更快。你可以有两种选择:全局异步和混合异步。
配置混合异步
D:/logs
%d %p %c{1.} [%t] %m%n
com.itheima 日志是异步的,root日志是同步的
使用异步日志需要注意的问题:
总结: