聊一下 Java 的日志系列一

我们在使用 Java 的日志库的时候,比如我们现在项目在用的 logback,可以配置滚动策略,简单介绍下启动逻辑,这里我们定义的是
ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy 它是继承了 ch.qos.logback.core.rolling.TimeBasedRollingPolicy
它的启动方法就是下面这个 start 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void start() {
SizeAndTimeBasedFNATP<E> sizeAndTimeBasedFNATP = new SizeAndTimeBasedFNATP<E>(Usage.EMBEDDED);
if(maxFileSize == null) {
addError("maxFileSize property is mandatory.");
return;
} else {
addInfo("Archive files will be limited to ["+maxFileSize+"] each.");
}

sizeAndTimeBasedFNATP.setMaxFileSize(maxFileSize);
timeBasedFileNamingAndTriggeringPolicy = sizeAndTimeBasedFNATP;

if(!isUnboundedTotalSizeCap() && totalSizeCap.getSize() < maxFileSize.getSize()) {
addError("totalSizeCap of ["+totalSizeCap+"] is smaller than maxFileSize ["+maxFileSize+"] which is non-sensical");
return;
}

// most work is done by the parent
super.start();
}

配置了 timeBasedFileNamingAndTriggeringPolicy 策略

然后调用了父类的启动方法,主要看下父类的

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
48
49
50
51
52
53
54
55
56
57
public void start() {
// set the LR for our utility object
renameUtil.setContext(this.context);

// find out period from the filename pattern
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);
}

if (timeBasedFileNamingAndTriggeringPolicy == null) {
timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
}
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 != UNBOUND_HISTORY) {
archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
archiveRemover.setMaxHistory(maxHistory);
archiveRemover.setTotalSizeCap(totalSizeCap.getSize());
if (cleanHistoryOnStart) {
addInfo("Cleaning on start up");
Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
cleanUpFuture = archiveRemover.cleanAsynchronously(now);
}
} else if (!isUnboundedTotalSizeCap()) {
addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value ["+totalSizeCap+"]");
}

super.start();
}
  • 第一步是给 renameUtil 设置 context

  • 第二步是判断 fileNamePatternStr是否已配置,没配置会报错,如果配置了就会去判断压缩格式,是否要压缩以及压缩的是 gz 还是 zip

  • 第三步就是设置压缩器了

  • 第四步是判断文件后缀格式,注意是不带压缩格式的

  • 第五步是判断 timeBasedFileNamingAndTriggeringPolicy 是否已设置,这里是在子类里已经设置了 ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP 否则就是 ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy 这个默认的

  • 第六步比较重要就是启动 timeBasedFileNamingAndTriggeringPolicy

timeBasedFileNamingAndTriggeringPolicy 的启动逻辑里首先是调用了 SizeAndTimeBasedFNATP 的 start 方法,然后里面最开始调用了父类的 start

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
public void start() {
// we depend on certain fields having been initialized in super class
super.start();

if(usage == Usage.DIRECT) {
addWarn(CoreConstants.SIZE_AND_TIME_BASED_FNATP_IS_DEPRECATED);
addWarn("For more information see "+MANUAL_URL_PREFIX+"appenders.html#SizeAndTimeBasedRollingPolicy");
}

if (!super.isErrorFree())
return;


if (maxFileSize == null) {
addError("maxFileSize property is mandatory.");
withErrors();
}

if (!validateDateAndIntegerTokens()) {
withErrors();
return;
}

archiveRemover = createArchiveRemover();
archiveRemover.setContext(context);

// we need to get the correct value of currentPeriodsCounter.
// usually the value is 0, unless the appender or the application
// is stopped and restarted within the same period
String regex = tbrp.fileNamePattern.toRegexForFixedDate(dateInCurrentPeriod);
String stemRegex = FileFilterUtil.afterLastSlash(regex);

computeCurrentPeriodsHighestCounterValue(stemRegex);

if (isErrorFree()) {
started = true;
}
}

也就是下面的代码

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
public void start() {
DateTokenConverter<Object> dtc = tbrp.fileNamePattern.getPrimaryDateTokenConverter();
if (dtc == null) {
throw new IllegalStateException("FileNamePattern [" + tbrp.fileNamePattern.getPattern() + "] does not contain a valid DateToken");
}

if (dtc.getTimeZone() != null) {
rc = new RollingCalendar(dtc.getDatePattern(), dtc.getTimeZone(), Locale.getDefault());
} else {
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;
}

setDateInCurrentPeriod(new Date(getCurrentTime()));
if (tbrp.getParentsRawFileProperty() != null) {
File currentFile = new File(tbrp.getParentsRawFileProperty());
if (currentFile.exists() && currentFile.canRead()) {
setDateInCurrentPeriod(new Date(currentFile.lastModified()));
}
}
addInfo("Setting initial period to " + dateInCurrentPeriod);
computeNextCheck();
}

前面是日期时区等配置处理,然后是判断按日期生成的文件会不会冲突,接下去是设置当前时间段,如果配置了日志文件的话就会把当前时间段设置成已有的日志文件的最后更改时间,最后的 computeNextCheck 比较重要

主要是下面的方法

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
protected void computeNextCheck() {
nextCheck = rc.getNextTriggeringDate(dateInCurrentPeriod).getTime();
}
public Date getNextTriggeringDate(Date now) {
return getEndOfNextNthPeriod(now, 1);
}
public Date getEndOfNextNthPeriod(Date now, int periods) {
return innerGetEndOfNextNthPeriod(this, this.periodicityType, now, periods);
}
static private Date innerGetEndOfNextNthPeriod(Calendar cal, PeriodicityType periodicityType, Date now, int numPeriods) {
cal.setTime(now);
switch (periodicityType) {
case TOP_OF_MILLISECOND:
cal.add(Calendar.MILLISECOND, numPeriods);
break;

case TOP_OF_SECOND:
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.SECOND, numPeriods);
break;

case TOP_OF_MINUTE:
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.MINUTE, numPeriods);
break;

case TOP_OF_HOUR:
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.HOUR_OF_DAY, numPeriods);
break;

case TOP_OF_DAY:
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.DATE, numPeriods);
break;

case TOP_OF_WEEK:
cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.WEEK_OF_YEAR, numPeriods);
break;

case TOP_OF_MONTH:
cal.set(Calendar.DATE, 1);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.MONTH, numPeriods);
break;

default:
throw new IllegalStateException("Unknown periodicity type.");
}

return cal.getTime();

 会按照我们设置的 FileNamePattern 中的 datePattern 来推断 periodicityType,比如我们是小时滚动的,那就是 TOP_OF_HOUR ,算出来下一个时间检查点,后面会按这个判断是否作为触发事件触发日志滚动更新

父类的 start 逻辑讲完以后,子类的其实比较简单,先判断使用方式,我们这是嵌入式的,然后是父类有没有产生错误,继续是最大文件大小是否设置了,再判断日期格式是否正常,然后是归档移除类的创建,并设置到上下文中,然后计算当前时间的最大日志文件计数器,最后判断是否报错,没有的话就启动成功了