一 第三方程序不应仅限于会用
关于日志文件备份的问题,我跟很多人都沟通过,但是结果很遗憾,绝大部分人对于Log4j的理解仅限于常规的使用,网路上能搜索到的资源绝大部分也只是关于配置的介绍。
其实这样的现象并不少见,略微延展起来就变成了老生常谈的一个问题:对于开源框架的使用,会用即可?我记得刚毕业那会就跟同学争论过,时至今日我依然认为,会用是最起码的要求,而理解其实现的原理是必要的,因为我们无法保证在使用任何第三方程序时,不会出现问题或者特殊的需求,如果我们对于其三方程序的理解仅限于使用上,那么这些问题一旦出现——脑壳疼。
当下的需求就是一个例子,如果不清楚Log4j是如何进行文件备份的,那么何谈实现更为复杂的需求?这里我不再重复网络上满篇的底层实现逻辑,有兴趣的朋友自行百度,简而言之一句话:文件备份逻辑在Appender的rollOver()方法里。
二 同时按日期和文件大小备份
这是一个很常规的需求,然而Log4j对于这样的需求似乎很难支持,要么选择RollingFileAppender按文件大小进行备份,要么选择DailyRollingFileAppender按天进行文件备份。
前几部分我简单的介绍了按代码方式配置Log4j的思路,最后的一部分,让我们来实现这个需求,通过复写来实现日志文件同时按日期和文件大小进行备份。
实现思路是自己实现一个Appender,并重写rollOver()方法,考虑到按文件大小进行备份的实现,RollingFileAppender已经做到了,那么我们对RollingFileAppender进行一个简单的功能扩展即可。
package com.bubbling;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import org.apache.log4j.RollingFileAppender;
import org.apache.log4j.helpers.CountingQuietWriter;
import org.apache.log4j.spi.LoggingEvent;
/**
* 派生自RollingFileAppender,所有具备按文件大小备份的条件; 通过重写其rollOver()方法,补充按日期备份的逻辑,即可实现。
*
* @author 胡楠
*
*/
public class MyRollingFileAppender extends RollingFileAppender
{
/**
* 按日期备份时的文件名包含的日期样式
*/
public static final String strDatePattern = "yyyyMMdd";
/**
* 当前的日期,一旦日期发生变化,则触发对应的备份逻辑
*/
private String strDate = new SimpleDateFormat(strDatePattern).format(Calendar.getInstance().getTime());
/*
* 注意:别对这个方法理解有问题,这里仅仅是发生了文件备份后的处理逻辑,不是判断是否进行备份的逻辑
*/
public void rollOver()
{
File target;
File file;
// 如果设置了最大文件备份数量(每日的哟),那么执行以下逻辑
if (maxBackupIndex > 0)
{
file = new File(getBackupFileName(maxBackupIndex));
if (file.exists())
{
file.delete();
}
// 这里的逻辑略绕,从当前已备份到的最大序号开始,倒着遍历,看不懂的跟踪调试下就好
for (int i = maxBackupIndex - 1; i >= 1; i--)
{
file = new File(getBackupFileName(i));
if (file.exists())
{
target = new File(getBackupFileName(i + 1));
file.renameTo(target);
}
}
target = new File(getBackupFileName(1));
this.closeFile();
file = new File(fileName);
file.renameTo(target);
}
// 如果没有设置过最大文件备份数量,则给他21个亿
else
{
for (int i = 1; i < Integer.MAX_VALUE; i++)
{
target = new File(getBackupFileName(i));
if (!target.exists())
{
this.closeFile();
file = new File(fileName);
file.renameTo(target);
break;
}
}
}
// 以上仅处理了按文件大小备份,下面处理日期发生变化后的处理
try
{
String strFile = this.getFile();
if (!(strDate.equals(strFile.substring(strFile.indexOf("-") + 1, strFile.lastIndexOf(".log")))))
{
StringBuilder builder = new StringBuilder(strFile.substring(0, strFile.lastIndexOf("-") + 1));
builder.append(strDate);
builder.append(".log");
this.fileName = builder.toString();
}
this.setFile(fileName, false, bufferedIO, bufferSize);
}
catch (IOException e)
{
e.printStackTrace();
}
}
/*
* 注意:这里才是判断是否备份的逻辑,依然重写,使日期变更也触发rollOver()方法
*/
protected void subAppend(LoggingEvent event)
{
String strCurrentDate = new SimpleDateFormat(strDatePattern).format(Calendar.getInstance().getTime());
// 满足以下所有条件才进行文件备份
if (fileName != null && ((CountingQuietWriter) qw).getCount() >= maxFileSize || !strDate.equals(strCurrentDate))
{
// 如果是因为日期变动导致的备份,需要调整当前时间
if (!strDate.equals(strCurrentDate))
{
strDate = strCurrentDate;
}
this.rollOver();
}
super.subAppend(event);
}
/**
* 此方法用于自行改写备份文件名,我喜欢***yyyyMMdd-00001.log这样的格式,看着舒服
*
* @param maxBackupIndex
* @return
*/
private String getBackupFileName(int maxBackupIndex)
{
StringBuilder builder = new StringBuilder();
builder.append(fileName.substring(0, fileName.indexOf(".log")));
builder.append("-");
builder.append(String.format("%05d", maxBackupIndex));
builder.append(".log");
return builder.toString();
}
}
三 调整ThreadLogger使用复写的Appender
实现了自定义的Appender之后,我们把ThreadLogger中创建Logger对象的方法重构下,不再使用原RollingFileAppender对象,而且因为加入了文件大小和文件备份数量等参数,LogUtil也需要调整。
public class ThreadLogger
{
……
private String filePath = "";
private LogTarget logTarget = LogTarget.File;
private LogLevel logLevel = LogLevel.Debug;
// 补充新的属性,文件大小
private String maxFileSize = "1MB";
// 补充新的属性,文件备份数量
private int maxBackupIndex = 10;
……
private Logger getFileLogger()
{
// 初始化一个RollingFileAppender对象,此处调整为自定义的Appender
MyRollingFileAppender appender = new MyRollingFileAppender();
// 设置文件备份数量
appender.setMaxBackupIndex(maxBackupIndex);
// 设置文件大小
appender.setMaxFileSize(maxFileSize);
// 设置Appender对象名,若不设置则运行时报错
appender.setName(filePath);
……
}
……
}
四 简单测试下
最后简单验证下,因为没有仔细的完善所有执行逻辑,仅仅为了举个例子,所以执行结果大致符合预期就算是大功告成,各位看官有兴趣可以自行对逻辑进行处理。
package com.bubbling;
import com.bubbling.LogUtil.LogLevel;
public class LogUtilTest
{
public static void main(String[] args)
{
long id = Thread.currentThread().getId();
LogUtil.setFilePath("D:\\测试\\" + id + ".log");
LogUtil.setLogLevel(LogLevel.Debug);
for (int i = 0; i < 5000000; i++)
{
LogUtil.debug("测试线程:" + id + "debug输出");
LogUtil.info("测试线程:" + id + "info输出");
LogUtil.warn("测试线程:" + id + "warn输出");
LogUtil.error("测试线程:" + id + "error输出");
}
}
}
五 结语
从一开始的需求,到逐步的实现,这其中并没有什么特别高深莫测的东西,我始终主张能能最简单的实现,来满足当下的需求即可,直到对性能有了更高的要求,再去仔细分析优化空间。
但这并不是说下手写代码的时候不需要考虑性能问题,严谨的逻辑(一般就好)是每一个程序员的基础,查阅源码、文档及跟踪调试是基本功。
不要总是想着复制粘贴,如果你对技能提升有渴望,请多花些心思去思考设计方案,而不是急着拷贝别人的代码。
程序员应该多花时间在设计上,实现上消耗的时间应该是最少的,代码的编写也不是一步到位的,随着实现的深入,逐步重构,完善代码层级结构,设计模式也就渐渐显现出来,合理的设计也对后期的维护提供了坚实的基础。虽说道理如此简单,但是真的能沉下心来这样做的,怕是十不足一。