Java log日志简介
基本在所有有关Java规范的书中,都会提到禁止使用System.out.print()
来记录日志,而日志系统在基本的项目开发中,是绝对不可缺少的一部分。其实用了这么久的日志,基本配置文件都是直接复制的公司的标准格式,而前前后后使用过log4j
,commons-logging
,logback
等,他们的区别又在哪里呢?
- commons-logging
commons-logging
作为Apache
最早提供的日志门面接口,实现日志接口和实现的解耦,其内部也有简单的日志实现,实际使用的地方并不多 -
Log4j
logging for java
,log4
这类名字非常出现,同款的还有log4net
等,它是一种经典的日志解决方案,内部把日志系统抽象封装成Logger
、appender
、pattern
等实现。刚开始出现的时候,受到了各个开源框架的青睐 -
logback
算是
Log4j
的改进版,作者是Log4j
的原作者,相比Log4j
,性能更好,内存消耗更少,并且本身集成了slf4j
,算是对slf4j
最原生的支持 -
Log4j2
log4j2
是Apache
接手Log4j
后推出的一个改进版,对比Log4j
,log4j2
性能快了很多,并且支持properties
文件和json
格式的配置文件的支持,在官方JMH
测试中,Log4j2
的性能稍微比logback
快一点,虽然Log4j2
拥有如此多的有点,但是它的流行程度却没有logback
广泛 -
slf4j
全称为
Simple Logging Facade for Java
,用于对不同日志框架提供一个简单的门面封装,可以在部署的时候不用修改任何配置即可接入任何一种日志方案,和commons-logging
比较类似
说了这么多,其实一般用的最多的还是slf4j
和logback
,关于slf4j
这里多说两句:slf4j
作为简单的日志门面接口,是不包含具体的日志的实现的,因此一般都是使用sl4j
加上一种实现。然而在平时开发过程中,引入的包有时候会自带一种日志实现,此时可能会引起slf4j
选择包冲突:
比如引入的xx.jar
使用的是slf4j
加Log4j
的日志框架,而我们系统使用的是slf4j
+logback
,当引入xx.jar
时候,slf4j
在查找具体的实现包的时候,可能选择了Log4j
,这个时候我们可能就会发现我们为Log4j
所配置的配置文件不生效了。。
解决方案其实很简单,排除其Log4j
的依赖即可
配置Logback
其中logback
一共包含三个模块:
logback-core
:日志模块的基础构建,其他的模块基于它构建logback-classic
:logback
对slf4j
的实现,其中改进了很多slf4j
的功能logback-access
:用于与Servlet
容器(Tomcat,jetty)等交互的模块
因此我们想要使用logback
,直接在Maven
中引入logback-classic
接口,它包含了logback-core
和slf4j
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
配置文件
logback
在启动时,根据以下步骤寻找配置文件:
- 在
classpath
中寻找logback-test.xml
文件 - 如果找不到
logback-test.xml
,则在classpath
中寻找logback.groovy
文件 - 如果找不到
logback.groovy
,则在classpath
中寻找logback.xml
文件 - 如果上述的文件都找不到,则
logback
会使用JDK
的SPI
机制查找META-INF/services/ch.qos.logback.classic.spi.Configurator
中的logback
配置实现类,这个实现类必须实现Configuration
接口,使用它的实现来进行配置 - 如果上述操作都不成功,
logback
就会使用它自带的BasicConfigurator
来配置,并将日志输出到console
。
以上查找方法,可以通过logback
的debug
信息查看
配置logback.xml
,大体从以下思路配置:
- 首先,我们需要区分这是日志级别,对于不同的日志,我们所做的操作可能不同,比如
debug
信息直接打在控制台,info
信息保存在info
文件中,error
信息保存在error
文件中 - 其次,我们需要有一个地方能够配置目前需要打开的日志级别,比如将日志级别调至
warn
,那么info
信息便不会再存入文件中了
由以上思路,我们可以大概画出文件的总体框架:
<configuration>
<appender> //输出到控制台的信息配置
//....
</appender>
<appender> //输出到info文件的配置
//...
</appender>
<appender> //输出到error文件的配置
</appender>
<logger> //特殊处理日志定义
//..
</logger>
<root level="debug"> //总日志开关
//...
</root>
</configuration>
上面便是logback
的总体日志配置,下面详细说说每个节点的作用
其中每个appender
配置一种行为,比如这个appender
定义仅将信息输出到控制台中,另外一个appender
定义将信息输出到某个文件中,最后一个appender
可以配置将信息输出到某个邮件/数据库/网络请求中,总之appender
便是用来定义不同的行为.
有时候在配置文件中,可能包含多个appender
,而有时候这些appender
不可能什么时候都要使用,比如发送邮件这个appender
,可能只有测试和正式上线的时候才会使用,这个时候,便是logger
和root
的功能,root
作为所有logger
的爸爸,用来定义默认appender
,在root
中,需要定义需要使用的appender
,比如在平时开发过程中,不论什么级别的日志,仅仅将日志信息打在控制台就行了,这个时候root
中就只用包含行为为控制台的appender
即可,在正式上线后,再讲其他的appender
添加到root
中即可。
说完root
,那为什么还有logger
呢?logger
是用来添加特权包和类的,某些时候,可能某个包/类涉及到非常重要的业务,这个包报错了的话,需要立即处理,这个时候便可以定义一个邮件appender
,但是其他包的日志可以稍后来排查,如果直接在root
中添加发邮件的appender
,那么只要报错就会发邮件,比较麻烦,这个时候就可以使用logger
定义一个特权包,这个特权包可以单独定义appender
不过需要知道的是,在logback
中,消息都是冒泡的,而root
作为所有的logger
的父亲,消息经过logger
处理后,会再次冒泡到logger
的父类中,最后一直到root
到这里,logback.xml
大体框架我们已经了解,下面来具体了解各个节点。
先来一个完整的logback.xml
,这个配置文件,在日常项目中,已经完全够用:
<?xml version="1.0" encoding="UTF-8"?>
<!--
scan: 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod: 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
configuration 子节点为 appender、logger、root
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
<!--用于区分不同应用程序的记录-->
<contextName>logback-test</contextName>
<!--日志文件所在目录,实际路径为 %{classpath}/logs-->
<property name="LOG_HOME" value="logs"/>
<!--统一输出格式-->
<property name="STANDARD_PATTERN" value= "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ---- [%15thread] %logger{36} : %msg%n"/>
<!--关闭logback自身的日志-->
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<!--控制台输出-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger输出日志的logger名 %msg:日志消息,%n是换行符 -->
<!--这里是按照SpringBoot输出格式配置-->
<pattern>{STANDARD_PATTERN}</pattern>
</encoder>
</appender>
<!--INFO日志文件-->
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- ThresholdFilter:临界值过滤器,只存放比INFO日志级别更高的日志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--
官方解释.i:当某天:比如2019-01-01,产生了总共30M的日志的时候,则会分别生成
2019-01-01.0,2019-01-01.1,2019-01-01.2分别三个日志文件,
.i的作用便是用来分割超过日志大小的日志
-->
<fileNamePattern>{LOG_HOME}/app_stdout.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory><!--保存最近30天的日志-->
<maxFileSize>10MB</maxFileSize><!--单个文件最大10M-->
<totalSizeCap>20G</totalSizeCap><!--总文件最多存放20G-->
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>{STANDARD_PATTERN}</pattern>
</encoder>
</appender>
<!--ERROR日志文件-->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- ThresholdFilter:临界值过滤器,过滤掉 TRACE 和 DEBUG 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>error</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>{LOG_HOME}/app_stderr.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory><!--保存最近30天的日志-->
<maxFileSize>10MB</maxFileSize><!--单个文件最大10M-->
<totalSizeCap>20G</totalSizeCap><!--总文件最多存放20G-->
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>${STANDARD_PATTERN}</pattern>
</encoder>
</appender>
<!--这里如果是info,spring、mybatis等框架则不会输出:TRACE < DEBUG < INFO < WARN < ERROR-->
<!--root是所有logger的祖先,均继承root,如果某一个自定义的logger没有指定level,就会寻找
父logger看有没有指定级别,直到找到root。-->
<root level="debug">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE_INFO"/>
<appender-ref ref="FILE_ERROR"/>
</root>
<!--为某个包单独配置logger
<logger name="com.dengchengchao.logbacktest" level="DEBUG" additivity="false">
<appender-ref ref="task" />
</logger>
注意:additivity必须设置为false,这样只会交给task这个appender,否则其他appender也会打印com.dengchengchao.logbacktest里的log信息。
-->
</configuration>
下面一一解释
configuration
: 配置节点scan
:是否启动热部署,默认为truescanPeriod
:热部署扫苗时间间隔,默认单位毫秒debug
:是否开始logback
本身日志信息,默认false
contextName
:相当于为每个APP设置的单独的配置域,用于区分不同的引用程序的记录property
:和所有配置文件一样,key-value
键值对,用于定义变量appender
:定义配置行为name
:唯一ID,方便在后面logger
中指定class
:所对应的行为类,比如要将信息输出到控制台,就得引入ch.qos.logback.core.ConsoleAppender
encoder
:针对appender
的具体配置charset
:日志编码,用于解决乱码问题,一般为UTF-8
pattern
:日志格式定义filter
:日志级别过滤,低于此级别的日志不会触发此appender
logger
:特权logger
定义additivity
:是否继续将消息冒泡level
:开启的日志级别,低于此级别的消息不会被记录appender-ref
:需要使用的appender
其他的在上面的日志中,都有详细的解释。
logback使用
日志的使用,基本上大家都会,不过在这里需要说几点注意事项:
- 由于
logback
是完全实现的slf4j
的接口,因此使用logback
依然是使用日志门面slf4j
-
在获取日志对象过程中,我们有两种方式:
private static final Logger log = LoggerFactory.getLogger(Test.class); private Logger log = LoggerFactory.getLogger(Test.class);
很多人认为使用第一种方式效率更高,然后再
slf4j
中,实际上是做了对象缓存的,每次初始化的对象都会被缓存在缓存池中,第二次使用的时候就会被直接赋值,因此两者差别其实并不大 -
很多人的疑问:为什么使用
logger
需要传入一个xxx.class
?其实这个是logback
的设计初衷:设计者为了使得每个类都单独使用一个logger
,因此使用这个类的名字来作为key
,同时这样在日志输出的时候,能够很方便的定位到时哪个类打印的日志。如果你为了方便,依然随便传一个字符串进去。 -
在
Tomcat
源码中,出现了很多类似这样的代码:if(log.isDebugEnabled()){ log.debug("xxx"+"ddd"+"xxx"); }
为的是在正式环境中,节约一些字符串拼接的效率,然而在
slf4j
中,提供了更友好的字符串拼接日志:log.info("{},ddd,{}","test","hello");
效果完全一样,并且看起来更加美观
参考资料:
[简书]—maxwellyue:slf4j+logback的配置及使用
[掘金]—glmapper:看完这个不会配置 logback ,请你吃瓜!