第二部分用很简单的样例来描述了Logger对象的初始化,其实逻辑是不严谨的,而且运行时会有一些异常,这里我简单修正下,还是以RollingFileAppender为例,能够让其进行日志输出,如果有更加丰富的需求,那么请参考官方文档,或者自行百度下,我的博客仅做设计思路分享,不做教程:
private Logger getFileLogger()
{
// 初始化一个RollingFileAppender对象
RollingFileAppender appender = new RollingFileAppender();
// 设置Appender对象名,若不设置则运行时报错
appender.setName(filePath);
// 设置日志内容追加到文件内容末尾
appender.setAppend(true);
// 设置日志文件的存储位置
appender.setFile(filePath);
// 不开启异步模式
appender.setBufferedIO(false);
// 仅开启异步模式,缓存大小才有意义
appender.setBufferSize(0);
// 设置日志输出格式
appender.setLayout(new PatternLayout("%m%n"));
// 下面的方法是对上面四个属性设置的一个封装
// appender.setFile("", true, false, 0);
// 需要激活Appender对象的配置,这样属性设置才会生效
appender.activateOptions();
// 注意这里需要是指Logger对象名,后续设计会对此处进行重构,目前以调用类的SimpleName作为Logger对象的name属性值
Logger logger = Logger.getLogger(filePath);
// 为Logger对象添加Appender成员
logger.addAppender(appender);
// 设置Logger对象不继承上层节点属性配置,仅向文件中输出内容
logger.setAdditivity(false);
// 为什么设置日志输出级别为Trace,因为后续我们需要通过LogUtil公开的方法对日志级别进行动态控制,所以此处暂时设置为最低级别
logger.setLevel(Level.TRACE);
return logger;
}
第二部分已经通过代码配置的方式完成了Log4j的核心对象的初始化工作,按需求继续设计,则要考虑如何实现线程级的日志对象管理了。
这里有两个方向可以考虑(其他人有想法可以分享下):
这里选用第二种实现方式,首先对LogUtil进行重构改造,并且将Logger对象的实例化过程对应用隐藏,一应配置首先通过LogUtil的默认属性配置,并通过LogUtil提供的静态方法针对每一个线程进行重置,那么我们首先需要一个ThreadLogger类,对Logger对象进行配置隐藏。
创建ThreadLogger类,由LogUtil对其进行维护管理,ThreadLogger内部则维护一组Logger对象,供所有线程使用。
这里大家一定要对结构层次有所了解,不然设计到最后会懵逼的,并且慎之又慎的注意所有属性的访问级别,哪些属性应该是类级访问的,哪些是对象级可访问的。
package com.bubbling;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.RollingFileAppender;
import com.bubbling.LogUtil.LogLevel;
import com.bubbling.LogUtil.LogTarget;
/**
* 线程级日志对象,共享一组Logger对象对外提供日志输出功能
*
* @author 胡楠
*
*/
public class ThreadLogger
{
// ThreadLogger维护一组Logger对象,供所有线程使用,使用ConcurrentHashMap解决并发访问问题,其中map的key值为Logger对象名
private static ConcurrentHashMap<String, Logger> loggerMap = new ConcurrentHashMap<String, Logger>();
// 清空所有Logger对象,
public static void cleanThreadLogger()
{
loggerMap.clear();
}
private String filePath = "";
private LogTarget logTarget = LogTarget.File;
private LogLevel logLevel = LogLevel.Debug;
ThreadLogger()
{
}
ThreadLogger(String configFilePath)
{
// TODO 使用配置文件初始化ThreadLogger对象,文件解析过程自己写,这里仅作参考
}
public String getFilePath()
{
return filePath;
}
public void setFilePath(String path)
{
this.filePath = path;
}
public LogTarget getLogTarget()
{
return logTarget;
}
public void setLogTarget(LogTarget target)
{
if (logTarget != target)
{
this.logTarget = target;
Logger logger = loggerMap.get(filePath);
if (logger != null)
{
synchronized (logger)
{
getLogger();
}
}
}
}
public LogLevel getLogLevel()
{
return logLevel;
}
public void setLogLevel(LogLevel level)
{
this.logLevel = level;
}
public Logger getLogger()
{
Logger logger = loggerMap.get(filePath);
if (logger == null)
{
logger = initLogger();
loggerMap.put(filePath, logger);
}
return logger;
}
public void logTrace(String message)
{
if (logLevel.ordinal() <= LogLevel.Trace.ordinal())
{
getLogger().trace(message);
}
}
public void logDebug(String message)
{
if (logLevel.ordinal() <= LogLevel.Debug.ordinal())
{
getLogger().debug(message);
}
}
public void logInfo(String message)
{
if (logLevel.ordinal() <= LogLevel.Info.ordinal())
{
getLogger().info(message);
}
}
public void logWarn(String message)
{
if (logLevel.ordinal() <= LogLevel.Warn.ordinal())
{
getLogger().warn(message);
}
}
public void logError(String message)
{
if (logLevel.ordinal() <= LogLevel.Error.ordinal())
{
getLogger().error(message);
}
}
private Logger initLogger()
{
Logger logger = null;
if (LogTarget.Console == logTarget)
{
logger = getConsoleLogger();
}
else if (LogTarget.File == logTarget)
{
logger = getFileLogger();
}
else if (LogTarget.Socket == logTarget)
{
logger = getSocketLogger();
}
return logger;
}
private Logger getSocketLogger()
{
// TODO Auto-generated method stub
return null;
}
private Logger getConsoleLogger()
{
// TODO Auto-generated method stub
return null;
}
private Logger getFileLogger()
{
// 初始化一个RollingFileAppender对象
RollingFileAppender appender = new RollingFileAppender();
// 设置Appender对象名,若不设置则运行时报错
appender.setName(filePath);
// 设置日志内容追加到文件内容末尾
appender.setAppend(true);
// 设置日志文件的存储位置
appender.setFile(filePath);
// 不开启异步模式
appender.setBufferedIO(false);
// 仅开启异步模式,缓存大小才有意义
appender.setBufferSize(0);
// 设置日志输出格式
appender.setLayout(new PatternLayout("%m%n"));
// 下面的方法是对上面四个属性设置的一个封装
// appender.setFile("", true, false, 0);
// 需要激活Appender对象的配置,这样属性设置才会生效
appender.activateOptions();
// 注意这里需要是指Logger对象名,后续设计会对此处进行重构,目前以调用类的SimpleName作为Logger对象的name属性值
Logger logger = Logger.getLogger(filePath);
// 为Logger对象添加Appender成员
logger.addAppender(appender);
// 设置Logger对象不继承上层节点属性配置,仅向文件中输出内容
logger.setAdditivity(false);
// 为什么设置日志输出级别为Trace,因为后续我们需要通过LogUtil公开的方法对日志级别进行动态控制,所以此处暂时设置为最低级别
logger.setLevel(Level.TRACE);
return logger;
}
}
这是一个非常简单的封装,ThreadLogger维护了一组Logger对象,并且每个进程都对应一个ThreadLogger对象,仅提供了简单的日志文件路径及日志输出级别的控制,如果有其他的需要,那么读者可自行设计。
线程级的日志对象已经实现完毕,那么接下来就需要对LogUtil进行调整,LogUtil应该维护一组ThreadLogger对象,以应对每一个线程的日志输出需求,并且对外提供日志输出的方法:
package com.bubbling;
import java.util.concurrent.ConcurrentHashMap;
/**
* 1.实现代码方式配置Log4j
* 2.实现线程级日志对象管理
* 3.实现日志的异步输出模式
* 4.实现按日志文件大小及日期进行文件备份
*
* @author 胡楠
*
*/
public final class LogUtil
{
/**
* 阻止外部实例化LogUtil,LogUtil应该是一个仅提供日志配置及输出相关方法的工具类
*/
private LogUtil()
{
}
/**
* 使用枚举定义日志输出的目的地
*/
public enum LogTarget
{
Console, File, Socket;
}
/**
* 对应Log4j的日志输出级别,依然用枚举进行定义
*/
public enum LogLevel
{
Trace, Debug, Info, Warn, Error;
private int value;
public void setValue(int value)
{
this.value = value;
}
public int getValue()
{
return value;
}
}
/**
* 配置文件路径
*/
private static String LOGGER_CONFIG_FILE_PATH = null;
/**
* LogUtil维护一组ThreadLogger,这些ThreadLogger共享一组Logger对象
*/
private static ConcurrentHashMap<Long, ThreadLogger> LOGGER_MAP = new ConcurrentHashMap<Long, ThreadLogger>();
/**
* 提供设置配置文件的方法,以重置默认的配置,注意调用该方法后应该清除掉当前已实例化的所有Logger对象
*
* @param path
* 配置文件路径
*/
public static void setConfigFilePath(String path)
{
LOGGER_CONFIG_FILE_PATH = path;
reloadConfig();
}
/**
* 设置当前处理线程对应的日志输出对象的日志文件路径
*
* @param path
*/
public static void setFilePath(String path)
{
getThreadLogger().setFilePath(path);
}
/**
* 设置当前处理线程对应的日志输出对象的日志输出级别
*
* @param level
*/
public static void setLogLevel(LogLevel level)
{
getThreadLogger().setLogLevel(level);
}
public static void trace(String message)
{
log(LogLevel.Trace, message);
}
public static void debug(String message)
{
log(LogLevel.Debug, message);
}
public static void info(String message)
{
log(LogLevel.Info, message);
}
public static void warn(String message)
{
log(LogLevel.Warn, message);
}
public static void error(String message)
{
log(LogLevel.Error, message);
}
/**
* 重新装在配置,若配置文件路径不为空,则清空当前所有Logger对象,后续对象创建通过新的LoggerConfig来
*/
private static void reloadConfig()
{
if (LOGGER_CONFIG_FILE_PATH != null)
{
ThreadLogger.cleanThreadLogger();
}
}
private static ThreadLogger getThreadLogger()
{
long threadId = Thread.currentThread().getId();
ThreadLogger logger = LOGGER_MAP.get(threadId);
if (logger == null)
{
if (LOGGER_CONFIG_FILE_PATH == null)
{
logger = new ThreadLogger(LOGGER_CONFIG_FILE_PATH);
}
else
{
logger = new ThreadLogger();
}
}
LOGGER_MAP.put(threadId, logger);
return logger;
}
private static void log(LogLevel level, String message)
{
if (level == LogLevel.Trace)
{
getThreadLogger().logTrace(message);
}
else if (level == LogLevel.Debug)
{
getThreadLogger().logDebug(message);
}
else if (level == LogLevel.Info)
{
getThreadLogger().logInfo(message);
}
else if (level == LogLevel.Warn)
{
getThreadLogger().logWarn(message);
}
else if (level == LogLevel.Error)
{
getThreadLogger().logError(message);
}
}
}
可以看到,LogUtil提供了一个公开的,且针对线程设置的日志文件路径、日志输出级别的方法,并且提供了针对不同日志输出级别的静态方法,如此我们已经基本上实现了大部分需求,通过代码的方式对Log4j实现了配置,并且针对每一个应用线程实现了简单的属性控制,接下来我再提供一个简单的测试方法,供大家参考:
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(LogUtil.LogLevel.Error);
LogUtil.debug("测试线程:" + id + "debug输出");
LogUtil.info("测试线程:" + id + "info输出");
LogUtil.warn("测试线程:" + id + "warn输出");
LogUtil.error("测试线程:" + id + "error输出");
Thread thread = new Thread(new Runnable()
{
public void run()
{
long id = Thread.currentThread().getId();
LogUtil.setFilePath("D:\\测试\\" + id + ".log");
LogUtil.setLogLevel(LogLevel.Debug);
LogUtil.debug("测试线程:" + id + "debug输出");
LogUtil.info("测试线程:" + id + "info输出");
LogUtil.warn("测试线程:" + id + "warn输出");
LogUtil.error("测试线程:" + id + "error输出");
}
});
thread.start();
}
}
最后的输出结果是没有问题的,两个线程两个日志文件,其对应的文件路径也不相同,并且两个线程的日志输出级别不同,其输出的内容亦不相同。
到此为止,我们还剩下最后两个个需求,如何实现异步模式的日志输出,因为同步模式下,Logger频繁的通过IO来写入文件,这对磁盘的访问压力是非常大的,而且据我自己的测试,Log4j本身对异步模式的支持并不是特别好,仅提供了一定大小的缓存,缓存满了之后再行写入文件,虽然减少了IO访问次数,但是实际上的执行效率并没有很大的提升,这一点在Log4j2上改进的非常明显。
并且实现日志同时按日期和日志文件大小进行备份也是极为关键的,这涉及到对源码的延展,如何重写Appender,以实现定制化的需求。
我会在第四部分来单独说一下如何实现异步模式的日志输出,重点不在实现需求上,而在于给所有读者提供一个Java多线程并发操作相关的设计思路,很多新手对于线程、并发这些概念的理解比较模糊,更多的场景是上手之后写出的代码其质量较差,执行效率非常低,借此机会跟大家一起做一个相关方面的交流。