代码方式配置Log4j并实现线程级日志管理 第三部分

一 对第二部分的一些补充

  第二部分用很简单的样例来描述了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的核心对象的初始化工作,按需求继续设计,则要考虑如何实现线程级的日志对象管理了。

  这里有两个方向可以考虑(其他人有想法可以分享下):

  1. 每个处理线程分配一个Logger对象,并使用ThreadLocal进行管理,保证不会出现并发问题。但这里有一个比较难以解决的问题,如果线程数量过多,或者说应用的线程池管理并不严格的情况下,Logger对象可能会非常的多,而且没有行之有效的对象回收机制,那么是存在内存消耗剧烈的情况的,甚至内存溢出。
  2. 维护一组Logger对象,并且对所有线程提供Logger对象资源,以实现对象管理及资源的复用。但是需要注意,因为Logger对象组对于所有线程开放,那么解决并发问题显得尤为重要。

  这里选用第二种实现方式,首先对LogUtil进行重构改造,并且将Logger对象的实例化过程对应用隐藏,一应配置首先通过LogUtil的默认属性配置,并通过LogUtil提供的静态方法针对每一个线程进行重置,那么我们首先需要一个ThreadLogger类,对Logger对象进行配置隐藏。

三 实现ThreadLogger

  创建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进行调整,LogUtil应该维护一组ThreadLogger对象,以应对每一个线程的日志输出需求,并且对外提供日志输出的方法:

package com.bubbling;

import java.util.concurrent.ConcurrentHashMap;

/**
 * 1.实现代码方式配置Log4j <br>
 * 2.实现线程级日志对象管理 <br>
 * 3.实现日志的异步输出模式 <br>
 * 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多线程并发操作相关的设计思路,很多新手对于线程、并发这些概念的理解比较模糊,更多的场景是上手之后写出的代码其质量较差,执行效率非常低,借此机会跟大家一起做一个相关方面的交流。

猜你喜欢

转载自blog.csdn.net/o983950935/article/details/85013555