最近在看代码的过程中,发现身边的许多人在使用Java日志框架时,对于应该引入何种依赖不甚了解,搜索网络上的文章,常常也是互不一致。这篇文章可以看着是Java日志框架的入门使用和实践建议,重点介绍不同组合方式下的依赖设置及其背后的逻辑,一方面给自己备查,另外也希望对小伙伴们有所帮助。
Java日志框架家族繁杂,出于实用的原则,这里主要介绍主流的几个项目:SLF4J、Logback、Log4j 2,以及它们之间各种搭配用法和使用建议。
另外,由于Log4j 1项目已经在2015-08-05正式宣布死亡(最终版本停留在2012-05-13发布的1.2.17),因此这里也不再讨论Log4j 1,下文所有提到Log4j的地方,都是指Log4j 2。
对于日志框架,可以按照「分层」的概念来理解:接口层、实现层。开发者在使用日志框架时,建议基于接口层而非实现层进行开发,这样的好处是,避免项目与某一个具体日志框架耦合,这是一个常见的编程理念,应该比较容易理解。
例如,项目最初使用SLF4J作为接口层,使用Logback作为实现层,而你的项目代码中使用的也是接口层的类org.slf4j.Logger,这种情况下,当将来你想将实现层切换为Log4j时,你最需要改动依赖项,而不需要改动代码。
但是,如果你最初的项目代码中使用的并非是接口层的类,而是实现层(即Logback)的类ch.qos.logback.classic.Logger(这可能是因为手滑,毕竟类名都是一样的)。这种情况下,想要切换实现层,就需要改动所有涉及使用到这个类的代码。
由于logback-classic中既有实现层,也包含了对接口层SLF4J的依赖,因此,最简单的设置可以是这样的:
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.12</version> </dependency>
不过,就像简述里说的,为了避免开发者不小心误用实现类ch.qos.logback.classic.Logger,推荐使用如下的依赖设置,注意其中的scope设置:
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.32</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.12</version> <scope>runtime</scope> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency>
这里给出一个最常见的配置文件,包含控制台输出、滚动文件输出:
<configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>logs/app-%d{yyyy-MM-dd-HH}-%i.log</fileNamePattern> <!-- 单个日志文件超过10M,则进行滚动,对文件进行递增编号(即%i) --> <maxFileSize>10MB</maxFileSize> <!-- 所有日志文件的大小限制,超出则删除旧文件 --> <totalSizeCap>5GB</totalSizeCap> <!-- 与fileNamePattern相结合,本例中由于时间粒度是小时,因此这里表示保存48个小时 --> <maxHistory>48</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="ROLLING_FILE" /> </root> </configuration>
import org.slf4j.Logger; import org.slf4j.LoggerFactory; ... private static final Logger logger = LoggerFactory.getLogger(App.class); ... logger.info("First name: {}, last name: {}", firstName, lastName);
由于log4j-slf4j-impl中既有实现层,也包含了对接口层SLF4J的依赖,因此,最简单的设置可以是这样的:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.20.0</version> </dependency>
不过,基于与上一节同样的逻辑,推荐使用下面的设置:
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.20.0</version> <scope>runtime</scope> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency>
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="INFO"> <!-- log4j internal log level --> <Appenders> <Console name="CONSOLE" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingFile name="ROLLING_FILE" fileName="logs/log4j2/roll-by-time-and-size/app.log" filePattern="logs/log4j2/roll-by-time-and-size/app-%d{yyyy-MM-dd-HH}-%i.log" ignoreExceptions="false"> <PatternLayout> <Pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</Pattern> </PatternLayout> <Policies> <!-- 启动时,会删除多余的日志文件 --> <OnStartupTriggeringPolicy/> <!-- 自动感知filePattern中的时间设置,本例中是按小时粒度进行滚动 --> <TimeBasedTriggeringPolicy/> <!-- 单个日志文件超过10M,则进行滚动,递增编号(即filePattern中的%i) --> <SizeBasedTriggeringPolicy size="10M"/> </Policies> <!-- max配置与上面的filePattern结合,由于本例中是按小时粒度进行滚动,因此这里表示每小时内最多产生五个编号文件,超出这循环覆盖,如不设置max,则默认为7 --> <DefaultRolloverStrategy max="5"> <Delete basePath="logs" maxDepth="1"> <!-- 最近30天,最多5GB的日志 --> <IfFileName glob="app-*.log"> <IfAny> <IfLastModified age="30d"/> <IfAccumulatedFileSize exceeds="5GB"/> </IfAny> </IfFileName> </Delete> </DefaultRolloverStrategy> </RollingFile> </Appenders> <Loggers> <Root level="warn"> <AppenderRef ref="CONSOLE"/> <AppenderRef ref="ROLLING_FILE"/> </Root> </Loggers> </Configuration>
由于与上例相同,都是基于SLF4J接口层,因此使用方式相同:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; ... private static final Logger logger = LoggerFactory.getLogger(App.class); ... logger.info("First name: {}, last name: {}", firstName, lastName);
一般我们会基于SLF4J接口层进行开发,但是如果你硬要单独使用Log4j,也不是不可以。
最简单的,我们可以使用以下配置:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> </dependency>
不过,由于Log4j自身也分了接口层和实现层,推荐使用如下配置:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.20.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> <scope>runtime</scope> <exclusions> <exclusion> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> </exclusion> </exclusions> </dependency>
(同上)
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; ... private static final Logger logger = LogManager.getLogger(App.class); ... logger.info("First name: {}, last name: {}", firstName, lastName);
有人可能会说,Log4j自身拆成了接口层和实现层,是不是意味着,使用Log4j接口层的情况下,实现层还能使用别的日志系统?是的,例如可以使用「Log4j接口层 + Logback」的搭配:
<!-- 1) Log4j接口层 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.20.0</version> </dependency> <!-- 2) Log4j项目提供的「桥接层」,将Log4j接口层桥接到SLF4J接口层,由于Logback是基于SLF4J,因此经过桥接之后,就可以使用Logback作为实现层 --> <!-- 注:log4j-to-slf4j含有对log4j-api的依赖,因此上面可以不用单独列出log4j-api依赖,不过,为了逻辑清晰,还是保留 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-to-slf4j</artifactId> <version>2.20.0</version> </dependency> <!-- 3) Logback实现层 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.12</version> <scope>runtime</scope> </dependency>
如果你在开发一个玩具项目,对于日志框架的选择和使用当然可以比较随意,但是,如果是开发一个正经的项目,尤其是你的项目将作为公众可用的第三方库时,遵循最佳实践、保持灵活性则是非常必要的,因为你不知道使用方希望在他的项目中使用什么日志框架。
另外,我曾经作为面试官的时候,也常常询问面试者如何配置日志框架的依赖,这是一个很简单的题目,不过,一样可以考察对方几个知识点,包括日志框架、解耦、Maven中的scope设置等,总之,这是一个不错的考察编程常识的点。
评论已关闭。