干翻全岛蛙蛙 发表于 2025-2-12 15:13:47

6.appender

一、媒介

前一篇文章先容了appender、conversionRule、root和logger节点的解析, 为的是为本篇详细先容它们的原理做铺垫, 日记打印也是主要围绕这几个对象开展的
二、源码解析

Appender

https://i-blog.csdnimg.cn/direct/e1c3edb341704833be3e01d9812e70ce.png#pic_center
在 Logback 框架中,Appender 是用来将日记事件输出到目标(如文件、控制台、数据库等)的组件。而 UnsynchronizedAppenderBase 和 AppenderBase 是两种焦点的抽象类,提供了实现日记输出的基础功能。
下面是UnsynchronizedAppenderBase 和 AppenderBase 的对比; 我们常用的ConsoleAppender和RollingFileAppender都是UnsynchronizedAppenderBase 的子类
特性UnsynchronizedAppenderBaseAppenderBase线程安全性不线程安全线程安全同步机制无需同步(开发者需手动处置惩罚)内置同步机制性能性能更高,因为没有同步开销性能稍低,因为引入了同步逻辑实用场景单线程或已外部同步的高性能需求场景多线程环境下无需手动同步的场景 appender接口定义
实现了FilterAttachable接口哦
public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable<E> {

    /**
   * 设置名称
   */
    void setName(String name);
   
    /**
   * 获取appender的名称
   */
    String getName();

    /**
   * 添加日志
   */
    void doAppend(E event) throws LogbackException;
}

public interface FilterAttachable<E> {
    /**
   * 添加过滤器
   */
    void addFilter(Filter<E> newFilter);
        /*
       * 清空过滤器
       */
    void clearAllFilters();

    /*
   * 获取复制所有的过滤器
   */
    List<Filter<E>> getCopyOfAttachedFiltersList();

    /*
   * 循环遍历链中的过滤器。一旦过滤器决定ACCEPT或DENY,则返回该值。如果所有过滤器都返回NEUTRAL,则返回NEUTRAL。
   */
    FilterReply getFilterChainDecision(E event);
}
接口比较简朴, 焦点方法就是这个doAppend, 用于添加我们的日记。
appender继承了FilterAttachable接口, 添加了对过滤器的支持, 允许我们根据过滤器判定是否必要打印日记
这里可以看出appender是支持filter标签的(因为addFilter方法)
UnsynchronizedAppenderBase

非同步Appender
abstract public class UnsynchronizedAppenderBase<E> extends ContextAwareBase implements Appender<E> {
    /**
   * 用来阻止当前线程递归调用doAppend方法
   */
    private ThreadLocal<Boolean> guard = new ThreadLocal<Boolean>();
   
    /**
   * 它是FilterAttachable接口的实现类, 使用静态代理模式
   */
    private FilterAttachableImpl<E> fai = new FilterAttachableImpl<E>();
   
    /** 模板方法 */
    public void doAppend(E eventObject) {
      // 阻止当前线程递归调用doAppend方法
      if (Boolean.TRUE.equals(guard.get())) {
            return;
      }
      try {
            // 设置当前线程已经进来的标识
            guard.set(Boolean.TRUE);
            // appender还未启动
            if (!this.started) {
                // 还未到允许重试此时, 输出警告信息
                if (statusRepeatCount++ < ALLOWED_REPEATS) {
                  addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
                }
                return;
            }
            // 过滤器处理, 如果返回DENY则不处理
            if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
                return;
            }
            // 子类的append方法, 去添加日志吧
            this.append(eventObject);

      } catch (Exception e) {
            // 异常次数不达最大重试次数, 输出异常信息
            if (exceptionCount++ < ALLOWED_REPEATS) {
                addError("Appender [" + name + "] failed to append.", e);
            }
      } finally {
            // 释放当前线程的标识
            guard.set(Boolean.FALSE);
      }
    }
    /** 需子类实现 */
    abstract protected void append(E eventObject);
   
    /** 下面这四个方法都是静态代理的体现 */
    public void addFilter(Filter<E> newFilter) {
      // 添加过滤器到代理对象FilterAttachableImpl中
      fai.addFilter(newFilter);
    }
    public void clearAllFilters() {
      fai.clearAllFilters();
    }
    public List<Filter<E>> getCopyOfAttachedFiltersList() {
      return fai.getCopyOfAttachedFiltersList();
    }
    public FilterReply getFilterChainDecision(E event) {
      return fai.getFilterChainDecision(event);
    }
}
方法小结

[*]利用静态代理对象FilterAttachableImpl实现对FilterAttachable接口的实现, UnsynchronizedAppenderBase中实现的FilterAttachable接口中的方法都是由静态代理对象FilterAttachableImpl处置惩罚
[*]模板方法doAppend用来处置惩罚公共逻辑
[*]一个线程一次只能打印一条日记, 为了避免子类appender中递归doAppend方法, 所以这里利用ThreadLocal做一个拦截校验
[*]利用过滤器先对日记事件做一次拦截, 如果拦截器返回了FilterReply.DENY, 该日记将会被扬弃
[*]最后调用appender方法交给子类实现详细的日记打印逻辑
这里读者可以去了解下静态代理、动态代理、正向代理、方向代理的区别, 以及门面模式、包装模式和静态代理的区别; 看到源码里面出现奇怪的设计, 请不要慌, 肯定是有章法的, 要有一颗好奇的心。
我们看一下FilterAttachableImpl类的getFilterChainDecision方法
FilterAttachableImpl
public FilterReply getFilterChainDecision(E event) {

    final Filter<E>[] filterArrray = filterList.asTypedArray();
    final int len = filterArrray.length;
        // 遍历过滤器
    for (int i = 0; i < len; i++) {
      // 过滤器处理之后返回FilterReply
      final FilterReply r = filterArrray.decide(event);
      // 只要是返回DENY或者ACCEPT类型, 就直接返回
      if (r == FilterReply.DENY || r == FilterReply.ACCEPT) {
            return r;
      }
    }

    // no decision
    return FilterReply.NEUTRAL;
}
这里看到过滤器只要返回FilterReply.DENY和FilterReply.ACCEPT就直接返回了, FilterReply.DENY代表了肯定拒绝, FilterReply.ACCEPT代表了肯定通过, 只有FilterReply.NEUTRAL属于暗昧状态, 必要继续走下去。就像你去追一个人, 对方要是说yes那就成了, 对方说no那就拜拜, 对方要是说or, 那就完犊子了, 要打持久战了。
OutputStreamAppender

OutputStreamAppender 是一个基础组件,它负责将日记事件写入输出流,并提供了一些通用功能,比如流管理和结构支持。由于它的设计简朴且功能集中,其他复杂的 Appender(如文件和滚动日记输出)都可以基于它构建
public class OutputStreamAppender<E> extends UnsynchronizedAppenderBase<E> {
   
    /**
   * encoder最终负责将事件写入OutputStream。
   */
    // getter/setter
    protected Encoder<E> encoder;
   
    /** 数据写入的目的地 */
    private OutputStream outputStream;
   
    /** 是否立即刷新数据 */
    boolean immediateFlush = true;
   
    public void start() {
      int errors = 0;
      // ... encoder 和 outputStream不能为空
      if (this.encoder == null) {
            addStatus(new ErrorStatus("No encoder set for the appender named \"" + name + "\".", this));
            errors++;
      }
      // ... 省略部分代码
      // only error free appenders should be activated
      if (errors == 0) {
            // 标记为启动
            super.start();
            // 添加初始化数据, 每次loggerContext启动的时候(一般也是项目启动), 可以记录日志
            encoderInit();
      }
    }
   
    void encoderInit() {
      if (encoder != null && this.outputStream != null) {
            try {
                byte[] header = encoder.headerBytes();
                writeBytes(header);
            } catch (IOException ioe) {
                this.started = false;
                // ...
            }
      }
    }
    /** 核心方法, 实现父类的抽象方法 */
    protected void subAppend(E event) {
      // 未启动不处理
      if (!isStarted()) {
            return;
      }
      try {
            // loggingEvent默认是DeferredProcessingAware
            if (event instanceof DeferredProcessingAware) {
                // 1.预处理消息; 包括填充消息中的占位符, 将mdc数据初始化到loggingEvent中
                ((DeferredProcessingAware) event).prepareForDeferredProcessing();
            }
            // 2.写出数据
            writeOut(event);

      } catch (IOException ioe) {
            // 标记为启动失败
            this.started = false;
            addStatus(new ErrorStatus("IO failure in appender", this, ioe));
      }
    }
   
    protected void writeOut(E event) throws IOException {
      // 编码数据
      byte[] byteArray = this.encoder.encode(event);
      // 写出数据
      writeBytes(byteArray);
    }
   
    private void writeBytes(byte[] byteArray) throws IOException {
      if (byteArray == null || byteArray.length == 0)
            return;
                // 这里是加非公平锁
      streamWriteLock.lock();

      try {
            // 得启动成功后才能写数据
            if (isStarted()) {
                // 写出数据
                writeByteArrayToOutputStreamWithPossibleFlush(byteArray);
                // 更新写出数据量
                updateByteCount(byteArray);
            }
      } finally {
            streamWriteLock.unlock();
      }
    }
   
   protected final void writeByteArrayToOutputStreamWithPossibleFlush(byte[] byteArray) throws IOException {
         // 写出数据
      this.outputStream.write(byteArray);
      // 如果立即刷新(默认是true)
      if (immediateFlush) {
            // 数据刷出到目的地
            this.outputStream.flush();
      }
    }
   
    /** 设置layout */
    public void setLayout(Layout<E> layout) {
      addWarn("This appender no longer admits a layout as a sub-component, set an encoder instead.");
      addWarn("To ensure compatibility, wrapping your layout in LayoutWrappingEncoder.");
      addWarn("See also " + CODES_URL + "#layoutInsteadOfEncoder for details");
      // layout默认使用LayoutWrappingEncoder覆盖已有的encoder
      LayoutWrappingEncoder<E> lwe = new LayoutWrappingEncoder<E>();
      lwe.setLayout(layout);
      lwe.setContext(context);
      this.encoder = lwe;
    }
}
由于OutputStreamAppender类比较简朴, 这里就不一个个方法详细看了
方法小结

[*]如果设置了OutputStreamAppender范例的appender, 在logback框架启动的时候, 可以设置encoder的headerBytes参数, 打印在运行的开始
[*]OutputStreamAppender类实现了UnsynchronizedAppenderBase的subAppend方法
[*]先处置惩罚日记信息, 比方添补日记的占位符(比方: log.info(“你好{}”, “uncelqiao”)), 这里会转换成"你好uncelqiao", 还会把mdc上下文放到日记事件LoggingEvent中
[*]写出数据到outputStream中, 这里有一个immediateFlush的boolean字段, 用于控制是否立刻将内容刷出, 默认是true
[*]更新写入的字节数, 可以用来记载总写入数,然后切割文件
[*]setLayout方法覆盖了encoder属性, 也就是说你先设置了encoder再设置layout标签的话, 前一个encoder会失效, 一样寻常我们不这么用
这里可以看出OutputStreamAppender范例的appender是支持添加encoder、OutputStream、immediateFlush、layout标签的(因为对应的setter方法)
ConsoleAppender

用来将日记打到控制台的
public class ConsoleAppender<E> extends OutputStreamAppender<E> {
    /**
   * 日志除数目的地; 默认是System.out
   */
    protected ConsoleTarget target = ConsoleTarget.SystemOut;
    /**
   * 是否使用jansi框架打印日志
   */
    protected boolean withJansi = false;

    @Override
    public void start() {
      // 这里提醒我们打印到控制台的速度是很慢的, 应该避免在生产环境开启打印到控制台
      addInfo("BEWARE: Writing to the console can be very slow. Avoid logging to the ");
      addInfo("console in production environments, especially in high volume systems.");
      addInfo("See also " + CONSOLE_APPENDER_WARNING_URL);
      OutputStream targetStream = target.getStream();
      // 开启了jansi日志打印, 就使用withJansi的OutputStream
      if (withJansi) {
            targetStream = wrapWithJansi(targetStream);
      }
      // 设置日志打印目的地
      setOutputStream(targetStream);
      super.start();
    }
   
    /** 设置打印目的地, 这里可以选择System.out或者System.err */
    public void setTarget(String value) {
      ConsoleTarget t = ConsoleTarget.findByName(value.trim());
      if (t == null) {
            targetWarn(value);
      } else {
            target = t;
      }
    }
}
方法小结

[*]ConsoleAppender类主要是用来提供日记打印的位置OutputStream, 默认是OutputStream, 利用System.out.write写出数据, System.out.flush()刷新数据
[*]可以在logback.xml的ConsoleAppender定义子标签target来设置利用System.out还是System.err
[*]可以利用withJansi标签开启利用JANSI框架打印日记, 必要引入 org.fusesource.jansi:jansi:{version}包, 可以在这里看版本信息 https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
ConsoleAppender给appender标签提供了target子节点来设置日记输出流
综上理解, 我们可以把ConsoleAppender设置成这样
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- ThresholdFilter用于控制当前appender允许打印的日志级别 -->
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>INFO</level>
    </filter>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %X{mdcKey} %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <immediateFlush>true</immediateFlush>
    <target>System.out</target>
    <withJansi>false</withJansi>
</appender>
关于其中的layout和encoder的设置, appender中encoder和layout有先后次序, 背面的覆盖前面的encoder, 这里推荐下面的第2种设置
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- 1.不指定时class, 默认是PatternLayoutEncoder, 它使用默认的PatternLayout类 -->
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <!-- 2.也可以使用可以指定layout的encoder; 推荐使用这种 -->
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
      <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}) %clr([%X{traceId}]){magenta} %clr([%thread]){blue} %clr(%-5level) %clr(%logger{50}){cyan} %clr(%file:%line){cyan} - %msg%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
      </layout>
    </encoder>
    <!-- 3.单独用layout的话, 默认使用的encoder就是LayoutWrappingEncoder -->
    <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
    </layout>
</appender>
FileAppender

fileAppender也是OutputStreamAppender的直接子类, 用来将日记输入到文件
public class FileAppender<E> extends OutputStreamAppender<E> {
    /** 缓存流可缓存大小 */
    public static final long DEFAULT_BUFFER_SIZE = 8192;
   
    /** 日志文件内容追加到末尾 */
    protected boolean append = true;
   
    /**
   * 日志文件名
   */
    protected String fileName = null;
    /** 是否启用严格模式 */
    private boolean prudent = false;
   
    public void start() {
      int errors = 0;
      // 文件名, 获取的是fileName字段
      if (getFile() != null) {
            addInfo("File property is set to [" + fileName + "]");
                        // 1.严格模式下必须设置为追加模式
            if (prudent) {
                if (!isAppend()) {
                  setAppend(true);
                  addWarn("Setting \"Append\" property to true on account of \"Prudent\" mode");
                }
            }
            // 2.判断当前文件是否在当前日志上下文中已经存在过
            if (checkForFileCollisionInPreviousFileAppenders()) {
                addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting.");
                addError(MORE_INFO_PREFIX + COLLISION_WITH_EARLIER_APPENDER_URL);
                errors++;
            } else {
                // file should be opened only if collision free
                try {
                  // 3.打开文件, 设置outputStream
                  openFile(getFile());
                } catch (java.io.IOException e) {
                  errors++;
                  addError("openFile(" + fileName + "," + append + ") call failed.", e);
                }
            }
      } else {
            errors++;
            addError("\"File\" property not set for appender named [" + name + "].");
      }
      if (errors == 0) {
            super.start();
      }
    }
   
    public void openFile(String file_name) throws IOException {
      // 非公平锁
      streamWriteLock.lock();
      try {
            File file = new File(file_name);
            // 判断文件目录是否存在
            boolean result = FileUtil.createMissingParentDirectories(file);
            if (!result) {
                addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
            }
            // 得到文件的outputStream
            ResilientFileOutputStream resilientFos = new ResilientFileOutputStream(file, append, bufferSize.getSize());
            resilientFos.setContext(context);
            // 设置OutputStreamAppender中的outputStream属性值
            setOutputStream(resilientFos);
      } finally {
            streamWriteLock.unlock();
      }
    }
}
这个start方法有点东西

[*]严酷模式下, 必定开启文件追加模式(append=true)
[*]当前日记框架启动过程中如果已经设置了当前日记文件在LoggerContext上下文中, 那么当前的appender将会失效(目前不知道什么场景下会有这种情况)
[*]fileName属性是全路径, 利用的是ResilientFileOutputStream包装了一下FileOutputStream类作为日记输出流
下面看看写入数据
@Override
protected void writeOut(E event) throws IOException {
    // 严格模式
    if (prudent) {
      // 安全写入
      safeWriteOut(event);
    } else {
      // 直接使用OutputStreamAppender的writeOut写出数据
      super.writeOut(event);
    }
}

private void safeWriteOut(E event) {
    byte[] byteArray = this.encoder.encode(event);
    if (byteArray == null || byteArray.length == 0)
      return;

    streamWriteLock.lock();
    try {
       safeWriteBytes(byteArray);
    } finally {
      streamWriteLock.unlock();
    }
}

private void safeWriteBytes(byte[] byteArray) {
    ResilientFileOutputStream resilientFOS = (ResilientFileOutputStream) getOutputStream();
    FileChannel fileChannel = resilientFOS.getChannel();
    if (fileChannel == null) {
      return;
    }

    // 1.清除当前线程的中断状态,并获取中断状态, 因为fileChannel.lock()有对当前线程是否中断的判断
    boolean interrupted = Thread.interrupted();

    FileLock fileLock = null;
    try {
      // 2.加个文件锁, 不是juc的类哦
      fileLock = fileChannel.lock();
      long position = fileChannel.position();
      long size = fileChannel.size();
      if (size != position) {
            // 3.移动写入点
            fileChannel.position(size);
      }
      // 4.调用OutputStreamAppender的writeByteArrayToOutputStreamWithPossibleFlush方法写出数据
      writeByteArrayToOutputStreamWithPossibleFlush(byteArray);
    } catch (IOException e) {
      // Mainly to catch FileLockInterruptionExceptions (see LOGBACK-875)
      resilientFOS.postIOFailure(e);
    } finally {
      releaseFileLock(fileLock);

      // 5.设置为线程本来的状态
      if (interrupted) {
            Thread.currentThread().interrupt();
      }
    }
}
方法小结

[*]严酷模式下, 会清除当火线程的中断状态
[*]写数据之前加个文件锁, 当前文件只能我访问哦(降级效率的做法)
[*]移动写入点到文件末尾
[*]写入数据
[*]还原线程原本的状态(这一趟清除中断状态 再到规复状态 会不会有aba的题目呢??)
[*]非严酷模式下直接利用OutputStreamAppender的writeOut写出数据
要说明一点, 这里实际写入数据利用的是FileOutputStream, 但是移动指针是利用的是fileChannel, 实在底层FileOutputStream 和 FileChannel 利用同一个文件指针
做个对比
特性FileOutputStreamFileChannel操作灵活性简朴易用,只支持次序写入支持随机访问,可移动文件指针、切片等操作同步机制不支持直接同步多个线程操作支持多线程同步,线程安全写入方式只能直接写字节数据支持直接写入和缓冲区操作性能相对较低性能更高,特殊是在处置惩罚大文件时 对FileAppender做一个小结

[*]FileAppender也是OutputStreamAppender的子类, 利用outputStream将日记写出到文件, 利用的是BufferedOutputStream
[*]利用appender属性设置日记文件以追加的形式记载
[*]利用file属性设置日记记载的位置, file属性是文件的全路径
[*]利用prudent=true/false 来开关严酷模式, 建议用false大概直接不设置, 蛮耗性能的, 还要独占文件
RollingFileAppender

主要用于将日记信息写入文件,并支持 文件滚动(Rolling),以避免日记文件过大或超出存储限制。
public class RollingFileAppender<E> extends FileAppender<E> {
    /**
   * 当前激活的文件
   */
    File currentlyActiveFile;
   
    /**
   * 触发策略
   */
    TriggeringPolicy<E> triggeringPolicy;
   
    /**
   * 滚动策略
   */
    RollingPolicy rollingPolicy;
   
    public void start() {
      // ...
    }
   
    @Override
    public String getFile() {
      // file属性可以为空, 为空时默认是以fileNamePattern的格式生成文件
      return rollingPolicy.getActiveFileName();
    }
   
    public void rollover() {
      // ...
    }
   
    protected void subAppend(E event) {
           // ...   
    }
   
    protected void updateByteCount(byte[] byteArray) {
      // ...
    }
}
RollingFileAppender的焦点就是这几个属性和方法


[*]currentlyActiveFile: 当前记载日记的文件
[*]triggeringPolicy 触发滚动的策略
[*]rollingPolicy 滚动策略
[*]start方法: 校验并初始化一些焦点内容
[*]getFile方法: 获取当前利用的日记文件
[*]rollover方法: 滚动文件
[*]subAppend: 添加日记
[*]updateByteCount: 更新写入的数量
start方法
public void start() {
    // 需要先设置触发策略
    if (triggeringPolicy == null) {
      addWarn("No TriggeringPolicy was set for the RollingFileAppender named " + getName());
      addWarn(MORE_INFO_PREFIX + RFA_NO_TP_URL);
      return;
    }
    // 需要先启动触发策略,
    if (!triggeringPolicy.isStarted()) {
      addWarn("TriggeringPolicy has not started. RollingFileAppender will not start");
      return;
    }
    // 判断是否有同一个格式的文件名, 不允许
    if (checkForCollisionsInPreviousRollingFileAppenders()) {
      addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting.");
      addError(MORE_INFO_PREFIX + COLLISION_WITH_EARLIER_APPENDER_URL);
      return;
    }

    // we don't want to void existing log files
    // 默认是append
    if (!append) {
      addWarn("Append mode is mandatory for RollingFileAppender. Defaulting to append=true.");
      append = true;
    }
    // 必须有滚动策略
    if (rollingPolicy == null) {
      addError("No RollingPolicy was set for the RollingFileAppender named " + getName());
      addError(MORE_INFO_PREFIX + RFA_NO_RP_URL);
      return;
    }

    // sanity check for http://jira.qos.ch/browse/LOGBACK-796
    // 校验文件名是否满足配置的文件格式, <file>标签和<fileNamePattern>标签格式不能相同
    if (checkForFileAndPatternCollisions()) {
      addError("File property collides with fileNamePattern. Aborting.");
      addError(MORE_INFO_PREFIX + COLLISION_URL);
      return;
    }
    // 严格模式
    if (isPrudent()) {
      if (rawFileProperty() != null) {
            addWarn("Setting \"File\" property to null on account of prudent mode");
            setFile(null);
      }
      if (rollingPolicy.getCompressionMode() != CompressionMode.NONE) {
            addError("Compression is not supported in prudent mode. Aborting");
            return;
      }
    }

    addInfo("Active log file name: " + getFile());
    currentlyActiveFile = new File(getFile());
    // 初始化-记录已有文件的长度
    initializeLengthCounter();
    // 父类启动, 设置outputStream
    super.start();
}
方法小结

[*] 触发策略不能为空, 并且要先启动, 它在ImplicitModelHandler中会默认先启动
[*] 默认是文件追加模式
[*] 滚动策略不能为空
[*] file标签和fileNamePattern标签格式不能雷同, 比方下面的是不允许的
<file>${log.path}/2025-02/roller_test.2025-02-09.log.gz</file>
    <fileNamePattern>${log.path}/%d{yyyy-MM, aux, Asia/Shanghai}/roller_test.%d{yyyy-MM-dd}.log.gz</fileNamePattern>

[*] file标签是允许为空的, 但是fileNamePattern标签不能为空
[*] 利用rollingPolicy获取当前激活的文件(当前写入日记的文件)
[*] 初始化文件大小记载器
RollingPolicy
滚动策略, 这里以TimeBasedRollingPolicy为例先容
public class TimeBasedRollingPolicy<E> extends RollingPolicyBase implements TriggeringPolicy<E> {
    /** 无压缩后缀的文件命名模式 */
    FileNamePattern fileNamePatternWithoutCompSuffix;
   
    /**
   * 压缩器
   */
    private Compressor compressor;
   
    /**
   * 文件从命名工具类
   */
    private RenameUtil renameUtil = new RenameUtil();
   
    /**
   * 存档删除器
   */
    private ArchiveRemover archiveRemover;
   
    /**
   * 触发策略; 默认是DefaultTimeBasedFileNamingAndTriggeringPolicy; 使用静态代理
   */
    TimeBasedFileNamingAndTriggeringPolicy<E> timeBasedFileNamingAndTriggeringPolicy;
}
TimeBasedRollingPolicy继承了RollingPolicyBase抽象类, 说明它是一个滚动策略, 实现了TriggeringPolicy, 也说明白它是一个触发策略。
从几个属性可以看得出来

[*]compressor: 它支持压缩日记文件
[*]archiveRemover: 支持删除存档过期的文件
[*]timeBasedFileNamingAndTriggeringPolicy: 静态代理触发策略
start方法
public void start() {
    // set the LR for our utility object
    renameUtil.setContext(this.context);

    // 从文件名模式中找出周期; 由fileNamePattern标签配置的文件表达式
    if (fileNamePatternStr != null) {
      fileNamePattern = new FileNamePattern(fileNamePatternStr, this.context);
      // 根据后缀判断压缩模式, 并设置
      determineCompressionMode();
    } else {
      addWarn(FNP_NOT_SET);
      addWarn(CoreConstants.SEE_FNP_NOT_SET);
      throw new IllegalStateException(FNP_NOT_SET + CoreConstants.SEE_FNP_NOT_SET);
    }
    // 文件压缩对象
    compressor = new Compressor(compressionMode);
    compressor.setContext(context);

    // wcs : without compression suffix
    // 无压缩后缀的文件命名器
    fileNamePatternWithoutCompSuffix = new FileNamePattern(
            Compressor.computeFileNameStrWithoutCompSuffix(fileNamePatternStr, compressionMode), this.context);

    addInfo("Will use the pattern " + fileNamePatternWithoutCompSuffix + " for the active file");
    // 压缩模式
    if (compressionMode == CompressionMode.ZIP) {
      // 获取文件名
      String zipEntryFileNamePatternStr = transformFileNamePattern2ZipEntry(fileNamePatternStr);
      zipEntryFileNamePattern = new FileNamePattern(zipEntryFileNamePatternStr, context);
    }
    // 默认使用DefaultTimeBasedFileNamingAndTriggeringPolicy
    if (timeBasedFileNamingAndTriggeringPolicy == null) {
      timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<>();
    }
    timeBasedFileNamingAndTriggeringPolicy.setContext(context);
    // 触发策略设置滚动策略
    timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
    // 启动触发策略
    timeBasedFileNamingAndTriggeringPolicy.start();

    if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
      addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
      return;
    }

    // the maxHistory property is given to TimeBasedRollingPolicy instead of to
    // the TimeBasedFileNamingAndTriggeringPolicy. This makes it more convenient
    // for the user at the cost of inconsistency here.
    // 最大保存历史记录
    if (maxHistory != UNBOUNDED_HISTORY) {
      archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
      // 保留的文件个数
      archiveRemover.setMaxHistory(maxHistory);
      // 保留的文件大小最大阈值
      archiveRemover.setTotalSizeCap(totalSizeCap.getSize());
      // 启动时删除过期文件
      if (cleanHistoryOnStart) {
            addInfo("Cleaning on start up");
            Instant now = Instant.ofEpochMilli(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
            cleanUpFuture = archiveRemover.cleanAsynchronously(now);
      }
    } else if (!isUnboundedTotalSizeCap()) {
      addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value [" + totalSizeCap + "]");
    }

    super.start();
}
方法小结

[*]fileNamePattern标签必须设置, 并且fileNamePattern对象是通过fileNamePattern标签的值构建的
[*]压缩模式是根据fileNamePattern标签的后缀决定的, 支持.zip和.gz格式压缩(没有压缩后缀也行, 那就不举行压缩)
[*]默认利用DefaultTimeBasedFileNamingAndTriggeringPolicy作为滚动触发策略
[*]可以通过maxHistory标签设置最多保存多少天的存档文件, 和保存的文件大小
触发滚动
添加日记时会先判定是否必要先滚动文件
public boolean isTriggeringEvent(File activeFile, final E event) {
    return timeBasedFileNamingAndTriggeringPolicy.isTriggeringEvent(activeFile, event);
}
这里默认借助的是DefaultTimeBasedFileNamingAndTriggeringPolicy对象来判定是否必要滚动
DefaultTimeBasedFileNamingAndTriggeringPolicy
被@NoAutoStart注解标识的LifeCycle对象不会在ImplicitModelHandler中注入时自动启动
@NoAutoStart
public class DefaultTimeBasedFileNamingAndTriggeringPolicy<E> extends TimeBasedFileNamingAndTriggeringPolicyBase<E> {
    public void start() {
      // 启动父类
      super.start();
      // 异常退出
      if (!super.isErrorFree()) {
            return;
      }
      // 默认按照时间混动的日志不支持%i的拆分
      if (tbrp.fileNamePattern.hasIntegerTokenCOnverter()) {
            addError("Filename pattern [" + tbrp.fileNamePattern
                  + "] contains an integer token converter, i.e. %i, INCOMPATIBLE with this configuration. Please remove it.");
            return;
      }
      // 实例化存档删除器
      archiveRemover = new TimeBasedArchiveRemover(tbrp.fileNamePattern, rc);
      archiveRemover.setContext(context);
      started = true;
    }
   
    public boolean isTriggeringEvent(File activeFile, final E event) {
      // 当前时间, 也可以由用户设置
      long currentTime = getCurrentTime();
      // 下一次检查的时间
      long localNextCheck = atomicNextCheck.get();
      // 当前已经到了检查时间
      if (currentTime >= localNextCheck) {
            // 根据当前时间和滚动单位计算下一次的检查时间, 滚动单位根据fileNamePattern标签配置的时间计算
            long nextCheck = computeNextCheck(currentTime);
            atomicNextCheck.set(nextCheck);
            // 当前文件的时间
            Instant instantOfElapsedPeriod = dateInCurrentPeriod;
            addInfo("Elapsed period: " + instantOfElapsedPeriod.toString());
            // 滚动时, 将当前时间转换成存档文件名,
            // 例如当前是2025-02-11, 配置的fileNamePattern为/Users/xxx/%d{yyyy-MM, aux, Asia/Shanghai}/roller_test.%d{yyyy-MM-dd}.log
            // 那么这里就是/Users/xxx/2025-02/roller_test.2025-02-11.log
            // 那么这个文件名就会保存当天所有的日志, 然后新建一个日志作为当前活动的日志文件
            this.elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convert(instantOfElapsedPeriod);
            // 设置下一次滚动的基础时间; 也就是接下来记录日志的时间
            setDateInCurrentPeriod(currentTime);
            return true;
      } else {
            return false;
      }
    }
}
DefaultTimeBasedFileNamingAndTriggeringPolicy继承自TimeBasedFileNamingAndTriggeringPolicyBase, 覆写了start方法, 并实现了isTriggeringEvent方法
start方法

[*]启动父类TimeBasedFileNamingAndTriggeringPolicyBase
[*]不允许fileNamePattern标签有%i标识, %i是给按照文件大小策略滚动用的
[*]实例化存档删除器
isTriggeringEvent
这个方法用来判定是否必要触发文件滚动的

[*]判定当前时间是否到了触发滚动的时间; 默认当前时间是体系的当前时间, 也可以利用标签设置; 默认滚动时间是程序启动时根据滚动周期盘算的下一个周期时间
[*]如果当前必要滚动, 那么盘算并设置下一次滚动的时间(根据当前时间和滚动周期盘算)
[*]盘算当前记载日记的文件滚动时归档的文件名
这里我们再来看一下父类的start方法
TimeBasedFileNamingAndTriggeringPolicyBase#start
public void start() {
    // 日期转换器(primary标识的, 也就是日志格式上没有AUX标识的)
    DateTokenConverter<Object> dtc = tbrp.fileNamePattern.getPrimaryDateTokenConverter();
    if (dtc == null) {
      throw new IllegalStateException(
                "FileNamePattern [" + tbrp.fileNamePattern.getPattern() + "] does not contain a valid DateToken");
    }
    // 日期转换器; %d{yyyy-MM, Asia/Shanghai}, 这种没有有aux的primary是true,时区就是Asia/Shanghai
    if (dtc.getZoneId() != null) {
      TimeZone tz = TimeZone.getTimeZone(dtc.getZoneId());
      rc = new RollingCalendar(dtc.getDatePattern(), tz, Locale.getDefault());
    } else {
      // dtc.getDatePattern(): primary的日期格式, 如: %d{yyyy-MM} 中的yyyy-MM
      // 并且会设置滚动周期类型
      rc = new RollingCalendar(dtc.getDatePattern());
    }
    addInfo("The date pattern is '" + dtc.getDatePattern() + "' from file name pattern '"
            + tbrp.fileNamePattern.getPattern() + "'.");
    // 打印滚动周期
    rc.printPeriodicity(this);
        // 判断滚动周期是否正确
    if (!rc.isCollisionFree()) {
      addError(
                "The date format in FileNamePattern will result in collisions in the names of archived log files.");
      addError(CoreConstants.MORE_INFO_PREFIX + COLLIDING_DATE_FORMAT_URL);
      withErrors();
      return;
    }

    long timestamp = getCurrentTime();
    // 设置当前活动的日志时间, 也就是日志记录的一个周期时间,会根据这个时间判断是否需要滚动, 以及滚动后的文件名
    setDateInCurrentPeriod(timestamp);
    // file标签; appender的file
    if (tbrp.getParentsRawFileProperty() != null) {
      File currentFile = new File(tbrp.getParentsRawFileProperty());
      // <file>标签配置的文件存在才会设置自定义的时间为当前周期的时间
      if (currentFile.exists() && currentFile.canRead()) {
            timestamp = currentFile.lastModified();
            // 文件的修改时间作为周期计算的起点
            setDateInCurrentPeriod(timestamp);
      }
    }
    addInfo("Setting initial period to " + dateInCurrentPeriod);
    // 根据滚动周期计算并设置下一个检查时间
    long nextCheck = computeNextCheck(timestamp);
    atomicNextCheck.set(nextCheck);
}
方法小结

[*]根据fileNamePattern标签设置的滚动周期实例化时间滚动器RollingCalendar
[*]设置当前时间为当前的日记周期
[*]如果当前记载日记的文件存在, 那么当文件的最后修改时间作为当前的日记周期
[*]根据日记周期时间盘算下一个周期时间
滚动rollover
public void rollover() throws RolloverFailure {

    // 当前日志周期归档文件名
    String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
    // 最后一个斜杆之后的内容, 也就是有后置的文件名
    String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);

    // fileNamePattern标签不是.zip或者.gz后缀
    if (compressionMode == CompressionMode.NONE) {
      // file属性不为空
      if (getParentsRawFileProperty() != null) {
            // 当前文件重命名为归档文件的名称
            renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
      }
    } else {
      // file属性为空
      if (getParentsRawFileProperty() == null) {
            compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName,
                  elapsedPeriodStem);
      } else {
            // 重命名并压缩
            compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
      }
    }
    // 清空归档文件
    if (archiveRemover != null) {
      Instant now = Instant.ofEpochMilli(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
      this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
    }
}
方法小结

[*]fileNamePattern标签不是.zip大概.gz后缀, 并且存在file标签内容, 那么将当前日记文件重定名为归档文件的名称
[*]如果fileNamePattern有压缩后缀, 并且file标签内容存在, 那么直接压缩
[*]如果fileNamePattern有压缩后缀, 并且file标签内容不存在, 会借助临时文件来压缩
[*]删除归档的过期文件
FileNamePattern

FileNamePattern作为<fileNamePattern>标签的解析类, 它可以将一个字符串按照不同的部弟子成一个转换链Converter, 然后可以通过这个转换链根据提供的参数更换占位符生成正确的字符串内容。下面大抵看一下它的内容。
public class FileNamePattern extends ContextAwareBase {
    /**
   * 允许转换的字符串与其对应的转换器
   */
    static final Map<String, Supplier<DynamicConverter>> CONVERTER_MAP = new HashMap<>();
    static {
      // 对i的转换;数字转字符串, 然后补齐最小长度的转换器
      CONVERTER_MAP.put(IntegerTokenConverter.CONVERTER_KEY, IntegerTokenConverter::new);
      // 对d的转换;日期转字符串, 然后补齐最小长度的转换器
      CONVERTER_MAP.put(DateTokenConverter.CONVERTER_KEY, DateTokenConverter::new);
    }

    /**
   * 需要转换成转换链的模板字符串
   */
    String pattern;
   
    /**
   * 根据pattern生成的转换链的头部节点
   */
    Converter<Object> headTokenConverter;
   
    public FileNamePattern(String patternArg, Context contextArg) {
      // the pattern is slashified
      setPattern(FileFilterUtil.slashify(patternArg));
      setContext(contextArg);
      // 解析fileNamePattern属性得到的转换链
      parse();
      // start converters的各个节点
      ConverterUtil.startConverters(this.headTokenConverter);
    }

    void parse() {
      try {
            // )转为\)
            String patternForParsing = escapeRightParantesis(pattern);
            // 实例化解析器并解析各个部分生成token
            Parser<Object> p = new Parser<Object>(patternForParsing, new AlmostAsIsEscapeUtil());
            p.setContext(context);
            // 解析token生成语法树
            Node t = p.parse();
            // 根据树形节点生成树形转换器
            this.headTokenConverter = p.compile(t, CONVERTER_MAP);
      } catch (ScanException sce) {
            addError("Failed to parse pattern \"" + pattern + "\".", sce);
      }
    }
}
FileNamePattern将一个表达式字符串pattern解析成转换器链headTokenConverter, 并且默认支持对%i和%d的解析, 这里%i是用来根据日记文件大小切割的序号, 是代表一个整型, %d就是日期格式, 下面罗列它两个比较重要的方法
// 将参数经过转换器处理后得到转换后的字符串
public String convertMultipleArguments(Object... objectList) {
    StringBuilder buf = new StringBuilder();
    Converter<Object> c = headTokenConverter;
    while (c != null) {
      if (c instanceof MonoTypedConverter) {
            // date和Integer的TokenConverter的转换器,需要判断是否是可转换的
            MonoTypedConverter monoTyped = (MonoTypedConverter) c;
            for (Object o : objectList) {
                if (monoTyped.isApplicable(o)) {
                  buf.append(c.convert(o));
                }
            }
      } else {
            buf.append(c.convert(objectList));
      }
      c = c.getNext();
    }
    return buf.toString();
}
// 将数字转为正则, 将date转为字符串, 然后生成字符串
// 可以做到 根据文件格式, 生成对应的正则表达式, 注意只对%i和%d两种类型做处理
// 例如: /Users/uncleqiao/logs/%d{yyyy-MM, aux, Asia/Shanghai}/roller_test.%d{yyyy-MM-dd}.log.gz
// 转成: /Users/uncleqiao/logs/\d{4}-\d{2}/roller_test.\d{4}-\d{2}-\d{2}.log.gz
public String toRegex() {
    StringBuilder buf = new StringBuilder();
    Converter<Object> p = headTokenConverter;
    while (p != null) {
      // 普通文本
      if (p instanceof LiteralConverter) {
            buf.append(p.convert(null));
      }
      // %i, 将数字格式转为正则表达式
      else if (p instanceof IntegerTokenConverter) {
            buf.append("\\d+");
      }
      // %d, 将日期格式转为正则表达式
      else if (p instanceof DateTokenConverter) {
            DateTokenConverter<Object> dtc = (DateTokenConverter<Object>) p;
            buf.append(dtc.toRegex());
      }
      p = p.getNext();
    }
    return buf.toString();
}
三、总结


[*]appender主要分为UnsynchronizedAppenderBase和AppenderBase两大类, 前一种代表了异步添加日记, 后一种代表了同步添加日记, 一样寻常我们利用的是UnsynchronizedAppenderBase, 效率更高, 同一个线程中日记还是有序的。
[*]appender可以通过<filter>标签设置打印日记前的过滤
[*]OutputStreamAppender范例的appender是支持添加encoder、OutputStream、immediateFlush、layout标签
[*]ConsoleAppender通过System.out打印日记到控制台
[*]FileAppender利用的是BufferedOutputStream将日记写入到文件中; FileOutputStream和FileChannel共享一个文件指针, 可以利用FileChannel移动指针, FileOutputStream读写文件
[*]RollingFileAppender支持按照时间(大概大小大概时间大小)滚动日记,


[*]它可以通过file指定当前活动的日记文件(当前写日记的文件)-非必须;
[*]可以通过rollingPolicy标签指定滚动策略,
[*]按时间滚动的话一样寻常是用TimeBasedRollingPolicy, 它既是滚动策略, 也是触发滚动策
[*]可以通过fileNamePattern标签设置归档文件格式, 默认利用的是DefaultTimeBasedFileNamingAndTriggeringPolicy作为触发策略
[*]可以通过maxHistory设置日记归档文件最多保存的天数

[*]fileNamePattern标签支持%i占位数字对按照文件大小分割的文件举行编号; %d对日期占位


[*]一个fileNamePattern标签中%d占位的时间格式有一个必须不被aux修饰, 没有被aux修饰的第一个时间将会定义为primary, 用来定义文件滚动周期的, 比方${log.path}/%d{yyyy-MM, aux, Asia/Shanghai}/roller_test.%d{yyyy-MM-dd}.log 这里的yyyy-MM-dd就是约定按天滚动

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 6.appender