文章主要介绍SpringBoot日志门面与系统,详细说明LogBack
1.日志简述
- 现在大部分项目中使用的日志库可以分为日志门面和日志库(有点接口和实现类的感觉)
- 使用日志库和其它组件一样分为配置和api的使用
1.1 日志门面
1.1.1 common-logging
common-logging 是 apache 的一个开源项目。也称Jakarta Commons Logging,缩写 JCL。
common-logging 的功能是提供日志功能的 API 接口,本身并不提供日志的具体实现(当然,common-logging 内部有一个 Simple logger 的简单实现,但是功能很弱,直接忽略),而是在运行时动态的绑定日志实现组件来工作(如 log4j、java.util.loggin)。
1.1.2 slf4j
全称为 Simple Logging Facade for Java,即 java 简单日志门面。
slf4j是项目中用的比较多的日志门户。类似于 Common-Logging,slf4j 是对不同日志框架提供的一个 API 封装,可以在部署的时候不修改任何配置即可接入一种日志实现方案。但是,slf4j 在编译时静态绑定真正的 Log 库。使用 SLF4J 时,如果你需要使用某一种日志实现,那么你必须选择正确的 SLF4J 的 jar 包的集合(各种桥接包)。
1.2 日志系统
1.2.1 不常用的日志系统
java.util.logging(JUL)
官方提供的日志系统
Log4j
Log4j 应该说是 Java 领域资格最老,应用最广的日志工具。Log4j 是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX 系统日志等。
Log4j 的短板在于性能,在Logback 和 Log4j2 出来之后,Log4j的使用也减少了。
1.2.2 Logback
Logback 是由 log4j 创始人 Ceki Gulcu 设计的又一个开源日志组件,是作为 Log4j 的继承者来开发的,提供了性能更好的实现,异步 logger,Filter等更多的特性。
logback 当前分成三个模块:logback-core、logback-classic 和 logback-access。
- logback-core - 是其它两个模块的基础模块。
- logback-classic - 是 log4j 的一个 改良版本。此外 logback-classic 完整实现 SLF4J API 使你可以很方便地更换成其它日志系统如 log4j 或 JDK14 Logging。
- logback-access - 访问模块与 Servlet 容器集成提供通过 Http 来访问日志的功能。
官网地址: http://logback.qos.ch/
1.2.3 Log4j2
维护 Log4j 的人为了性能又搞出了 Log4j2。
Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。
Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。
2. logback
放个官方教程
logback是由原来log4j1的团队编写的,成为了log4j1的替代品,是项目中使用较多的日志系统
官方给出使用logback而不是log4j1的原因
2.1 依赖导入
SpringBoot项目默认的日志实现就是logback,可以不导入任何依赖使用,如果不是Spring应用可以通过以下方式导入
1 2 3 4 5
| <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.6</version> </dependency>
|
项目中日志经常配合AOP使用,这里需要导入一下AOP的依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
2.2 简单使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
@Aspect @Component public class WebLogAspect { public static final Logger logger= LoggerFactory.getLogger(WebLogAspect.class); @Around("execution(public void javax.servlet.http.HttpServlet.service(..)))") public Object log(ProceedingJoinPoint jp) throws Throwable { Object[] args = jp.getArgs(); HttpServletRequest request = (HttpServletRequest) args[0]; HttpServletResponse response = (HttpServletResponse) args[1]; String url = request.getRequestURL().toString(); String method = request.getMethod(); long startTime = System.currentTimeMillis(); try{ jp.proceed(new Object[]{request,response}); }catch (Throwable e){ throw e; } long time = (System.currentTimeMillis() - startTime); logger.info("请求路径为{},请求参数为{}",url,method); logger.info("响应状态码为{}",response.getStatus()); logger.info("接口响应时间为{}ms",time); return null; } }
|
这个切面会拦截所有请求,然后打印出请求的路径、参数,响应的状态码以及响应时间。AOP以及javaWeb的内容不是本文的重点,我们重点关注一下Logger
现在问题来了,这个Logger
到底是什么?从WebLogAspect.class
中getLogger是什么意思?logger.info()又是什么方法?
为了解答这些问题我们先来看看logback日志打印的一些组件(其实log4j1、logback、log4j2中组件都差不多,只是配置文件写法可能有点差异)
2.3 组件说明
2.3.1 Logger
logback中的记录器都会缓存在一个Map中(就像Spring缓存Bean那样),记录器Logger是有层级、有继承关系的(具体见LoggerContext)。
Logback的Logger是slf4j中Logger的实现,下面是logback中Logger的实现以及Logger的一些属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package ch.qos.logback.classic;
public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable { public static final String FQCN = Logger.class.getName(); private String name; private transient Level level; private transient int effectiveLevelInt; private transient Logger parent; private transient List<Logger> childrenList; private transient AppenderAttachableImpl<ILoggingEvent> aai; private transient boolean additive = true; final transient LoggerContext loggerContext; }
|
Logger中的属性
name:上面我们知道记录器是缓存在Map中的,这个name就是Map的key
LoggerFactory.getLogger()
的入参有两个,name 和 class,传入class的时候,name就是类的全限定名
Level:我们知道日志是有级别的,比如上面调用的info
,级别关系如下
TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
Logger中的Level表示这个Logger能支持打印的最低级别,默认是 DEBUG,当调用低于Level级别日志打印方法时会没有显示(可以把上面代码改成trace()方法试试)
parent与childrenList
用来维护Logger层级关系的属性,getLogger()
时会先从Map里面拿,取不到的时候再创建,Logger创建时会检测有无父Logger,没有的话会创建父Logger,然后会检测父Logger有无父Logger,循环往上直到碰到顶级Logger(name为ROOT的Logger)。
举例:
1 2
| public static final Logger logger= LoggerFactory.getLogger(WebLogAspect.class);
|
那么创建的时候就会先检测是否有name为**”WebLogAspect”的Logger,没有的话再检测是否有 name为 aspect的Logger,又没有的话会检测是否有 name为 demo的Logger … 直到检测到没有 name为org的Logger ,org的父类是 “ROOT”**,这时会根据ROOT中的属性来创建 **”org”**,根据 **”org”**创建 **”example”**直到 **”WebLogAspect”**被创建
系统默认的属性都会放在**”ROOT”**中,比如上面说的Level默认是DEBUG
2.3.2 appender
Logger会将输出日志的任务交给Appender(附加器)来完成,不同的Appender会将日志打印到不同的地方,比如控制台附加器、文件附加器、网络附加器
Logback中的appender是一个接口,以下是官网扒下来的结构图
除了UnsynchronizedAppenderBase
,Appender的基本实现还有一个AppenderBase
,这两者的区别看名字就能猜到
1 2 3 4
| public synchronized void doAppend(E eventObject) {...}
public void doAppend(E eventObject) {...}
|
Logger与Appender的关联
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable { private transient AppenderAttachableImpl<ILoggingEvent> aai; public int appendLoopOnAppenders(E e) { int size = 0; Appender<E>[] appenderArray = (Appender[])this.appenderList.asTypedArray(); int len = appenderArray.length; for(int i = 0; i < len; ++i) { appenderArray[i].doAppend(e); ++size; }
return size; } } public class AppenderAttachableImpl<E> implements AppenderAttachable<E> { private final COWArrayList<Appender<E>> appenderList = new COWArrayList(new Appender[0]); }
|
2.3.3 信息的过滤以及处理
filter
Filter是用来决定附加器是否输出日志的,与Appender的关系如下
1 2 3 4
| public abstract class UnsynchronizedAppenderBase<E> extends ContextAwareBase implements Appender<E> { private FilterAttachableImpl<E> fai = new FilterAttachableImpl(); }
|
每个过滤器都会返回一个枚举值,枚举的值有:DENY、NEUTRAL、ACCEPT。
附加器会根据过滤器的返回值来判断是否输出日志:
- DENY:不输出日志
- NEUTRAL:中立
- ACCPET:输出日志
多个过滤器的情况下,会以链式的方式进行过滤,当过滤器的返回值为 DENY、ACCPET时,会直接决定附加器是否打印日志;当过滤器返回值为 NEUTRAL时会执行判断下一个过滤器,直达出现返回值为 DENY、ACCPET或者没有过滤器结束,最后一个过滤器的返回值为 NEUTRAL也会打印日志
encoder
Encoders are responsible for transforming an incoming event into a byte array(官方原话,负责将需要打印的event转成字节数组)
1 2 3 4 5 6 7
| public interface Encoder<E> extends ContextAware, LifeCycle { byte[] headerBytes();
byte[] encode(E var1);
byte[] footerBytes(); }
|
通俗的来说Encoder是用来将信息格式化输出的
2.4 配置文件
2.4.1 spring 配置文件
先来看看Spring配置文件中能配置的属性(application的配置粒度不够细,并且不利于更换日志系统,所以项目一般不会直接在application中配置日志信息)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| logging: level: com.example.springboot03: trace pattern: console: '%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSSXXX}})' dateformat: yyyy-MM-dd HH:mm:ss file: name: F:/WorkSpaceSpringBoot/my.log logback: rolling policy: clean-history-on-start: false file-name-pattern: '${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz' max-file-size: 2KB max-history: 7 total-size-cap: 4KB
|
2.4.2 logback配置文件(可以说是logback的核心了)
文件名为”logback-spring.xml”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <?xml version="1.0" encoding="UTF-8"?> <configuration debug="false">
<property name="LOG_HOME" value="/home" />
<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%n</pattern> </encoder> </appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <charset class="java.nio.charset.Charset">UTF-8</charset> </encoder> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender> <logger name="com.apache.ibatis" level="TRACE"/> <logger name="java.sql.Connection" level="DEBUG"/> <logger name="java.sql.Statement" level="DEBUG"/> <logger name="java.sql.PreparedStatement" level="DEBUG"/>
<root level="DEBUG"> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE"/> </root> </configuration>
|
参考
Logback Manual