目录 日志概述 日志体系概述 混乱的日志体系 上古时代 开创先驱 搞事情的 JUL JCL 应运而生 再起波澜 再度青春 配置讲解 日志级别 日志组件 JUL log4j logback logback 组件之间的关系 基本配置信息 log4j2 jcl slf4j Spring Boot 中的日志使用 日志使用建议 门面约束 单一原则 依赖约束 避免传递 注意写法 减少分析 精简至上 附录 日志概述 ❓为什么需要日志?
在很多的情况下,日志可能是我们了解应用如何执行的唯一方式,所以学习日志非常有必要。更何况发展到今天的软件系统已经非常复杂,特别时服务器的软件,设计到的知识和内容问题非常的多,有了日志才能够更方便了解系统运行过程中出现的种种问题。通过日志可以处理历史数据、诊断问题的追踪以及理解系统的活动。
根据日志记录的内容不通,可以人为的将日志分为两种类型的日志:调试日志 和系统日志
在软件开发中,需要经常的调试程序,或者做一些状态的输出,便于我们查询程序的运行状况,为了让我们能够更加灵活且方便的控制这些调试信息,我们肯定是需要更加专业的日志技术。我们平时在调试程序的过程中所使用的肯定就是专业开发工具自带的 debug 功能,可以实时查看程序运行情况,不能够有效保存运行情况的信息。调试日志是能够更加方便的去「 重现 」这些问题
系统日志是实际生产环境中最常使用的日志,经常用来记录系统中硬件、软件和系统相关问题的信息,同时还可以监视系统中发生的事件,用户可以通过它来检查错误发生的原因,或者寻找到攻击时留下的痕迹。系统日志包括系统运行日志、应用日志和安全日志这几种
日志体系概述 在 Java 中一直秉持着面向接口编程,所以任何优秀的框架一定是先存在接口,而后再是根据这些接口做到不同的实现,Java 日志体系如下图:
Java 中相关的日志门面系统概述如下:
接口 含义 JCL
Java Commons Loggin,Apache 基⾦会所属的项⽬,是⼀套 Java ⽇志接⼝ SLF4J
Simple Logging Facade for Java,是一套简单的 Java 日志门面,只提供相关接口,和其他日志之间需要桥接
门面其实就是表示,这是一套接口 ,并没有给出 Java 日志的具体实现方式,其中所说的桥接,指的是桥接模式
❓为什么要使用日志门面技术
每一种日志框架都有自己单独的 API,要使用对应的框架就要使用对应的 API,这就大大的增加了应用程序代码对于日志框架的耦合性。使用日志门面技术之后,对于应用程序来说,无论底层的日志框架如何改变,应用程序不需要修改任何一行代码就可以上线运行
Java 中相关的日志实现概述如下:
日志实现 含义 JUL
JDK 中的⽇志⼯具 Log4j
⾪属于 Apache 基⾦会的⼀套⽇志框架,现已不再维护 Log4j2
Log4j 的升级版本,与 Log4j 变化很⼤,不兼容 Logback
⼀个具体的⽇志实现框架,和 Slf4j 是同⼀个作者,性能很好
混乱的日志体系 Java 的日志体系非常的混乱,本小节将主要阐述 Java 日志体系的发展过程
上古时代 在 JDK 1.3 及以前,Java 打⽇志依赖 System.out.println()
, System.err.println()
或者 e.printStackTrace()
,Debug ⽇志被写到 STDOUT 流,错误⽇志被写到 STDERR 流。这样打⽇志有⼀个⾮常⼤的缺陷:日志记录⾮常机械,⽆法定制,且⽇志粒度不够细分
开创先驱 随后,在 2001 年发布了 Log4j
,Log4j 在设计上⾮常优秀, 它定义的 Logger、Appender、Level 等概念对后续的 Java Log 框架有深远的影响,如今的很多⽇志框架基本沿⽤了这种思想。Log4j 的性能是个问题,在 Logback 和 Log4j2
出来之后,2015 年 9 ⽉, Apache 软件基⾦会宣布,Log4j
不再维护,建议所有相关项⽬升级到 Log4j2 ✨ 如果要在项目中使用 log4j,只需要两步就可以了,分别是:引入 log4j 依赖以及配置 log4j
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > log4j</groupId > <artifactId > log4j</artifactId > <version > 1.2.17</version > </dependency > <dependency > <groupId > log4j</groupId > <artifactId > apache-log4j-extras</artifactId > <version > 1.2.17</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 log4j.rootLogger =debug log4j.appender.stdout =org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout =org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern =log4j:[%d{yyyy-MM-dd HH:mm:ssa}]:%p %l%m%n log4j.appender.dailyfile =org.apache.log4j.DailyRollingFileAppender log4j.appender.dailyfile.DatePattern ='_'yyyy-MM-dd'.log' log4j.appender.dailyfile.File =./log4j.log log4j.appender.dailyfile.Append =true log4j.appender.dailyfile.Threshold =INFO log4j.appender.dailyfile.layout =org.apache.log4j.PatternLayout log4j.appender.dailyfile.layout.ConversionPattern =log4j:[%d{yyyy-MM-dd HH:mm:ssa}] [Thread: %t][ Class:%c >> Method: %l ]%n%p:%m%n
搞事情的 JUL sun 公司对于 log4j 的出现内⼼心隐隐表示嫉妒。于是在 jdk1.4 版本后,开始搞事情,增加了一个日志包(java.util.logging
),简称为 JUL,⽤以对抗 log4j ,但是却给开发造成了麻烦,这是因为 JUL 和 log4j 的 API 并不统一,使得项目中日志难以管理。
✨ 使用 JUL 非常的简单,由于是 JDK 中自带的所以并不需要引入额外的依赖,只需要配置即可,默认的配置路径是:$JAVA_HOME/jre/lib/logging.properties
1 2 Logger loggger = Logger.getLogger(Demo.class.getName());logger.finest("xxxx" );
JUL 功能远不如 log4j 完善,⾃带的 Handlers 有限,性能和可用性上也一般,不是非常推荐在项目中使用
JCL 应运而生 由于 JUL 的 api 与 log4j 是完全不同的,并且日志系统互相没有关联,彼此没有约定,不同人的代码使⽤不同日志,替换和统一也就变成了了⽐较棘手的⼀件事。解决这个问题的答案就是进行抽象 ,抽象出一个接口层 ,对每个日志实现都适配或者转接,这样这些提供给别⼈的库都直接使用抽象层即可 ,以后调用的时候,就调用这些接⼝
JCL(Jakarta Commons Logging)应运而生,也就是 commons-logging-xx.jar
组件。JCL 只提供 log 接口,具体的实现则在运行时动态寻找。用户可以自由选择第三方的日志组件作为具体实现,像 log4j,或者 jdk 自带的 jul, common-logging 会通过动态查找的机制,在程序运行时自动找出真正使用的日志库。
common-logging 内部有一个 Simple logger 的简单实现,但是功能很弱。所以使用 common-logging,通常都是配合着 log4j 以及其他日志框架来使用,用它的好处就是,代码依赖是 common-logging 而非 log4j 的 API, 避免了和具体的日志 API 直接耦合,在有必要时,可以更改日志实现的第三方库。
JCL 会在 ClassLoader 中进⾏查找,如果能找到 Log4j 则默认使⽤ log4j 实现,如果没有则使⽤ JUL(jdk ⾃带的) 实现,再没有则使⽤ JCL 内部提供的 SimpleLog 实现,如下图:
如果需要在项目中使用 JCL 同样分为两步:引入依赖以及配置
1 2 3 4 5 <dependency > <groupId > commons-logging</groupId > <artifactId > commons-logging</artifactId > <version > 1.2</version > </dependency >
1 2 3 4 5 6 public class TestLoger { public static void main (String[] args) { Log log = LogFactory.getLog(TestLoger.class); log.info("xxxxxxx" ); } }
JCL 缺点也很明显,⼀是效率较低,二是容易引发混乱,三是 JCL 机制有很⼤的可能会引发内存泄露。同时,JCL 的书写存在⼀个不太优雅的地⽅,典型的场景如下:假如要输出一条 debug 日志,而一般情况下,生产环境 log 级别都会设到 info 或者以上,那这条 log 是不不会被输出的。于是,在代码里就出现了
1 logger.debug("this is a debug info , message :" + msg);
这个有什么问题呢?虽然⽣产不会打出⽇志,但是这其中都会做⼀个字符串连接操作,然后⽣成⼀个新的字符串。如果这条语句在循环或者被调⽤很多次的函数中,就会多做很多⽆⽤的字符串连接非常影响性能,所以,JCL 推荐的写法如下。这样虽然解决了问题,但是这个代码实在看上去不怎么舒服
1 2 3 if (logger.isDebugEnabled()) { logger.debug("this is a debug info , message :" + msg); }
再起波澜 于是,针对以上情况,log4j 的作者再次出⼿手,他觉得 JCL 不好用,⾃己⼜写了一个新的接⼝ api,就 是 slf4j,并且为了追求更极致的性能,新增了一套日志的实现,就是 logback
pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-api</artifactId > <version > 1.7.30</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-core</artifactId > <version > 1.2.3</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > 1.2.3</version > </dependency >
logback-core 提供基础抽象,logback-classic 提供日志实现,并且直接就是基于 Slf4j API。所以 slf4j 配合 logback 来完成日志时,不需要像其他的日志框架一样提供适配器器
slf4j 本身并没有实际的日志输出能力,它底层还是需要去调⽤用具体的日志框架 API,也就是它需要跟具体的⽇志框架结合使用。由于具体日志框架⽐较多,而且互相也大都不兼容,日志⻔⾯接口要想实现 与任意⽇日志框架结合就需要额外对应的桥接器器
slf4j.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?xml version="1.0" encoding="UTF-8" ?> <configuration scan ="true" scanPeriod ="60 seconds" debug ="false" > <property name ="logPattern" value ="logback:[ %-5level] [%date{yyyy-MM-dd HH:mm:ss.SSS}] %logger{96} [%line] [%thread]- %msg%n" ></property > <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <charset > UTF-8</charset > <pattern > ${logPattern}</pattern > </encoder > </appender > <root level ="DEBUG" > <appender-ref ref ="STDOUT" /> </root > </configuration >
有了新的 slf4j 后,上⾯字符串的拼接问题,被以下代码所取代,⽽ logback 也提供了更⾼级的特性,如:异步 logger,Filter 等
1 logger.debug("this is a debug info, message :{}" ,msg);
再度青春 前⾯提到,log4j 由 apache 宣布,2015 年后,不再维护。推荐大家升级到 log4j2,虽然 log4j2 沿袭了 log4j 的思想,然而 log4j2 和 log4j 完全是两码事,并不不兼容。log4j2 以性能著称,它比其前身 Log4j 1.x 提供了重⼤大改进,同时类比 logback,它提供了 Logback 中可用的许多改进,同时修复了 Logback 架构中的一些固有问题。功能上,它有着和 Logback 相同的基本操作,同时又有⾃己独特的部分,⽐如:插件式结构、配置⽂文件优化、异步⽇日志等。
pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-api</artifactId > <version > 2.13.0</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > 2.13.0</version > </dependency > <dependency > <groupId > com.lmax</groupId > <artifactId > disruptor</artifactId > <version > 3.4.1</version > </dependency >
1 2 Logger logger = LogManager.getLogger(Demo.class);logger.debug("debuff Msg" );
log4j2.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?xml version="1.0" encoding="UTF-8" ?> <configuration status ="info" monitorInterval ="30" > <Properties > <Property name ="pattern" > log4j2:[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n</Property > </Properties > <appenders > <Console name ="Console" target ="SYSTEM_OUT" > <PatternLayout pattern ="${pattern}" /> </Console > </appenders > <loggers > <logger name ="org.springframework" level ="INFO" > </logger > <root level ="info" > <appender-ref ref ="Console" /> </root > </loggers > </configuration >
但是最近出现的 log4j2 安全隐患问题,让我有点怀疑是否要进一步使用这个日志框架
配置讲解 众多 Java 日志的使用并不是日志框架的难点,重点在于:如何使用配置文件,定制化日志
日志级别 ⼀个完整的⽇志组件都要具备⽇志级别的概念,每种⽇志组件级别定义不同,⽇常编码最经常⽤到的主流分级如下(由低到⾼)
日志级别 含义 trace
路径跟踪 debug
⼀般⽤于⽇常调式 info
打印重要信息 warn
给出警告 error
出现错误或问题
级别 级别含义 SEVERE 错误——最高的日志级别 WARNING 警告 INFO (默认级别)信息 CONFIG 配置 FINE 详细信息(少) FINER 详细信息(中) FINEST 详细信息(多)——最低级的日志级别
两个特殊的级别
级别 含义 OFF 可用来关闭日志记录 ALL 启用所有消息的日志记录
对于日志的级别,重点关注的是 new 对象的时候的第二个参数,是一个数值
1 public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);
级别 数值 OFF Integer.MAX_VALUE SEVERE 1000 ALL Integer.MIN_VALUE
这个数值的意义在于,如果设置的日志级别是 INFO-800,最终展现的日志信息,必须是数值大于 800 的所有的日志信息,最终展现的就是:SERVER,WARNING,INFO
1 2 Logger logger = Logger.getLogger("com.pineappltman.test.JulLearning" );logger.setLevel(Level.CONFIG);
如果仅仅只是通过以下形式来设置日志级别,那么不能起到效果,将来需要搭配处理器 handler 共同设置才会生效
日志组件 日志组件 含义 appender ⽇志输出⽬的地,负责⽇志的输出 (输出到什么 地⽅) logger ⽇志记录器,负责收集处理⽇志记录 (如何处理⽇志) layout ⽇志格式化,负责对输出的⽇志格式化(以什么形式展现)
JUL 默认情况下配置文件的路径为$JAVAHOME\jre\lib\logging.properties
,可以通过编码的方式指定配置文件
1 2 3 4 5 6 static { System.setProperty("java.util.logging.config.file" , Demo.class.getClassLoader().getResource("logging.properties" ) .getPath() ); }
JUL 中的级别有:SEVER(最高值)、WARNING、INFO、CONFIG、FINE、FINER、FINEST(最小值)、OFF(关闭日志)、ALL(启动所有日志) JUL 中的处理器如下:
处理器 含义 StreamHandler ⽇志记录写⼊ OutputStream ConsoleHandler ⽇志记录写⼊ System.err FileHandler 日志记录写入单个文件或一组滚动日志文件 SocketHandler ⽇志记录写⼊远程 TCP 端口的处理程序 MemoryHandler 缓冲内存中⽇日志记录
格式化
格式化 含义 SimpleFormatter 格式化为简短的⽇日志记录摘要 XMLFormatter 格式化为详细的 XML 结构信息 输出格式可以自定义输出,只要继承自抽象类 java.util.logging.Formatter
即可
log4j 启动时,默认会寻找 resorce folder
下的 log4j.xml
,若没有,会寻找 lo4g.properties
Loggers(日志记录器),Appenders(输出控制器)和 Layout(日志格式化器)组成。其中 Loggers 控制日志的输出以及输出级别(JUL 做日志级别 Level);Appenders 指定日志的输出方式(输出到控制台、文件等);Layout 控制日志信息的输出格式
logback Logback 是由 log4j 创始人设计的另一个开源日志组件,性能比 log4j 要好,Logback 主要分为三个模块:
logback-core
:其他两个模块的基础模块logback-classic
:它是 log4j 的一个改良版本,同时它完整实现了 slf4j APIlogback-access
:访问模块与 Servlet 容器集成提供通过 Http 来访问日志的功能pom.xml 1 2 3 4 5 6 7 8 9 10 <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 >
LogBackTest.java 1 2 3 4 5 6 7 8 9 10 public final static Logger LOGGER = LoggerFactory.getLogger(LogBackTest.class);@Test public void testSlf4j () { LOGGER.error("error" ); LOGGER.warn("warn" ); LOGGER.info("info" ); LOGGER.debug("debug" ); LOGGER.trace("trace" ); }
logback 会依次读取一下类型配置文件: logback.groovy
,logback-test.xml
和logback.xml
如果均不存在,会采用默认的配置
logback 组件之间的关系 Logger:日志的记录器,把它关联到应用的对应的 context 上后,主要用于存放日志对象,也可以定义日志类型、级别 Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等 Layout:负责把事件转换成字符串,格式化的日志信息的输出。在 logback 中 Layout 对象被封装在 encoder 中 基本配置信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <property name ="pattern" value ="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]%-5level %msg%n" /> <appender name ="console" class ="ch.qos.logback.core.ConsoleAppender" > <target > System.err</target > <encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder" > <pattern > ${pattern}</pattern > </encoder > </appender > <root level ="ALL" > <appender-ref ref ="console" /> </root > </configuration >
log4j2 Log4j will inspect the log4j.configurationFile
system property and, if set, will attempt to load the configuration using the ConfigurationFactory
that matches the file extension. If no system property is set the YAML ConfigurationFactory will look for log4j2-test.yaml
or log4j2-test.yml
in the classpath If no such file is found the JSON ConfigurationFactory will look for log4j2-test.json
or log4j2-test.jsn
in the classpath If no such file is found the XML ConfigurationFactory will look for log4j2-test.xml
in the classpath. If a test file cannot be located the YAML ConfigurationFactory will look for log4j2.yaml
or log4j2.yml
on the classpath If a YAML file cannot be located the JSON ConfigurationFactory will look for log4j2.json
or log4j2.jsn
on the classpath. If a JSON file cannot be located the XML ConfigurationFactory will try to locate log4j2.xml
on the classpath If no configuration file could be located the DefaultConfiguration
will be used. This will cause logging output to go to the console. log4j2 的级别从低到高是:
1 ALL -> TRACE -> DEBUG -> INFO -> WARN -> ERROR -> FATAL -> OFF
处理器 含义 FileAppender 输出到本地⽂件 KafkaAppender 输出到 kafka 队列 FlumeAppender 将几个不同源的日志汇集、集中到⼀处 JMSQueueAppender/JMSTopicAppender 与 JMS 相关的日志输出 RewriteAppender 对日志事件进⾏掩码或注⼊信息 RollingFileAppender 对日志文件进行封存 RoutingAppender 在输出地之间进行筛选路由 SMTPAppender 将 LogEvent 发送到指定邮件列表 SocketAppender 将 LogEvent 以普通格式发送到远程主机 SyslogAppender 将 LogEvent 以 RPF5424 格式发送到远程主机 AsynchAppender 将一个 LogEvent 异步地写入多个不同输出地 ConsoleAppender 将 LogEvent 输出到命令行 FailoverAppender 维护一个队列,系统尝试向队列中的 Appender 依次输出 LogEvent,直到有一个成功为止
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <?xml version="1.0" encoding="UTF-8" ?> <configuration status ="info" monitorInterval ="30" > <Properties > <Property name ="pattern" > log4j2:[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t]%c{1}:%L - %msg%n</Property > </Properties > <appenders > <Console name ="Console" target ="SYSTEM_OUT" > <PatternLayout pattern ="${pattern}" /> </Console > <File name ="File" fileName ="./log4j2-file.log" append ="false" > <PatternLayout pattern ="${pattern}" /> </File > <RollingFile name ="RollingFile" fileName ="./log4j2-rollingfile.log" filePattern ="./$${date:yyyy-MM}/log4j2-%d{yyyy-MM-dd}-%i.log" > <ThresholdFilter level ="debug" onMatch ="ACCEPT" onMismatch ="DENY" /> <PatternLayout pattern ="${pattern}" /> <Policies > <TimeBasedTriggeringPolicy interval ="6" modulate ="true" /> <SizeBasedTriggeringPolicy size ="1 MB" /> </Policies > <DefaultRolloverStrategy max ="20" /> </RollingFile > </appenders > <loggers > <AsyncLogger name ="AsyncLogger" level ="trace" includeLocation ="true" additivity ="true" > <appender-ref ref ="Console" /> </AsyncLogger > <logger name ="Kafka" additivity ="false" level ="debug" > <appender-ref ref ="Kafka" /> <appender-ref ref ="Console" /> </logger > <root level ="info" > <appender-ref ref ="Console" /> </root > </loggers > </configuration >
jcl 配置文件,首先在 classpath 下寻找 commons-logging.properties
文件。如果找到,则使⽤其中定义的 Log 实现类;如果找不到,则再查找是否已定义系统环境变量 org.apache.commons.logging.Log
,找到则使⽤其定义的 Log 实现类
查看 classpath 中是否有 Log4j 的包,如果发现则⾃动使⽤ Log4j 作为⽇志实现类;否则,使用 JDK 自身的日志实现类(JDK 1.4 以后才有日志实现类);否则,使用 commons-logging 自己提供的一个简单的日志实现类 SimpleLog
JCL 有五个级别:
1 trace -> debug -> info -> warn -> error
1 2 3 4 5 6 org.apache.commons.logging.Log =org.apache.commons.logging.impl.Log4JLogger org.apache.commons.logging.LogFactory = org.apache.commons.logging.impl.LogFactoryImpl
slf4j 具体日志输出内容取决于具体的日志实现
slf4j 日志级别从高到低有五种:ERROR、WARN、INFO、DEBUG、TRACE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-jcl</artifactId > <version > 1.7.30</version > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-log4j12</artifactId > <version > 1.7.30</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-slf4j-impl</artifactId > <version > 2.13.0</version > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-jdk14</artifactId > <version > 1.7.30</version > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-simple</artifactId > <version > 1.7.30</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-core</artifactId > <version > 1.2.3</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > 1.2.3</version > </dependency >
如果存在多个桥接器的话,slf4j 会使用在 classpath 中较早出现的那个,如在 maven 中,会使用在 pom.xml
中定义较靠前的桥接器
不仅 slf4j 能够桥接到特定的日志实现,特定的实现也可以桥接到具体的日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <dependency > <groupId > org.slf4j</groupId > <artifactId > jul-to-slf4j</artifactId > <version > 1.7.30</version > </dependency > <groupId > org.slf4j</groupId > <artifactId > jcl-over-slf4j</artifactId > <version > 1.7.30</version > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > log4j-over-slf4j</artifactId > <version > 1.7.30</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-to-slf4j</artifactId > <version > 2.13.0</version > </dependency >
日志之间的桥接,非常容易出现 slf4j 的一个著名问题:slf4j 的日志环问题
Spring Boot 中的日志使用 springboot 框架在企业中的使用越来越普遍,spring boot 日志也是开发中常用的日志系统。spring boot 默认就是使用 SLF4J 作为日志门面,logback 作为日志实现来记录日志 引入如下依赖:
pom.xml 1 2 3 4 <dependency > <artifactId > spring-boot-starter-logging</artifactId > <groupId > org.springframework.boot</groupId > </dependency >
测试代码如下:
SpringbootLogApplicationTests.java 1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest class SpringbootLogApplicationTests {public static final Logger LOGGER = LoggerFactory.getLogger(SpringBootLogApplicationTests.class);@Test public void contextLoads () {LOGGER.error("error" ); LOGGER.warn("warn" ); LOGGER.info("info" ); LOGGER.debug("debug" ); LOGGER.trace("trace" ); } }
默认的日志配置如下:
application.yml 1 2 3 4 5 6 7 8 9 10 logging.level.com.pineapple=trace logging.pattern.console=%d{yyyy-MM-dd} [%thread ] [%-5level ] %logger{50} -%msg%n logging.file=D:/logs/springboot.log logging.pattern.file=%d{yyyy-MM-dd} [%thread ] %-5level %logger{50} - %msg%n
如果给类路径下放上每个日志框架自己的配置文件,Spring Boot 就不使用默认的配置了
日志框架 配置文件 Logback logback-spring.xml,locback.xml log4j2 log4j2-spring.xml,log4j2.xml jul logging.properties
日志使用建议 既然 Java 有这么多日志,那么到底应该如何优雅的使用 Java 日志?
门面约束 使用门面,而不是具体实现,使⽤ Log Facade 可以方便的切换具体的日志实现。⽽且,如果依赖多个项目,使⽤了不同的 Log Facade,还可以方便的通过 Adapter 转接到同⼀个实现上。
👴 日志门面现在推荐使用 Log4j-API 或者 SLF4j,不推荐继续使用 JCL
单一原则 只添加一个日志实现,项目中应该只使用一个具体的 Log Implementation,如果在依赖的项目中,使用的 Log Facade 不支持当前 Log Implementation,就添加合适的桥接器
👴 JUL 性能一般,log4j 性能也有问题而且不再维护,目前 log4j2 还有安全问题,推荐使用 logback
依赖约束 在项⽬中,Log Implementation 的依赖强烈建议设置为 runtime scope,并且设置为 optional。例如项目中使用了 SLF4J 作为 Log Facade,然后想使用 Log4j2 作为 Implementation,那么使用 maven 添加依赖的时候这样设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > ${log4j.version}</version > <scope > runtime</scope > <optional > true</optional > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-slf4j-impl</artifactId > <version > ${log4j.version}</version > <scope > runtime</scope > <optional > true</optional > </dependency >
设为 optional,依赖不会传递,这样如果是个 lib 项目,然后别的项⽬使用了这个 lib,不会被引入不想要的 Log Implementation 依赖; Scope 设置为 runtime,是为了防止开发⼈员在项⽬中直接使用 Log Implementation 中的类,强制约束开发⼈员使用 Facade 接⼝ 避免传递 尽量用 exclusion 排除依赖的第三方库中的⽇志坐标,同上⼀个话题,第三方库的开发者却未必会把具体的⽇志实现或者桥接器的依赖设置为 optional, 然后你的项⽬就会被迫传递引⼊这些依赖,而这些⽇志实现未必是你想要的,⽐如他依赖了 Log4j,你想使用 Logback,这时就很尴尬。另外,如果不同的第三⽅依赖使⽤了不同的桥接器和 Log 实现,极有可能会形成环。
这种情况下,推荐的处理⽅法,是使用 exclude 来排除所有的这些 Log 实现和桥接器的依赖,只保留第三⽅库⾥面对 Log Facade 的依赖
依赖 jstorm 会引⼊ Logback 和 log4j-over-slf4j,如果你在⾃己的项⽬中使用 Log4j 或其他 Log 实现的话,就需要加上 exclusion:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency > <groupId > com.alibaba.jstorm</groupId > <artifactId > jstorm-core</artifactId > <version > 2.1.1</version > <exclusions > <exclusion > <groupId > org.slf4j</groupId > <artifactId > log4j-over-slf4j</artifactId > </exclusion > <exclusion > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > </exclusion > </exclusions > </dependency >
注意写法 避免为不会输出的 log 买单。Log 库都可以灵活的设置输出级别,所以每一条程序中的 log,都是有可能不会被输出的,这时候要注意不要额外的付出代价。
1 2 logger.debug("this is debug: " + message); logger.debug("this is json msg: {}" , toJson(message));
第一条的字符串拼接,即使日志级别⾼于 debug 不会打印,依然会做字符串连接操作; 第二条虽然⽤了 SLF4J/Log4j2 中的懒求值⽅式,但是 toJson()这个函数却是总会被调用并且开销更大。 推荐的写法如下:
1 2 3 4 5 6 7 8 logger.debug("this is debug:{}" , message); logger.debug("this is json msg: {}" , () -> toJson(message)); if (logger.isDebugEnabled()) { logger.debug("this is debug: " + message); }
减少分析 输出的日志中尽量不要使用行号,函数名等信息。原因是:为了获取语句所在的函数名,或者行号,log 库的实现都是获取当前的 stacktrace ,然后分析取出这些信息,而获取 stacktrace 的代价是昂贵的。如果有很多的日志输出,就会占用大量的 CPU,在没有特殊需求的情况下,建议不要在日志中输出这些字段
精简至上 log 中尽量不要输出稀奇古怪的字符,这是个习惯和约束问题,下面这个语句最好不要出现在项目中
1 logger.debug("===========================:{}" ,message);
输出大量无关字符,虽然自己一时痛快,但是如果所有人都这样做的话,那 log 输出就没法看了!正确的做法是日志只输出必要的信息,如果要过滤,后期使用 grep
来筛选,只查看自己关心的日志
附录 复杂 Spring 项目中 SLF4J 最佳使用姿势