一、何为Appender?
appender将控制你的日志输出到什么地方?控制台or文件,高级一点的appender还可以控制你的日志文件是按时间滚动还是按文件大小滚动,
本篇文章将重点分析下 ch.qos.logback.core.rolling.RollingFileAppender
二、RollingFileAppender
2.1 类图
我们现在对上面的继承接口和实现做个简单的介绍
- LifeCycle:定义了start与stop方法,在使用某对象之前需要先start,关闭时使用stop,通常一些
初始化准备操作会写在这个start方法中,而释放资源,关闭流会写在stop方法中 - ContextAware:日志上下文aware,类似spring中的各种aware,比如ApplicationContextAware,继承该接口的类,会在使用前
注入日志上下文对象,除此之外还有一些addInfo,addWarn等添加状态的接口,在具体的实现中会记录到一个集合中,超过大小之后会
缓存在环形缓存中,另外还会除非状态监听器 - FilterAttachable:过滤器集,用来维护过滤器,比如 LevelFilter,用于控制日志是否允许输出
- Appender:输出日志的接口
- ContextAwareBase:ContextAware的基本实现
- UnsynchronizedAppenderBase:Appender的抽象模板,定义了基本的防止重入与调用filter决定是否输出日志的基本功能
- OutputStreamAppender:定义了重定向输出流与调用编码器处理日志的能力
- FileAppender:在start中确定文件,然后重定向输出流为文件
- RollingFileAppender:新增可以滚动文件日志的能力
下面我们会比较具体的去分析 UnsynchronizedAppenderBase -》RollingFileAppender的重要实现部分,首先我们来看UnsynchronizedAppenderBase
它的输出日志方法代码如下:
public void doAppend(E eventObject) {
// 检查当前线程是否已调用过该方法,防止重入
if (Boolean.TRUE.equals(guard.get())) {
return;
}
try {
// 标记当前线程已执行该方法
guard.set(Boolean.TRUE);
// 在使用当前Appender之前必须先调用strat方法
if (!this.started) {
// 允许提示警告的次数
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
}
return;
}
// 从filter集合中检查当前日志是否允许输出,比如我们常用的
// LevelFilter用于限制只能输出指定日志级别的日志还是大于该级别即可输出
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}
// 这时一个抽象方法,需要子类去实现
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);
可以看到 UnsynchronizedAppenderBase 实现了基本的过滤方法,具体怎么写出日志还需子类实现,那么我们继续来看看
OutputStreamAppender 的实现代码:
@Override
protected void append(E eventObject) {
if (!isStarted()) {
return;
}
subAppend(eventObject);
}
append方法只是检查了下当前Appender是否start,如果没有start将不允许执行,其主要逻辑实现在subAppend方法中:
protected void subAppend(E event) {
if (!isStarted()) {
return;
}
try {
// 此处会做一些日志消息格式化的操作,比如你的日志里有 {} 这个的大括号
// 那么这里将会替换成对应的参数
if (event instanceof DeferredProcessingAware) {
((DeferredProcessingAware) event).prepareForDeferredProcessing();
}
// 调用编码器对日志进行编码处理
byte[] byteArray = this.encoder.encode(event);
// 写出日志
writeBytes(byteArray);
} catch (IOException ioe) {
// as soon as an exception occurs, move to non-started state
// and add a single ErrorStatus to the SM.
this.started = false;
addStatus(new ErrorStatus("IO failure in appender", this, ioe));
}
}
可以看到上面的代码对我们传入的日志消息做了格式化并且调用了编码器进行编码,编码完成之后调用了如下方法:
private void writeBytes(byte[] byteArray) throws IOException {
if(byteArray == null || byteArray.length == 0)
return;
lock.lock();
try {
// 调用输出流输出日志
this.outputStream.write(byteArray);
// 是否立即刷盘
if (immediateFlush) {
this.outputStream.flush();
}
} finally {
lock.unlock();
}
}
写出日志的方法非常简单,调用输出流直接输出即可,到这里呢,其实数据已经刷出去了,至于输出到哪,那么跟你的outputStream
有关系了,你直接set你自己想要输出的流就可以,聪明的你肯定也能想到FileAppender做了什么了,
其实FileAppender就是把输出流修改成了输出到某个文件的FileOutputStream,它在start方法中做了以下操作:
public void openFile(String file_name) throws IOException {
lock.lock();
try {
File file = new File(file_name);
// 创建文件目录
boolean result = FileUtil.createMissingParentDirectories(file);
if (!result) {
addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
}
// 重定向输出流
ResilientFileOutputStream resilientFos = new ResilientFileOutputStream(file, append, bufferSize.getSize());
resilientFos.setContext(context);
setOutputStream(resilientFos);
} finally {
lock.unlock();
}
}
上面这个方法重定向了输出流,但是我们的日志是源源不断打印出去的,这样一个日志文件就会越来越大,为了避免这种情况我们需要搞些策略,比如
根据时间滚动,根据文件大小滚动,这个时候我们通常会配置一个叫 RollingFileAppender 的 Appender
@Override
protected void subAppend(E event) {
synchronized (triggeringPolicy) {
// 判断是否需要触发滚动操作
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
// 滚动
rollover();
}
}
super.subAppend(event);
}
如果我们是按照文件大小进行滚动日志,那么 triggeringPolicy.isTriggeringEvent 会判断当前打开的文件的大小,如果超过指定的大小,那么
会调用 rollover 进行日志滚动,会把日志进行重命名并对历史日志根据配置进行压缩操作,最后重定向输出流为最新的文件。回滚
其中 FixedWindowRollingPolicy 这种策略的操作如下:
public void ch.qos.logback.core.rolling.FixedWindowRollingPolicy#rollover() throws RolloverFailure {
// Inside this method it is guaranteed that the hereto active log file is
// closed.
// If maxIndex <= 0, then there is no file renaming to be done.
if (maxIndex >= 0) {
// 删除最大索引的日志文件,也就是最老的日志文件,比如我们配置滚动的最大日志文件数是5,那么文件滚动的下标到了5就会删除
File file = new File(fileNamePattern.convertInt(maxIndex));
if (file.exists()) {
file.delete();
}
// Map {(maxIndex - 1), ..., minIndex} to {maxIndex, ..., minIndex+1}
for (int i = maxIndex - 1; i >= minIndex; i--) {
// 重命名,比如 filename_%i => filename_1, filename_0...
String toRenameStr = fileNamePattern.convertInt(i);
File toRename = new File(toRenameStr);
// 如果存在那么就重命名
if (toRename.exists()) {
util.rename(toRenameStr, fileNamePattern.convertInt(i + 1));
} else {
addInfo("Skipping roll-over for inexistent file " + toRenameStr);
}
}
// move active file name to min
// 将历史文件进行压缩
switch (compressionMode) {
case NONE:
util.rename(getActiveFileName(), fileNamePattern.convertInt(minIndex));
break;
case GZ:
compressor.compress(getActiveFileName(), fileNamePattern.convertInt(minIndex), null);
break;
case ZIP:
compressor.compress(getActiveFileName(), fileNamePattern.convertInt(minIndex), zipEntryFileNamePattern.convert(new Date()));
break;
}
}
}
从上面的代码可以看到当日志文件的大小达到指定的大小之后会进行回滚,最新的文件下标越小,最后的文件将被删除
三、总结
我们选了logback中最常用的Appender进行分析,了解了一个日志是怎么输出的,但在这个日志输出之前我们还需要做些处理,比如根据用户配置的
格式进行输出,根据用户设置的编码进行编码,下面我们再来分析下Encoder的实现方式。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2022-04-02 4、JVM调优
2020-04-02 2、链表
2020-04-02 1、redis的简单动态字符串