日志库是很常见的,因为你在每一个项目中都需要他们。打印日志是服务器端应用中最重要的事情,因为日志是你了解你的程序发生了什么的唯一途径。尽管JDK附带自己的日志库,但是还是有很多更好的选择可用。
例如 Log4j 、 SLF4j 和 LogBack。本文基于笔者日志框架的使用经验,以及多家公司的代码规范要求等,做了一些总结和思考,Java开发人员应该熟悉日志记录的利弊, 并且了解为什么SLF4J要比Log4J要好。
读者在设计自己公司日志规范的时候,大家可以学习交流一下
SLF4J(Simple logging Facade for Java)不是一个真正的日志实现,而是一个抽象层( abstraction layer),它允许你在后台使用任意一个日志类库。如果是在编写供内外部都可以使用的API或者通用类库,那么你真不会希望使用你类库的客户端必须使用你选择的日志类库。
如果一个项目已经使用了log4j,而你加载了一个类库,比方说 Apache Active MQ——它依赖于于另外一个日志类库logback,那么你就需要把它也加载进去。但如果Apache Active MQ使用了SLF4J,你可以继续使用你的日志类库而无语忍受加载和维护一个新的日志框架的痛苦。
总的来说,SLF4J使你的代码独立于任意一个特定的日志API,这是一个对于开发API的开发者很好的思想。虽然抽象日志类库的思想已经不是新鲜的事物而且Apache commons logging也已经在使用这种思想了,但现在SLF4J正迅速成为Java世界的日志标准。
在代码中表示为{}的特性。占位符是一个非常类似于在String的format()方法中的%s,它会在运行时被某个提供的实际字符串所替换。
这不仅降低了你代码中字符串连接次数,而且还节省了新建的String对象。因为String对象是不可修改的并且它们建立在一个String池中,它们消耗堆内存( heap memory)而且大多数时间他们是不被需要的,例如当你的应用程序在生产环境以ERROR级别运行时候,一个String使用在DEBUG语句就是不被需要的。
通过使用SLF4J,你可以在运行时延迟字符串的建立,这意味着只有需要的String对象才被建立。而如果你已经使用log4j,那么你已经对于在if条件中使用debug语句这种变通方案十分熟悉了,但SLF4J的占位符就比这个好用得多。
上文提到我们slf4j只是规范,提供了api,但是具体打印日志,还是需要底层的日志框架和日志框架适配器。当前比较主流的日志框架有log4j 以及 logback,下面分别阐述两个日志框架的使用细节
pom坐标
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.29</version>
</dependency>
配置说明
配置根rootLogger : 用于配置当前日志输出的级别以及输出的位置
配置日志信息输出目的地Appender:
log4j.appender.appenderName=位置
log4j.appender.appenderName.optionN = valueN (根据位置不同,对应不同的optionN选项属性)
具体可根据实际需求,选择在rootlogger里面配置哪几个appender。
测试案例如下:
log4j.rootLogger=DEBUG, stdout,systemOut,logFile,logDailyFile,logRollingFile
# 控制台测试1
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
#控制台测试2
log4j.appender.systemOut = org.apache.log4j.ConsoleAppender
log4j.appender.systemOut.layout = org.apache.log4j.PatternLayout
log4j.appender.systemOut.layout.ConversionPattern = [%-5p][%-22d{yyyy/MM/dd HH:mm:ssS}][%l]%n%m%n
log4j.appender.systemOut.Threshold = DEBUG
log4j.appender.systemOut.ImmediateFlush = TRUE
log4j.appender.systemOut.Target = System.out
#文件
log4j.appender.logFile = org.apache.log4j.FileAppender
log4j.appender.logFile.layout = org.apache.log4j.PatternLayout
log4j.appender.logFile.layout.ConversionPattern = [%-5p][%-22d{yyyy/MM/dd HH:mm:ssS}][%l]%n%m%n
log4j.appender.logFile.Threshold = DEBUG
log4j.appender.logFile.ImmediateFlush = TRUE
log4j.appender.logFile.Append = TRUE
log4j.appender.logFile.File = D://log.log
log4j.appender.logFile.Encoding = UTF-8
#按照日期回滚文件
log4j.appender.logDailyFile = org.apache.log4j.DailyRollingFileAppender
log4j.appender.logDailyFile.layout = org.apache.log4j.PatternLayout
log4j.appender.logDailyFile.layout.ConversionPattern = [%-5p][%-22d{yyyy/MM/dd HH:mm:ssS}][%l]%n%m%n
log4j.appender.logDailyFile.Threshold = DEBUG
log4j.appender.logDailyFile.ImmediateFlush = TRUE
log4j.appender.logDailyFile.Append = TRUE
log4j.appender.logDailyFile.File = D://log.log
log4j.appender.logDailyFile.DatePattern = '.'yyyy-MM-dd-HH-mm'.log'
log4j.appender.logDailyFile.Encoding = UTF-8
#按照文件大小切割文件
log4j.appender.logRollingFile = org.apache.log4j.RollingFileAppender
log4j.appender.logRollingFile.layout = org.apache.log4j.PatternLayout
log4j.appender.logRollingFile.layout.ConversionPattern = [%-5p][%-22d{yyyy/MM/dd HH:mm:ssS}][%l]%n%m%n
log4j.appender.logRollingFile.Threshold = DEBUG
log4j.appender.logRollingFile.ImmediateFlush = TRUE
log4j.appender.logRollingFile.Append = TRUE
log4j.appender.logRollingFile.File = D://log.log
log4j.appender.logRollingFile.MaxFileSize = 1MB
log4j.appender.logRollingFile.MaxBackupIndex = 10
log4j.appender.logRollingFile.Encoding = UTF-8
具体配置项说明,详见文末链接文章
注意事项
3.1 注意上面pom,添加日志框架,如果使用slf4j ,需要有对应的日志框架适配器和slf4j 适配,否则会报错
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
3.2 resource目录下要有文件名为log4j.properties 的配置文件,不然日志打印不出来,报错
log4j:WARN No appenders could be found for logger (Testlog4j).
log4j:WARN Please initialize the log4j system properly.
3.3 一个日志门面只能配置一个日志适配器,如果pom依赖里面添加多个会警告,多个会选择pom里面第一个
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/D:/toolsdata/maven/mavenjar/org/slf4j/slf4j-log4j12/1.7.29/slf4j-log4j12-1.7.29.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/toolsdata/maven/mavenjar/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
pom坐标
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
配置说明
如果没有自己定义logback的配置文件, logback 默认地会调用BasicConfigurator ,创建一个最小化配置。最小化配置由一个关联到根 logger 的ConsoleAppender 组成,调用PatternLayoutEncoder机械能格式化。root logger 默认级别是 DEBUG。
2.1 logback 常用配置如下:
<configuration debug="false">
<property name="logging.app.path" value="/app/logs"/>
<property name="logging.app.name" value="DEDataService"/>
<property name="LOG_HOME" value="${logging.app.path}/${logging.app.name}"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
encoder>
appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/info.log.%d{yyyy-MM-dd}.%i.log.zipFileNamePattern>
<maxFileSize>100MBmaxFileSize>
<MaxHistory>30MaxHistory>
rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
encoder>
appender>
<appender name="ERRORFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/error.log.%d{yyyy-MM-dd}.logFileNamePattern>
<MaxHistory>30MaxHistory>
rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>errorlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
encoder>
appender>
<logger name="com.test0" level="warn"/>
<logger name="com.test3" level="OFF"/>
<logger name="com.test1" level="info" additivity="false">
<appender-ref ref="STDOUT"/>
logger>
<logger name="com.test2" level="info" additivity="false"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERRORFILE"/>
root>
configuration>
将上述配置文件logback.xml 添加到项目resource目录下面 ,启动项目有如下输出,可以看到我们在配置文件配置的很多细节都有在日志中体现。
19:01:57,685 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
19:01:57,685 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
19:01:57,685 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/code/newcode/DEDataAdapter/target/classes/logback.xml]
19:01:57,693 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
19:01:57,693 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [jar:file:/D:/code/newcode/DEDataAdapter/libs/invoice-datamodel-1.0.0.jar!/logback.xml]
19:01:57,693 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/code/newcode/DEDataAdapter/target/classes/logback.xml]
19:01:57,776 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
19:01:57,779 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
19:01:57,782 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
19:01:57,859 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.rolling.RollingFileAppender]
19:01:57,866 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [FILE]
19:01:57,872 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@2056418216 - Archive files will be limited to [100 MB] each.
19:01:57,872 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@2056418216 - Will use zip compression
19:01:57,872 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@2056418216 - Will use the pattern /app/logs/DEDataService/info.log.%d{yyyy-MM-dd}.%i.log for the active file
19:01:57,882 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP@26a7b76d - The date pattern is 'yyyy-MM-dd' from file name pattern '/app/logs/DEDataService/info.log.%d{yyyy-MM-dd}.%i.log.zip'.
19:01:57,882 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP@26a7b76d - Roll-over at midnight.
19:01:57,883 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP@26a7b76d - Setting initial period to Wed Apr 12 19:01:57 CST 2023
19:01:57,890 |-INFO in ch.qos.logback.core.rolling.RollingFileAppender[FILE] - Active log file name: /app/logs/DEDataService/info.log.2023-04-12.0.log
19:01:57,890 |-INFO in ch.qos.logback.core.rolling.RollingFileAppender[FILE] - File property is set to [null]
19:01:57,890 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.rolling.RollingFileAppender]
19:01:57,890 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [ERRORFILE]
19:01:57,892 |-INFO in c.q.l.core.rolling.TimeBasedRollingPolicy@1253946629 - No compression will be used
19:01:57,892 |-INFO in c.q.l.core.rolling.TimeBasedRollingPolicy@1253946629 - Will use the pattern /app/logs/DEDataService/error.log.%d{yyyy-MM-dd}.log for the active file
19:01:57,894 |-INFO in c.q.l.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy - The date pattern is 'yyyy-MM-dd' from file name pattern '/app/logs/DEDataService/error.log.%d{yyyy-MM-dd}.log'.
19:01:57,894 |-INFO in c.q.l.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy - Roll-over at midnight.
19:01:57,894 |-INFO in c.q.l.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy - Setting initial period to Wed Apr 12 19:01:57 CST 2023
19:01:57,901 |-INFO in ch.qos.logback.core.rolling.RollingFileAppender[ERRORFILE] - Active log file name: /app/logs/DEDataService/error.log.2023-04-12.log
19:01:57,901 |-INFO in ch.qos.logback.core.rolling.RollingFileAppender[ERRORFILE] - File property is set to [null]
19:01:57,902 |-INFO in ch.qos.logback.classic.joran.action.LoggerAction - Setting level of logger [com.test0] to WARN
19:01:57,902 |-INFO in ch.qos.logback.classic.joran.action.LoggerAction - Setting level of logger [com.test3] to OFF
19:01:57,902 |-INFO in ch.qos.logback.classic.joran.action.LoggerAction - Setting level of logger [com.test1] to INFO
19:01:57,902 |-INFO in ch.qos.logback.classic.joran.action.LoggerAction - Setting additivity of logger [com.test1] to false
19:01:57,902 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[com.test1]
19:01:57,903 |-INFO in ch.qos.logback.classic.joran.action.LoggerAction - Setting level of logger [com.test2] to INFO
19:01:57,903 |-INFO in ch.qos.logback.classic.joran.action.LoggerAction - Setting additivity of logger [com.test2] to false
19:01:57,903 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
19:01:57,903 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
19:01:57,903 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [FILE] to Logger[ROOT]
19:01:57,903 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [ERRORFILE] to Logger[ROOT]
19:01:57,903 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
19:01:57,904 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@7ce6a65d - Registering current configuration as safe fallback point
注意事项
3.1 logback 会从很多目录找配置文件xml,笔者之前项目刚开始犯得一个错是,springboot项目打的包里面有配置文件,在jar包外面有配置文件,结果因为jar包外面的yml没指定读取外面的logback.xml,导致项目读的是jar里面,即类路径下的配置
springboot yml增加配置如下:
logging:
#类路径,本地调试可以使用,不推荐
config: classpath:logback-pro.xml
#绝对路径,生产和本地调试使用,推荐
config: /app/conf/dataservice/logback-pro.xml
从上往下,日志级别依次降低,如果将log level设置在某一个级别上,那么比此级别优先级高的log都能打印出来。例如,如果设置优先级为WARN,那么FATAL、ERROR、WARN 3个级别的log能正常 输出,而INFO、DEBUG、TRACE级别的log则会被忽略。
示例:
try {
output = productClient.callRemoteService(input);
} catch (Exception e) {
log.error("productClient.callRemoteService异常, inputDTO={}", JSON.toJSONString(input), e);
throw new RemoteServiceException(FrontModule.PRODUCT.getCode(), "查询产品信息异常");
}
slf4j优于log4j的原因
log4j详细配置
log4j按照日期加大小切割参考 (未验证)
Slf4j日志绑定/日志桥接