本文全面梳理在Java开发中常用的日志框架,注重实战(即将功能实现)。主要面向的读者是架构师养成中的初/中级开发者。由于演示的项目全部采用Maven的方式,所以本文需要的背景知识是了解Maven。所以,如果你想全面认识了解Java日志框架,这篇文章也许很适合你!
本文的主要内容:
- 日志门面:JCL(Jakarta Commons Logging)、slf4j( Simple Logging Facade for Java)
- 日志的实现:JUL(java util logging)、logback、log4j、log4j2
为什么需要日志:
- 我们写代码的过程中,免不了要输出各种调试信息。在没有使用任何日志工具之前,都会使用 System.out.println 来做到。
- 这么做直观有效,但是有一系列的缺点:不知道这句话是在哪个类,哪个线程里出来的;不知道什么时候前后两句输出间隔了多少时间;无法关闭调试信息,一旦System.out.println多了之后,到处都是输出,增加定位自己需要信息的难度等等。
目录
日志门面
什么是门面:日志的规范,不做具体的实现,类似于JDBC中的规范
我们为什么要使用日志门面:
- 1. 面向接口开发,不再依赖具体的实现类。减少代码的耦合
- 2. 项目通过导入不同的日志实现类,可以灵活的切换日志框架
- 3. 统一API,方便开发者学习和使用
- 4. 统一配置便于项目日志的管理
JCL
Jakarta Commons Logging,是Apache提供的一个通用日志API。
它是为 "所有的Java日志实现"提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常弱(SimpleLog)。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk自带的日志(JUL)
总结:
- JDK内置的日志门面
- 功能弱,性能差,我们几乎不会使用它
slf4j
简单日志门面(Simple Logging Facade For Java)
- 给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。
- 当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。
- 对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。
官方网站: https://www.slf4j.org/
SLF4J是目前市面上最流行的日志门面。现在的项目中,基本上都是使用SLF4J作为我们的日志系统。
SLF4J日志门面主要提供两大功能:
- 日志框架的绑定
- 日志框架的桥接
为什么要使用SLF4J作为日志门面?
- 使用SLF4J框架,可以在部署时迁移到所需的日志记录框架。
- SLF4J提供了对所有流行的日志框架的绑定,例如log4j,JUL,Simple logging和NOP。因此可以在部署时切换到任何这些流行的框架。
- 无论使用哪种绑定,SLF4J都支持参数化日志记录消息。由于SLF4J将应用程序和日志记录框架分离,因此可以轻松编写独立于日志记录框架的应用程序。而无需担心用于编写应用程序的日志记录框架。
- SLF4J提供了一个简单的Java工具,称为迁移器。使用此工具,可以迁移现有项目,这些项目使用日志框架(如Jakarta Commons Logging(JCL)或log4j或Java.util.logging(JUL))到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)
<!--slf4j core 使用slf4j必須添加-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!--slf4j 自带的简单日志实现 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.27</version>
</dependency>
测试:
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可以绑定的日志框架:
注意点:
- 绑定时,只需要把相应的POM依赖导入即可,但是只能导入一种具体的日志框架实现
- 源码不动
绑定logback:
<!--slf4j core 使用slf4j必須添加-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!--logback 日志实现-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
绑定NOP:
<!--slf4j core 使用slf4j必須添加-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!--导入NOP日志实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.25</version>
</dependency>
绑定log4j:需要导入适配器
<!--slf4j core 使用slf4j必須添加-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!--导入log4j的适配器:log4j12-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.12</version>
</dependency>
<!--导入log4j日志的实现-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
绑定JDK自带的日志实现:需要导入适配器即可
<!--slf4j core 使用slf4j必須添加-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.27</version>
</dependency>
<!--导入适配器-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.25</version>
</dependency>
桥接旧的日志框架
背景:
- 一个项目刚开始时比较小,所以就使用JDK原生的日志框架;
- 但是随着项目逐渐扩大,原来的日志框架不满足现在庞大的项目,需要对日志框架进行升级,例如使用logback;
- 为了解决这种情况,SLF4J附带了几个桥接模块,这些模块将对log4j,JCL和java.util.logging API的调用重定向,就好像它们是对SLF4J API一样。
桥接过程示意图:
桥接操作步骤:
- 先去除之前老的日志框架的依赖
- 添加SLF4J提供的桥接组件
- 为项目添加SLF4J的具体实现
注意:桥接器(使slf4j关联不同)和适配器不能同时部署
总结:
- slf4j的绑定:指定日志的实现框架
- slf4j的桥接:换个新的日志实现
1. 把老的日志实现从依赖中删除
2.添加桥接器
3.添加新的日志实现框架
日志实现
日志门面负责规范API,统一接口。真正实现日志记录的是具体的实现框架们。
日志框架出现的历史顺序:log4j -->JUL--> logback --> log4j2
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:过滤器,根据Level确定哪些数据需要记录,哪些不需要。
工作过程:
- 用户使用Logger来进行日志记录,Logger持有若干个Handler,日志的输出操作是由Handler完成的。
- 在Handler在输出日志前,会经过Filter的过滤,判断哪些日志级别过滤放行哪些拦截,Handler会将日志内容输出到指定位置(日志文件、控制台等)。
- Handler在输出日志时会使用Layout,将输出内容进行排版。
第一个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
*/
Logger之间的父子关系
这种父子关系通过树状结构存储,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
}
}
日志级别Level
日志级别由java.util.logging.Level类中的七个静态常量确定:
- SEVERE(最高值):用于记录错误日志
- WARNING:用于记录警告日志
- INFO(默认级别):默认情况下,只会输出INFO以及更高级别的日志信息
- CONFIG:用于记录配置日志
- FINE:用于记录调试日志
- FINER:
- FINEST(最低值):
还有两个特殊的级别:
- OFF,可用来关闭日志记录。
- ALL,启用所有消息的日志记录。
输出不同的级别日志
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
*/
配置文件
默认日志配置文件:
- 根据打断点,开启调试模式,可以得知JUL会加载一个默认的配置文件,其路径为\%JAVAHOME\%jre\lib\logging.properties(JDK1.8)
- JDK11的路径为:\%JAVAHOME\%config\logging.properties
- 我们可以根据默认的配置文件格式,修改出自定义的日志配置文件
建立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
总结:
- Logger及其子父级关系
- 配置文件自定义输出格式、输出路径、日志记录级别
log4j
- Log4j是Apache下的一款开源的日志框架
- 通过在项目中使用 Log4J,我们可以控制日志信息输出到控制台、文件、甚至是数据库中。
- 我们可以控制每一条日志的输出格式,通过定义日志的输出级别,可以更灵活的控制日志的输出过程。方便项目的调试。
- 官方网站: http://logging.apache.org/log4j/1.2/
第一个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项目,导入依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
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(记录器):日志类别和级别,控制那些信息是需要记录的。
- Appenders (输出源):日志要输出的地方,例如输出到C:\users\Admin\desktop\test.log。
- Layouts(布局):日志以何种形式(或格式)输出,例如输出为HTML格式。
Loggers(记录器)
记录器功能:
- 负责收集不同级别和处理日志
- 使用静态工厂构造器获取,Logger的名字是大小写敏感的,可以是类全名、类的字节码文件
- 同JUL,其命名有继承机制:例如:name为org.apache.commons的logger会继承name为org.apache的logger。
- Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。
自log4j 1.2版以来, Logger 类已经取代了Category 类。对于熟悉早期版本的log4j的人来说,Logger 类可以被视为Category 类的别名。
日志级别:
- fatal 指出每个严重的错误事件将会导致应用程序的退出。
- error 指出虽然发生错误事件,但仍然不影响系统的继续运行。
- warn 表明会出现潜在的错误情形。
- info 一般和在粗粒度级别上,强调应用程序的运行全程。
- debug 一般用于细粒度级别上,对调试应用程序非常有帮助。
- trace 是程序追踪,可以用于输出程序运行中的变量,显示执行的流程。
还有两个特殊的级别:
- OFF,可用来关闭日志记录。
- ALL,启用所有消息的日志记录。
特性:
- 只输出级别不低于设定级别的日志信息。例如Loggers级别设定为INFO,则INFO、WARN、ERROR和FATAL级别的日志信息都会输出,而级别比INFO低的DEBUG则不会输出。
输出源Appenders
- org.apache.log4j.ConsoleAppender(控制台)
- org.apache.log4j.FileAppender(文件)
- org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
- org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
- org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
- org.apache.log4j.JDBCAppender(将日志信息保存到数据库)
布局:
- org.apache.log4j.HTMLLayout 格式化日志输出为HTML表格形式
- org.apache.log4j.SimpleLayout 简单的日志输出格式化,打印的日志格式为(info - message)
- org.apache.log4j.PatternLayout 最强大的格式化期,可以根据自定义格式输出日志,如果没有指定转换格式,就是用默认的转换格式
- org.apache.log4j.TTCCLayout 包含日志产生的时间、线程、类别等信息
配置文件
Log4j支持两种配置文件格式:
- 一种是XML格式的文件
- 一种是properties属性文件
第一个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
}
}
自定义Logger
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
Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好。
官方网站:https://logback.qos.ch/index.html
Logback主要分为三个模块:
- logback-core:其它两个模块的基础模块
- logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
- logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能
logback组件
- Logger:日志的记录器,把它关联到应用的对应的context上后,主要用于存放日志对象,也可以定义日志类型、级别。
- Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等。
- Layout:负责把事件转换成字符串,格式化的日志信息的输出。在logback中Layout对象被封装在encoder中。
logback会依次读取以下类型配置文件:
- logback.groovy
- logback-test.xml
- logback.xml
如果以上三种配置文件均不存在,则会采用默认配置。
官方提供的log4j.properties转换成logback.xml:https://logback.qos.ch/translator/
第一个logback程序
创建Maven项目,导入依赖:
<!--日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!--日志的具体实现-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
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配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--主要步骤:-->
<!-- 1.配置日志输出格式-->
<!-- 2.配置输出地-->
<!-- 3.配置日志记录器-->
<!--配置日志格式-->
<!--name:为这个输出格式取个名字;value:输出的格式控制-->
<!--后期调用:${outputPattern}-->
<property name="outputPattern" 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为method
%L为行号
%thread线程名称
%m或者%msg为信息
%n换行
-->
<!--配置日志输出地-->
<!--name:为这个输出地取个名字;class:具体的输出实现-->
<appender name="outputConsole" class="ch.qos.logback.core.ConsoleAppender">
<!--控制输出流对象 默认 System.out 改为 System.err-->
<target>System.err</target>
<!--日志消息格式配置-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${outputPattern}</pattern>
</encoder>
</appender>
<!--配置日志记录器-->
<!--使用根日志记录器,指定输出级别为ALL-->
<root level="ALL">
<appender-ref ref="outputConsole" />
</root>
</configuration>
测试:
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配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--主要步骤:-->
<!-- 1.配置日志输出格式-->
<!-- 2.配置输出地-->
<!-- 3.配置日志记录器-->
<!--配置日志格式-->
<!--name:为这个输出格式取个名字;value:输出的格式控制-->
<!--后期调用:${outputPattern}-->
<property name="outputPattern" 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为method
%L为行号
%thread线程名称
%m或者%msg为信息
%n换行
-->
<!--定义日志文件保存路径属性-->
<property name="log_dir" value="C:/users/kyle/desktop/log/" />
<!--配置日志输出地-->
<!--name:为这个输出地取个名字;class:具体的输出实现-->
<appender name="output2File" class="ch.qos.logback.core.FileAppender">
<!--日志文件保存路径-->
<file>${log_dir}/logback.log</file>
<!--日志消息格式配置-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${outputPattern}</pattern>
</encoder>
</appender>
<!--配置日志记录器-->
<!--使用根日志记录器,指定输出级别为ALL-->
<root level="ALL">
<appender-ref ref="output2File" />
<!--如果即想要控制台输出,又想要文件输出,则在这里多配置即可-->
<!--<appender-ref ref="output2Console" />-->
<!--<appender-ref ref="output2Database" />-->
</root>
</configuration>
测试方式同上一个。
配置输出到HTML
logback.xml
<!--html 格式日志文件输出 appender-->
<appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
<!--日志文件保存路径-->
<file>${log_dir}/logback.html</file>
<!--html 消息格式配置-->
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<!-- 具体的格式,类似于property标签中的输出属性 -->
<pattern>%-5level%d{yyyy-MM-dd HH:mm:ss.SSS}%c%M%L%thread%m</pattern>
</layout>
</encoder>
</appender>
日志拆分&过滤器
logback.xml
<!--日志拆分和归档压缩-->
<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>${outputPattern}</pattern>
</encoder>
<!--指定拆分规则-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--按照时间和压缩格式声明拆分的文件名-->
<fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern>
<!--按照文件大小拆分-->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
<!--日志级别过滤器-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--日志过滤规则:超过ERROR级别的日志才进行输出到日志文件-->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
异步日志
背景:
- 到目前为止,日志的记录都是在主线程中的,日志后的代码要等到日志记录操作完毕后才能继续往下执行。
- 如果要实现日志记录完全不占用主线程,而是自己新开一个线程去处理记录日志的操作,这就需要异步日志实现。
logback.xml实现异步日志
<!--配置日志输出地-->
<!--name:为这个输出地取个名字;class:具体的输出实现-->
<appender name="outputConsole" class="ch.qos.logback.core.ConsoleAppender">
<!--控制输出流对象 默认 System.out 改为 System.err-->
<target>System.err</target>
<!--日志消息格式配置-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${outputPattern}</pattern>
</encoder>
</appender>
<!--异步日志-->
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<!--指定某个具体的 appender-->
<appender-ref ref="outputConsole"/>
</appender>
自定义Logger
logback.xml
<!--自定义 looger 对象
name:取个名字,这里通过包的形式也会产生父子的继承关系
例如:net.hackyle.aa就继承与net.hackyle
level:只要级别超过了的,才会输出到日志
additivity:自定义logger对象是否继承rootLogger,false表明不继承
-->
<logger name="net.hackyle" level="info" additivity="false">
<!-- 输出地 -->
<appender-ref ref="output2File"/>
</logger>
log4j2
- Apache Log4j 2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升
- 官网: https://logging.apache.org/log4j/2.x/
- 目前市面上最主流的日志门面就是SLF4J,虽然Log4j2也是日志门面,因为它的日志实现功能非常强大,性能优越。所以大家一般还是将Log4j2看作是日志的实现,Slf4j + Log4j2应该是未来的大势所趋。
第一个log4j2程序
导入依赖:
<!-- Log4j2 门面API:目前主流不会直接使用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.11.1</version>
</dependency>
测试:
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
slf4j和log4j2组合
导入依赖:
<!--slf4j日志门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
<!-- Log4j2 日志实现 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
log4j2配置文件
由于log4j2是参考这logback来进行设计的,所以在配置文件上有相似的地方。
异步日志
在同步的日志中,日志的操作需要主线程去执行,日志后的业务逻辑需要等到日志操作完成后才会去执行。而在异步日志中,一旦需要日志操作,则新开辟一些线程去处理日志,主线程可以继续执行接下来的业务逻辑代码,不影响整体速度。
Log4j2最牛的地方在于异步输出日志时的性能表现
Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger(推荐使用),分别对应前面我们说的Appender组件和Logger组件。
配置异步日志需要添加依赖
<!--异步日志依赖-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.4</version>
</dependency>
AsyncAppender方式
log4j2.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<properties>
<property name="LOG_HOME">D:/logs</property>
</properties>
<Appenders>
<File name="file" fileName="${LOG_HOME}/myfile.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
</File>
<Async name="Async">
<AppenderRef ref="file"/>
</Async>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="Async"/>
</Root>
</Loggers>
</Configuration>
AsyncLogger方式
AsyncLogger才是log4j2 的重头戏,也是官方推荐的异步方式。它可以使得调用Logger.log返回的更快。你可以有两种选择:全局异步和混合异步。
- 全局异步就是,所有的日志都异步的记录,在配置文件上不用做任何改动,只需要添加一个log4j2.component.properties 配置;
- 混合异步就是,你可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活。但是注意:如果要使用混合异步,那就要把全局异步的关闭。
配置混合异步
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<properties>
<property name="LOG_HOME">D:/logs</property>
</properties>
<Appenders>
<File name="file" fileName="${LOG_HOME}/myfile.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
</File>
<!--开启异步-->
<Async name="Async">
<AppenderRef ref="file"/>
</Async>
</Appenders>
<Loggers>
com.itheima 日志是异步的,root日志是同步的
<AsyncLogger name="com.itheima" level="trace" includeLocation="false" additivity="false">
<AppenderRef ref="file"/>
</AsyncLogger>
<Root level="info" includeLocation="true">
<AppenderRef ref="file"/>
</Root>
</Loggers>
</Configuration>
使用异步日志需要注意的问题:
- 如果使用异步日志,AsyncAppender、AsyncLogger和全局日志,不要同时出现。要么使用呢全局异步,要么使用混合异步。如果同时出现,性能会和AsyncAppender一致,降至最低。
- 设置includeLocation=false ,打印位置信息会急剧降低异步日志的性能,比同步日志还要慢。
总结:
- 日志门面:
- JCL(Jakarta Commons Logging):JDK内置,弱,基本不用
- slf4j( Simple Logging Facade for Java):强,流行使用
- 日志的实现:
- JUL(java util logging):JDK内置,小项目中用
- log4j:可以将日志记录到控制台、文件、数据库中,支持properties和xml的文件格式
- logback:除了log4j的功能外,还支持将日志过滤、日志拆分、异步日志等
- log4j2:拥有logback的全部功能,性能更强,尤其是异步日志。
- slf4j+log4j2的组合方式是当下最流行的方式。