Logback学习系列(三) - Appender初探


Logback学习系列


前面学习了Logback中Logger的name以及level相关的知识,结论如下
在这里插入图片描述
在这里插入图片描述

Logger nameAssigned levelEffective level
rootDEBUGDEBUG
XINFOINFO
X.YnoneINFO
X.Y.ZnoneINFO

在这里插入图片描述

effective level q
level of request pTRACEDEBUGINFOWARNERROROFF
TRACEYESNONONONONO
DEBUGYESYESNONONONO
INFOYESYESYESNONONO
WARNYESYESYESYESNONO
ERRORYESYESYESYESYESNO

上面表格当中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.introductionroot两个祖先的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.MyApp1chapters.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 NameAttached AppendersAdditivity FlagOutput TargetsComment
rootA1not applicableA1Since the root logger stands at the top of the logger hierarchy, the additivity flag does not apply to it.
xA-x1, A-x2trueA1, A-x1, A-x2Appenders of “x” and of root.
x.ynonetrueA1, A-x1, A-x2Appenders of “x” and of root.
x.y.zA-xyz1trueA1, A-x1, A-x2, A-xyz1Appenders of “x.y.z”, “x” and of root.
securityA-secfalseA-secNo appender accumulation since the additivity flag is set to false. Only appender A-sec will be used.
security.accessnonetrueA-secOnly 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提供对应的接口才行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值