常见日志框架分析

1 简要介绍

Java领域有各种各样的日志输出框架,你是不是在所参与过的项目中看到别人用各种方式配置了自己的日志输出规则,但是却不知道每一种日志框架各自有什么特点,相互之间有什么关系?本文就简要介绍一下目前Java开发领域里最常用的几种日志框架应该如何使用,以及简单的一些工作原理。

1.1 JUL(Java Util Log)

JDK自带的日志输出工具,极少在实际项目中应用,因为功能弱,性能低。

1.2 Log4j 1.x

曾经的日志输出框架中的王者,简单易用,功能丰富,但是性能不佳,2015年8月5号停止更新维护。

1.3 JCL(Jakarta Commons Logging)

Apache基金会旗下的项目,只提供了输出日志的Facade API,必须结合JUL和Log4j等具体实现框架才能被使用。实际项目当中的各种第三方依赖使用的日志输出框架可能不同,但是我们又不想给每个种日志框架写一套日志输出配置,JCL可以根据运行过程中实际使用的日志框架自动切换。

1.4 Logback

Logback可以被看作是Log4j的继承者,提供了性能更好的实现,比如异步logger,Filter等更多的特性。

1.5 SLF4J(The Simple Logging Facade for Java)

类似于JCL的一种Facade API,本身并不实现日志输出功能。提供了比JCL更加便捷,而且提高了性能的日志输出语句编写方式。可以桥接各种主流日志输出框架。

1.6 Log4j2

不兼容Log4j 1.x,自身就做了Facade API和实现的分离,基本上是模仿SLF4J和Logback设计的,性能比Log4j 1.x提升很多。

2 使用过程中的坑

2.1 不要出现循环依赖

日志框架之间可以相互依赖,相互利用来达到项目当中的最佳配置。但是配置不当会导致各jar包之间出现循环依赖,可能出现项目无法正常启动。如下三对jar包就不能同时出现在项目中。

1. jcl-over-slf4j 和 slf4j-jcl

2. log4j-over-slf4j 和 slf4j-log4j12

3. jul-to-slf4j 和 slf4j-jdk14

2.2 不要做不需要输出的日志内容拼接

项目发布上线后,通常情况下只会打印INFO或更高级别的日志,不会打印DEBUG级别的日志,但是我们在写代码的过程中很少有人愿意先判断日志输出级别,再写打印日志的语句。

// 这是JCL的写法if (logger.isDebugEnabled()) {    logger.debug("只有在DEBUG级别才出现本日志, url:" + url);}

大部分的人都只愿意写一行输出日志的代码。

// 这是JCL的写法logger.debug("只有在DEBUG级别才出现本日志, url:" + url);

这带来一个问题,项目上线后,做了大量的字符串拼接工作,但是却不输出,白白消耗了服务器资源。

使用SLF4J提供的特性,可以解决该问题。

// 这是SLF4J的写法logger.debug("只有在DEBUG级别才出现本日志, url:{}", url);

因为是使用字符串format做拼接,性能比手工拼接略低。

在项目中可以采用如下方法避开字符串format的性能问题:

  • DEBUG级日志使用format方式编写

  • 其他一定会输出的日志使用手工拼接方式编写

需要注意的是,即使用了SLF4J的写法,如果用于填充占位符的是一个表达式或方法,一样会进行计算和调用,然后不输出计算的结果。所以仍然需要先判断日志输出级别,然后再写输出日志的那行代码。

if (logger.isDebugEnabled()) {    logger.debug("receive request: {}", toJson(request));}

2.3 不要在日志里输出没有业务意义的字符

日志数据的输出要消耗CPU、磁盘IO和内存,保存下来后还要占用大量磁盘空间。每多输出一个无意义的字符,都是对服务器资源的浪费,还会引起后期查询操作的困惑。

logger.debug("========================只有我知道这是什么=============");

2.4 强制使用Facade API,而不是具体实现包

使用Facade API可以方便的切换具体的日志实现。如果依赖多个项目,使用了不同的Facade API,可以方便的通过适配器转接到同一个实现上。如果依赖项目使用了多个不同的日志实现,处理起来就非常困难。

推荐使用 Log4j-API 或者 SLF4j,不推荐使用 JCL。

2.5 具体的日志实现依赖包设置为optional和使用runtime scope

被设置为optional的依赖不会传递,当你的项目会被当作工具包被其他项目使用时,因为被设置成optional,工具包项目中的日志实现依赖不会出现在调用方的依赖树中。

被设置为runtime scope的依赖包无法在编写代码过程中被调用,可以防止开发人员直接使用日志实现包中的方法,而不是通过Facade API来编写代码。

<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>

3 日志信息中的自定义变量是怎么输出的

为方便查找日志,通常会在属于同一线程的多行日志中输出相同的traceId,这是利用jdk自身支持的ThreadLocal实现的。关于java.lang.ThreadLocal的官方说明如下。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

以下关于traceId的设置、读取和输出操作,依赖于SLF4J和Log4j2,Log4j2的版本是2.11.2。

3.1 如何设置traceId

设置traceId只需要一行代码,下面简单地把UUID作为traceId的值。

org.slf4j.MDC.put("traceId", UUID.randomUUID().toString().replaceAll("-", ""));

设置traceId的方法调用栈如下。

put:90, DefaultThreadContextMap (org.apache.logging.log4j.spi)put:246, ThreadContext (org.apache.logging.log4j)put:31, Log4jMDCAdapter (org.apache.logging.slf4j)put:147, MDC (org.slf4j)

org.apache.logging.log4j.spi.DefaultThreadContextMap.put方法的简要源码如下:

private final ThreadLocal<Map<String, String>> localMap;@Overridepublic void put(final String key, final String value) {    if (!useMap) {    return;    }    Map<String, String> map = localMap.get();    map = map == null ? new HashMap<String, String>(1) : new HashMap<>(map);    map.put(key, value);    localMap.set(Collections.unmodifiableMap(map));}

3.2 日志配置中如何使用traceId

使用日志模板的转换符X设置traceId。具体模板格式参考[官方文档](https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/PatternLayout.html)。

[%d{yyyy-MM-dd HH:mm:ss}]|%X{traceId}|%C{1}.%M:%L|%m%n

从本地线程中读取traceId拼接到日志中的方法调用栈如下。

format:113, MdcPatternConverter (org.apache.logging.log4j.core.pattern)format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)toSerializable:334, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)toText:233, PatternLayout (org.apache.logging.log4j.core.layout)encode:218, PatternLayout (org.apache.logging.log4j.core.layout)encode:58, PatternLayout (org.apache.logging.log4j.core.layout)directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)callAppenders:464, LoggerConfig (org.apache.logging.log4j.core.config)processLogEvent:448, LoggerConfig (org.apache.logging.log4j.core.config)log:431, LoggerConfig (org.apache.logging.log4j.core.config)log:406, LoggerConfig (org.apache.logging.log4j.core.config)log:63, AwaitCompletionReliabilityStrategy (org.apache.logging.log4j.core.config)logMessage:146, Logger (org.apache.logging.log4j.core)tryLogMessage:2170, AbstractLogger (org.apache.logging.log4j.spi)logMessageTrackRecursion:2125, AbstractLogger (org.apache.logging.log4j.spi)logMessageSafely:2108, AbstractLogger (org.apache.logging.log4j.spi)logMessage:2019, AbstractLogger (org.apache.logging.log4j.spi)logIfEnabled:1890, AbstractLogger (org.apache.logging.log4j.spi)info:184, Log4jLogger (org.apache.logging.slf4j)

org.apache.logging.log4j.core.pattern.MdcPatternConverter.format方法的简要源码如下。

private final String key;private final String[] keys;private final boolean full;@Overridepublic void format(final LogEvent event, final StringBuilder toAppendTo) {    final ReadOnlyStringMap contextData = event.getContextData();    // if there is no additional options, we output every single    // Key/Value pair for the MDC in a similar format to Hashtable.toString()    if (full) {        if (contextData == null || contextData.size() == 0) {            toAppendTo.append("{}");            return;        }        appendFully(contextData, toAppendTo);    } else {        if (keys != null) {            if (contextData == null || contextData.size() == 0) {                toAppendTo.append("{}");                return;            }            appendSelectedKeys(keys, contextData, toAppendTo);        } else if (contextData != null){            // otherwise they just want a single key output            final Object value = contextData.getValue(key);            if (value != null) {                StringBuilders.appendValue(toAppendTo, value);            }        }    }}

4 演示源码

https://gitee.com/realmojiayi/useful-spring-demo

转载于:https://my.oschina.net/mojiayi/blog/3054028

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值