--- title: Log4j 2:Apache维护的一款高性能日志记录工具 category: - Java企业级开发 tag: - 辅助工具/轮子 --- Log4j 2,顾名思义,它就是 Log4j 的升级版,就好像手机里面的 Pro 版。我作为一个写文章方面的工具人,或者叫打工人,怎么能不写完这最后一篇。 Log4j、SLF4J、Logback 是一个爹——Ceki Gulcu,但 Log4j 2 却是例外,它是 Apache 基金会的产品。 SLF4J 和 Logback 作为 Log4j 的替代品,在很多方面都做了必要的改进,那为什么还需要 Log4j 2 呢?我只能说 Apache 基金会的开发人员很闲,不,很拼,要不是他们这种精益求精的精神,这个编程的世界该有多枯燥,毕竟少了很多可以用“拿来就用”的轮子啊。 上一篇也说了,老板下死命令要我把日志系统切换到 Logback,我顺利交差了,老板很开心,夸我这个打工人很敬业。为了表达对老板的这份感谢,我决定偷偷摸摸地试水一下 Log4j 2,尽管它还不是个成品,可能会会项目带来一定的隐患。但谁让咱是一个敬岗爱业的打工人呢。 ![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-a9461265-7652-4512-9219-6b3e82392415.png) ### 01、Log4j 2 强在哪 1)在多线程场景下,Log4j 2 的吞吐量比 Logback 高出了 10 倍,延迟降低了几个数量级。这话听起来像吹牛,反正是 Log4j 2 官方自己吹的。 Log4j 2 的异步 Logger 使用的是无锁数据结构,而 Logback 和 Log4j 的异步 Logger 使用的是 ArrayBlockingQueue。对于阻塞队列,多线程应用程序在尝试使日志事件入队时通常会遇到锁争用。 下图说明了多线程方案中无锁数据结构对吞吐量的影响。 Log4j 2 随着线程数量的扩展而更好地扩展:具有更多线程的应用程序可以记录更多的日志。其他日志记录库由于存在锁竞争的关系,在记录更多线程时,总吞吐量保持恒定或下降。这意味着使用其他日志记录库,每个单独的线程将能够减少日志记录。 ![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-43f0b03d-5c4a-4af3-9e4c-177956246740.png) 性能方面是 Log4j 2 的最大亮点,至于其他方面的一些优势,比如说下面这些,可以忽略不计,文字有多短就代表它有多不重要。 2)Log4j 2 可以减少垃圾收集器的压力。 3)支持 Lambda 表达式。 4)支持自动重载配置。 ### 02、Log4j 2 使用示例 废话不多说,直接实操开干。理论知识有用,但不如上手实操一把,这也是我多年养成的一个“不那么良好”的编程习惯:在实操中发现问题,解决问题,寻找理论基础。 **第一步**,在 pom.xml 文件中添加 Log4j 2 的依赖: ```xml org.apache.logging.log4j log4j-api 2.5 org.apache.logging.log4j log4j-core 2.5 ``` (这个 artifactId 还是 log4j,没有体现出来 2,而在 version 中体现,多少叫人误以为是 log4j) **第二步**,来个最简单的测试用例: ```java import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Demo { private static final Logger logger = LogManager.getLogger(Demo.class); public static void main(String[] args) { logger.debug("log4j2"); } } ``` 运行 Demo 类,可以在控制台看到以下信息: ``` ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. ``` Log4j 2 竟然没有在控制台打印“ log4j2”,还抱怨我们没有为它指定配置文件。在这一点上,我就觉得它没有 Logback 好,毕竟人家会输出。 这对于新手来说,很不友好,因为新手在遇到这种情况的时候,往往不知所措。日志里面虽然体现了 ERROR,但代码并没有编译出错或者运行出错,凭什么你不输出? 那作为编程老鸟来说,我得告诉你,这时候最好探究一下为什么。怎么做呢? 我们可以复制一下日志信息中的关键字,比如说:“No log4j2 configuration file found”,然后在 Intellij IDEA 中搜一下,如果你下载了源码和文档的话,不除意外,你会在 ConfigurationFactory 类中搜到这段话。 可以在方法中打个断点,然后 debug 一下,你就会看到下图中的内容。 ![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-4ba440d9-c0b6-4ad2-b538-9d303cc99d90.png) 通过源码,你可以看得到,Log4j 2 会去寻找 4 种类型的配置文件,后缀分别是 properties、yaml、json 和 xml。前缀是 log4j2-test 或者 log4j2。 得到这个提示后,就可以进行第三步了。 **第三步,**在 resource 目录下增加 log4j2-test.xml 文件(方便和 Logback 做对比),内容如下所示: ```xml ``` Log4j 2 的配置文件格式和 Logback 有点相似,基本的结构为 `< Configuration>` 元素,包含 0 或多个 `< Appenders>` 元素,其后跟 0 或多个 `< Loggers>` 元素,里面再跟最多只能存在一个的 `< Root>` 元素。 **1)配置 appender**,也就是配置日志的输出目的地。 有 Console,典型的控制台配置信息上面你也看到了,我来简单解释一下里面 pattern 的格式: - `%d{HH:mm:ss.SSS}` 表示输出到毫秒的时间 - `%t` 输出当前线程名称 - `%-5level` 输出日志级别,-5 表示左对齐并且固定输出 5 个字符,如果不足在右边补空格 - `%logger` 输出 logger 名称,最多 36 个字符 - `%msg` 日志文本 - `%n` 换行 顺带补充一下其他常用的占位符: - `%F` 输出所在的类文件名,如 Demo.java - `%L` 输出行号 - `%M` 输出所在方法名 - `%l` 输出语句所在的行数, 包括类名、方法名、文件名、行数 - `%p` 输出日志级别 - `%c` 输出包名,如果后面跟有 `{length.}` 参数,比如说 `%c{1.}`,它将输出报名的第一个字符,如 `com.itwanger` 的实际报名将只输出 `c.i` 再次运行 Demo 类,就可以在控制台看到打印的日志信息了: ``` 10:14:04.657 [main] DEBUG com.itwanger.Demo - log4j2 ``` **2)配置 Loggers**,指定 Root 的日志级别,并且指定具体启用哪一个 Appenders。 **3)自动重载配置**。 Logback 支持自动重载配置,Log4j 2 也支持,那想要启用这个功能也非常简单,只需要在 Configuration 元素上添加 `monitorInterval` 属性即可。 ``` ... ``` 注意值要设置成非零,上例中的意思是至少 30 秒后检查配置文件中的更改。最小间隔为 5 秒。 ### 03、Async 示例 除了 Console,还有 Async,可以配合文件的方式来异步写入,典型的配置信息如下所示: ``` %d %p %c [%t] %m%n ``` 对比 Logback 的配置文件来看,Log4j 2 真的复杂了一些,不太好用,就这么直白地说吧!但自己约的,含着泪也得打完啊。把这个 Async 加入到 Appenders: ``` %d %p %c [%t] %m%n ``` 再次运行 Demo 类,可以在项目根路径下看到一个 debug.log 文件,内容如下所示: ``` 2020-10-30 09:35:49,705 DEBUG com.itwanger.Demo [main] log4j2 ``` ### 04、RollingFile 示例 当然了,Log4j 和 Logback 我们都配置了 RollingFile,Log4j 2 也少不了。RollingFile 会根据 Triggering(触发)策略和 Rollover(过渡)策略来进行日志文件滚动。如果没有配置 Rollover,则使用 DefaultRolloverStrategy 来作为 RollingFile 的默认配置。 触发策略包含有,基于 cron 表达式(源于希腊语,时间的意思,用来配置定期执行任务的时间格式)的 CronTriggeringPolicy;基于文件大小的 SizeBasedTriggeringPolicy;基于时间的 TimeBasedTriggeringPolicy。 过渡策略包含有,默认的过渡策略 DefaultRolloverStrategy,直接写入的 DirectWriteRolloverStrategy。一般情况下,采用默认的过渡策略即可,它已经足够强大。 来看第一个基于 SizeBasedTriggeringPolicy 和 TimeBasedTriggeringPolicy 策略,以及缺省 DefaultRolloverStrategy 策略的配置示例: ``` %d %p %c{1.} [%t] %m%n ``` 为了验证文件的滚动策略,我们调整一下 Demo 类,让它多打印点日志: ``` for (int i = 1;i < 20; i++) { logger.debug("微信搜索「{}」,回复关键字「{}」,有惊喜哦","沉默王二", "java"); } ``` 再次运行 Demo 类,可以看到根目录下多了 3 个日志文件: ![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-07af98ca-cf94-427e-adb6-bd935e32a8d0.png) 结合日志文件名,再来看 RollingFile 的配置,就很容易理解了。 1)fileName 用来指定文件名。 2)filePattern 用来指定文件名的模式,它取决于过渡策略。 由于配置文件中没有显式指定过渡策略,因此 RollingFile 会启用默认的 DefaultRolloverStrategy。 先来看一下 DefaultRolloverStrategy 的属性: ![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-32b853ce-8beb-496b-b66f-31b650c257ab.png) 再来看 filePattern 的值 `rolling-%d{yyyy-MM-dd}-%i.log`,其中 `%d{yyyy-MM-dd}` 很好理解,就是年月日;其中 `%i` 是什么意思呢? 第一个日志文件名为 rolling.log(最近的日志放在这个里面),第二个文件名除去日期为 rolling-1.log,第二个文件名除去日期为 rolling-2.log,根据这些信息,你能猜到其中的规律吗? 其实和 DefaultRolloverStrategy 中的 max 属性有关,目前使用的默认值,也就是 7,那就当 rolling-8.log 要生成的时候,删除 rolling-1.log。可以调整 Demo 中的日志输出量来进行验证。 3)SizeBasedTriggeringPolicy,基于日志文件大小的时间策略,大小以字节为单位,后缀可以是 KB,MB 或 GB,例如 20 MB。 再来看一个日志文件压缩的示例,来看配置: ``` %d %p %c{1.} [%t] %m%n ``` - fileName 的属性值中包含了一个目录 gz,也就是说日志文件都将放在这个目录下。 - filePattern 的属性值中增加了一个 gz 的后缀,这就表明日志文件要进行压缩了,还可以是 zip 格式。 运行 Demo 后,可以在 gz 目录下看到以下文件: ![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongju/log4j2-1b04167d-a11f-4447-9062-cb3cdd59aa73.png) 到此为止,Log4j 2 的基本使用示例就已经完成了。测试环境搞定,我去问一下老板,要不要在生产环境下使用 Log4j 2。 ![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)