前面学习了Logback中Logger的name以及level相关的知识,结论如下
Logger name | Assigned level | Effective level |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | none | INFO |
X.Y.Z | none | INFO |
effective level q | ||||||
---|---|---|---|---|---|---|
level of request p | TRACE | DEBUG | INFO | WARN | ERROR | OFF |
TRACE | YES | NO | NO | NO | NO | NO |
DEBUG | YES | YES | NO | NO | NO | NO |
INFO | YES | YES | YES | NO | NO | NO |
WARN | YES | YES | YES | YES | NO | NO |
ERROR | YES | YES | YES | YES | YES | NO |
上面表格当中level of request p指的是logger调用方法时对应的请求等级,而effective level q指的是logger的有效等级,属于logger的固有属性
在logback当中的logger的基于名称的继承结构不但作用于有效level的继承,而且也作用于Appender。那么什么是Appender呢?通过logger可以确定是否需要打印日志,但是往哪里打印日志以及如何打印日志logger是没有办法回答的,此时Appender登场了。通过为logger搭配不同的Appender,可以往不同的目的地打印日志。比如打印到控制台使用ConsoleAppender,打印到文件当中使用FileAppender,甚至打印到数据库当中(DBAppender)。其实Appender属于logback的抽象(logback-core包中),并不是slf4j接口中定义的。可以为logger设置一个Appender,也可以设置多个Appender,甚至不指定Appender。logger将打印任务交给Appender,其定义如下
package ch.qos.logback.core;
import ch.qos.logback.core.spi.ContextAware;
import ch.qos.logback.core.spi.FilterAttachable;
import ch.qos.logback.core.spi.LifeCycle;
public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable<E> {
/**
* Get the name of this appender. The name uniquely identifies the appender.
*/
String getName();
/**
* This is where an appender accomplishes its work. Note that the argument
* is of type Object.
* @param event
*/
void doAppend(E event) throws LogbackException;
/**
* Set the name of this appender. The name is used by other components to
* identify this appender.
*
*/
void setName(String name);
}
这个接口当中两个是setter/getter方法,而doAppend接收的参数类型E取决于不同的Appender实现。但通常来说,在logback-classic
模块当中,E的类型为ILoggingEvent,而在logback-access
模块当中为AccessEvent。可以说这个doAppend是logback框架中最重要的方法了。它负责将日志事件按照某种格式输出到目标设备(有种序列化的概念)。
Appender同样是基于命名的,从结构中getter/setter方法也不难看出名称的重要性。因为需要通过名称来进行引用。
比如在logback的配置文件当中定义了一个名称为STDOUT的ConsoleAppender,然后logger通过appender-ref标签中的ref属性设置为STDOUT来进行引用。
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
</appender>
<!-- Strictly speaking, the level attribute is not necessary since -->
<!-- the level of the root level is set to DEBUG by default. -->
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
另外Appender继承了FilterAttachable类,可以为Appender添加过滤器控制日志事件是否需要进行处理。比如为上面名称为STDOUT的ConsoleAppender添加一个过滤器,添加了这个过滤器之后,只有当日志请求级别为INFO或INFO以上最终才会被打印到控制台,低于INFO会被过滤掉。
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
从这里来看,logger能决定是否需要打印日志,但最终是否打印日志,Appender也是由决定权的。Appenders 最终负责输出日志事件。 但是,他们可以将事件的实际格式委托给 Layout 或 Encoder 对象。 每个布局/编码器都与一个且只有一个 appender 相关联,称为拥有 appender。 一些 appender 具有内置或固定的事件格式。 因此,它们不需要也没有布局/编码器。 例如, SocketAppender 在通过网络传输它们之前简单地序列化日志事件。为名称为STDOUT设置encoder,如下所示
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<!-- encoders are assigned by default the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
通过ch.qos.logback.classic.Logger#addAppender方法可以为logger添加Appender。正如logger按照名称继承结构会继承有效level一样,logger可以继承祖先的Appender。
比如在下面的案例当中
public class LoggerNameTest {
private static final Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
private static final Logger logger = LoggerFactory.getLogger("chapters.introduction");
private static final Logger logger1 = LoggerFactory.getLogger("chapters.introduction.MyApp1");
private static final Logger logger2 = LoggerFactory.getLogger("chapters.introduction.MyApp2");
public static void main(String[] args) {
logger.debug("------------chapters.introduction---------------");
logger1.debug("------------chapters.introduction.MyApp1---------------");
logger2.debug("------------chapters.introduction.MyApp2---------------");
}
}
在配置文件当中定义了root logger的级别以及引用的appender。但是没有定义其他的logger。
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned by default the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
从上面logger2打印日志时使用了名称为名称为STDOUT的appender就不难证明appender确实是可以继承的。而且
与level不同,Appender可以有多个。所以一个logger可以从祖先那里继承多个Appender,比如一个组件包含有Appender1,另一个组件包含了Appender2和Appender3,那么当前logger从祖先那里至少能继承到3个Appender。
在logback.xml配置文件中添加一个logger和一个名称为FILE的RollingFileAppender(向文件中写入日志),并且将这个RollingFileAppender设置到新加入的logger当中。
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- Support multiple-JVMs writing to the same log file -->
<prudent>true</prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<logger name="chapters.introduction" level="debug">
<appender-ref ref="FILE"/>
</logger>
再次执行以上的测试
此时会生成一个日志文件其中包含有日志信息,同时控制台仍然也保留有日志信息。这证明了chapters.introduction.MyApp2
继承了chapters.introduction
和root
两个祖先的Appender。以上这种现象对应专业名称Appender Additivity
。
可是有时候,继承所有祖先的Appender会导致日志的重复打印,那么如何禁止这种Appender的继承传递呢?在logback当中,此时additivity
标识就有效了。通过将一个logger的additivity
属性(默认值为true)设置为false可以让当前的logger不再继承祖先的Appender,同时它的后代也不再继承在它之上的祖先的Appender。比如修改上面的配置,将名称为chapters.introduction
的logger的additivity属性设置为false。
<logger name="chapters.introduction" level="debug" additivity="false">
<appender-ref ref="FILE"/>
</logger>
此时控制台不再有日志了,首先chapters.introduction
自己没有继承root的Appender,其次它的两个后代chapters.introduction.MyApp1
和chapters.introduction.MyApp2
也都没有继承root的Appender。
那么如果设置chapters.introduction.MyApp1
的additivity的属性为false呢?
在配置文件中添加以下配置
<logger name="chapters.introduction.MyApp1" level="debug" additivity="false"/>
此时无论是控制台还是日志文件当中,有没有关于chapters.introduction.MyApp1
的日志了。由于没有为这个logger指定Appender,同时还通过additivity属性禁用了Appender继承,最后这个logger并没有打印日志的功能了。
总结一个表格如下
Logger Name | Attached Appenders | Additivity Flag | Output Targets | Comment |
---|---|---|---|---|
root | A1 | not applicable | A1 | Since the root logger stands at the top of the logger hierarchy, the additivity flag does not apply to it. |
x | A-x1, A-x2 | true | A1, A-x1, A-x2 | Appenders of “x” and of root. |
x.y | none | true | A1, A-x1, A-x2 | Appenders of “x” and of root. |
x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 | Appenders of “x.y.z”, “x” and of root. |
security | A-sec | false | A-sec | No appender accumulation since the additivity flag is set to false. Only appender A-sec will be used. |
security.access | none | true | A-sec | Only appenders of “security” because the additivity flag in “security” is set to false. |
对应的源码参考ch.qos.logback.classic.Logger#callAppenders
public void callAppenders(ILoggingEvent event) {
int writes = 0;
for (Logger l = this; l != null; l = l.parent) {
writes += l.appendLoopOnAppenders(event);
if (!l.additive) {
break;
}
}
// No appenders in hierarchy
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}
首先调用当前logger自己的appendLoopOnAppenders方法打印日志,并且通过for循环不断的获取parent,然后打印日志。但是如果某个正在循环logger的additive属性设置为false,立刻就跳出循环了。
恢复配置文件如下
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned by default the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
并修改测试为
package ch.qos.logback.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
public class LoggerNameTest {
private static final Logger logger = LoggerFactory.getLogger("chapters.introduction");
public static void main(String[] args) {
logger.debug("今天的日期是" + LocalDate.now());
}
}
在这里,我们要打印的信息与以往不同,之前我们需要打印的日志信息是明确的,但是现在打印的信息是会变化的,也可以说之前打印的是静态日志,现在是动态日志,按照官方来说是参数化日志(Parameterized logging
)。那么上面这种方式有啥问题吗?运行一下,结果如下
17:48:10.818 [main] DEBUG chapters.introduction - 今天的日期是2021-08-14
看起来,好像没啥问题。其实这里并不是功能性问题,而是性能问题。因为在判断是否需要真正打印日志之前,这里就从系统中获取了时间然后还进行了字符串的拼接。如下所示
如果是本来就需要打印日志还好,但是如果不需要打印日志呢?将root的level设置为INFO。此时是不会真实打印日志的,但是依旧要获取系统时间然后拼接字符串,而这部分的工作是应该尽量避免的。那么有什么好办法吗?第一种方式可以通过logger的方法判断当前级别是否需要打印日志,只有当满足条件时,才调用打印日志的方法。
public static void main(String[] args) {
if (logger.isDebugEnabled()) {
logger.debug("今天的日期是" + LocalDate.now());
}
}
以上这种方式运行得很好,而且解决了性能问题。只是写法上有点别扭,每次都要去判断一下,感觉有点重复,同时当参数很多的时候,一堆的字符串拼接符号也是一个问题。其实logback有更好的方式可以避免这个问题,通过占位符的方式,如下所示:
public static void main(String[] args) {
logger.debug("今天的日期是{}", LocalDate.now());
}
可以看到在level条件判断之前没有发生字符串的拼接了。只不过此时获取系统日期还是发生了。此时将获取的日志作为参数传递进来。
在buildLoggingEventAndAppend
方法当中会将必要的参数传入并构造一个LoggingEvent
对象。
public LoggingEvent(String fqcn, Logger logger, Level level, String message, Throwable throwable, Object[] argArray) {
this.fqnOfLoggerClass = fqcn;
this.loggerName = logger.getName();
this.loggerContext = logger.getLoggerContext();
this.loggerContextVO = loggerContext.getLoggerContextRemoteView();
this.level = level;
this.message = message;
this.argumentArray = argArray;
if (throwable == null) {
throwable = extractThrowableAnRearrangeArguments(argArray);
}
if (throwable != null) {
this.throwableProxy = new ThrowableProxy(throwable);
LoggerContext lc = logger.getLoggerContext();
if (lc.isPackagingDataEnabled()) {
this.throwableProxy.calculatePackagingData();
}
}
timeStamp = System.currentTimeMillis();
}
这里就包含了message和argArray,在这个对象调用toString
方法的时候就会进行参数的拼接了。本质用到了org.slf4j.helpers.MessageFormatter#arrayFormat方法来进行字符串拼接。
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('[');
sb.append(level).append("] ");
sb.append(getFormattedMessage());
return sb.toString();
}
public String getFormattedMessage() {
if (formattedMessage != null) {
return formattedMessage;
}
if (argumentArray != null) {
formattedMessage = MessageFormatter.arrayFormat(message, argumentArray).getMessage();
} else {
formattedMessage = message;
}
return formattedMessage;
}
上面两种方式都可以解决在不需要进行打印时字符串拼接的问题,但是只有第一种方式可以解决一些复杂对象的提前构造问题(在上面就是获取系统时间),是否可以既优雅编码又解决以上问题的方式呢?通过方法引用(函数式编程)应该是可以的,但是首先得slf4j提供对应的接口才行。