1. 程式人生 > >程式碼方式配置Log4j並實現執行緒級日誌管理 第三部分

程式碼方式配置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多執行緒併發操作相關的設計思路,很多新手對於執行緒、併發這些概念的理解比較模糊,更多的場景是上手之後寫出的程式碼其質量較差,執行效率非常低,藉此機會跟大家一起做一個相關方面的交流。