OK,上一篇博客咱們已經實現了日誌框架的基本的功能,可是還有一個最大的問題就是日誌輸出地不能重定向,而後一些輸出也不可控。那如今咱們來實現一個比較完整的日誌框架。html
設計思路以下:java
1,定義一堆常量LinkinLog4jConstants,這些常量用於框架中日誌輸出的配置項,爲了簡單和方便,這裏不必定非要依賴配置文件,因此在這些常量裏面都要付初始值。設計模式
2,新增配置文件,LinkinLog4j.propertites。這些配置文件中能夠配置上面常量項的任何一項用來覆蓋框架默認。app
3,框架核心類,LinkinLog4j新增shortName,ThreadName屬性,用於日誌輸出類名簡稱,線程名字。框架
4,LinkinLog4j定義幾個新的靜態常量,在static塊中統一初始化。比較重要的是:要有一個輸出流,用來重定向Appender,要有一個日誌+等級映射的map,用來控制各類子類的等級jvm
5,重寫log(),按照配置文件中的各類配置來控制日誌某些內容的輸出與不輸出。ide
6,框架輔助類,LinkinLog4jHelper。框架日誌工廠類,LinkinLog4jFactory,用來生成單例LinkinLog4j。工具
OK,如今我先貼出代碼,而後一邊本身寫個測試來試一下這個框架看下效果。我自測經過。性能
框架核心類,LinkinLog4j:測試
package linkinframe.linkinLog4j; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.TreeMap; public class LinkinLog4j { private static final String PROP_LOG_PREFIX; // 配置文件key值前綴 private static boolean SHOW_LOG_NAME; // 是否輸出類名全名旗標 private static boolean SHOW_SHORT_NAME; // 是否輸出類名簡稱旗標 private static boolean SHOW_THREAD_NAME; // 是否輸出線程名旗標 private static boolean SHOW_DATE_INFO; // 是否輸出時間旗標 private static boolean FLUSH; // 是否刷新輸出流旗標 private static final Map<String, LinkinLogLevel> LEVELDESCMAP; // 全部等級枚舉 key值數字 private static final Map<Integer, LinkinLogLevel> LEVELSTATUSMAP; // 全部等級枚舉 key值英語 private static Map<String, Integer> CONFIGLEVELS; // 封裝配置文件中的每一個類的日誌級別 private static SimpleDateFormat DATEFORMAT; // 時間格式化器 private static int ROOT_LEVEL; // 默認等級,若是沒有配置就使用該等級 private static PrintStream OUT; // 輸出流 static { LEVELDESCMAP = LinkinLogLevel.getLevelDescMap(); LEVELSTATUSMAP = LinkinLogLevel.getLevelStatusMap(); InputStream in = LinkinLog4jHelper.getConfigInputStream(LinkinLog4jConstants.PROP_NAME); Properties props = new Properties(); if (in != null) { try { props.load(in); in.close(); } catch (IOException e) { } } // 添加系統的全部的常量 props.putAll(System.getProperties()); PROP_LOG_PREFIX = LinkinLog4jConstants.PROP_LOG_PREFIX; SHOW_LOG_NAME = LinkinLog4jHelper.getBooleanProperty(props, LinkinLog4jConstants.SHOW_LOG_NAME, LinkinLog4jConstants.DEFAULT_SHOW_LOG_NAME); SHOW_SHORT_NAME = LinkinLog4jHelper.getBooleanProperty(props, LinkinLog4jConstants.SHOW_SHORT_NAME, LinkinLog4jConstants.DEFAULT_SHOW_SHORT_NAME); SHOW_THREAD_NAME = LinkinLog4jHelper.getBooleanProperty(props, LinkinLog4jConstants.SHOW_THREAD_NAME, LinkinLog4jConstants.DEFAULT_SHOW_THREAD_NAME); SHOW_DATE_INFO = LinkinLog4jHelper.getBooleanProperty(props, LinkinLog4jConstants.SHOW_DATE_INFO, LinkinLog4jConstants.DEFAULT_SHOW_DATE_INFO); FLUSH = LinkinLog4jHelper.getBooleanProperty(props, LinkinLog4jConstants.FLUSH, LinkinLog4jConstants.DEFAULT_FLUSH); ROOT_LEVEL = toIntegerLevel(LinkinLog4jHelper.getProperty(props, LinkinLog4jConstants.ROOT_LEVEL, LinkinLog4jConstants.DEFAULT_LEVEL)); // 初始化配置文件中的全部的日誌文件+等級 CONFIGLEVELS = parseConfigLevels(props); // 初始化時間格式。 String dateFormatStr = LinkinLog4jHelper.getProperty(props, LinkinLog4jConstants.DATE_FORMAT, LinkinLog4jConstants.DEFAULT_DATA_FORMAT); DATEFORMAT = new SimpleDateFormat(dateFormatStr); // 初始化輸出流,若是沒有找見則重定向爲控制檯輸出 String logFile = LinkinLog4jHelper.getProperty(props, LinkinLog4jConstants.LOG_FILE, LinkinLog4jConstants.DEFAULT_LOG_FILE); boolean append = LinkinLog4jHelper.getBooleanProperty(props, LinkinLog4jConstants.LOG_FILE_APPEND, LinkinLog4jConstants.DEFAULT_LOG_FILE_APPEND); OUT = getPrintStream(logFile, append); // jvm中增長一個關閉的鉤子 Runtime runtime = Runtime.getRuntime(); runtime.addShutdownHook(new Thread() { @Override public void run() { try { shutdown(); } catch (Exception e) { System.err.println("框架shutdown出錯!!!"); e.printStackTrace(System.err); } } private void shutdown() { } }); } private final String name; // 每一個日誌文件的名稱 private String shortName; // 日誌文件簡稱 private String threadName; // 線程名字 private final int level; // 每一個日誌文件的等級 public LinkinLog4j(String name) { this.name = name; this.level = getLogLevel(name); } /** * @建立時間: 2016年2月24日 * @相關參數: @param props * @相關參數: @return * @功能描述: 封裝日誌名+日誌等級進一個map */ private static Map<String, Integer> parseConfigLevels(Properties props) { Map<String, Integer> map = new TreeMap<String, Integer>(); for (String key : props.stringPropertyNames()) { if (key != null && key.startsWith(PROP_LOG_PREFIX)) { String logLevelValue = props.getProperty(key); String logName = parseLogName(key); map.put(logName, toIntegerLevel(logLevelValue)); } } return map; } /** * @建立時間: 2016年2月24日 * @相關參數: @param logNameKey * @相關參數: @return * @功能描述: 獲取日誌名稱,配置文件中去掉配置前綴 */ private static String parseLogName(String logNameKey) { return logNameKey.substring(PROP_LOG_PREFIX.length()); } /** * @建立時間: 2016年2月24日 * @相關參數: @param property * @相關參數: @return * @功能描述: 經過desc獲取等級枚舉中的status */ private static int toIntegerLevel(String desc) { return LEVELDESCMAP.get(desc).getStatus(); } /** * @建立時間: 2016年2月24日 * @相關參數: @param logFile * @相關參數: @param append * @相關參數: @return * @功能描述: 獲取輸出地appender */ private static PrintStream getPrintStream(String logFile, boolean append) { PrintStream out = null; try { LinkinLog4jHelper.createNewFileIfNotExists(logFile); out = new PrintStream(new FileOutputStream(logFile, append)); } catch (Exception e) { System.err.println("未找到輸出日誌路徑,默認使用控制檯輸出!"); return System.out; } return out; } /** * @建立時間: 2016年2月24日 * @相關參數: @param logName * @相關參數: @return * @功能描述: 獲取日誌類的輸出日誌等級 */ private int getLogLevel(String logName) { // 一、若是沒有配置,使用框架默認等級 if (CONFIGLEVELS == null || CONFIGLEVELS.isEmpty()) { return ROOT_LEVEL; } // 二、若是配置了,使用配置文件中的等級 int logLevel = -1; // {level=10000, level.test.junit4test=2147483647} // test.junit4test.LinkinLog4jTest for (String name : CONFIGLEVELS.keySet()) { if (logName.startsWith(name)) { logLevel = CONFIGLEVELS.get(name); } } if (logLevel == -1) { logLevel = ROOT_LEVEL; } return logLevel; } /***************** 定義一系列輸出日誌的方法 ***********************************/ public void info(String message) { info(message, null); } public void info(String message, Throwable cause) { log(LinkinLogLevel.INFO.getStatus(), message, cause); } public void debug(String message) { debug(message, null); } public void debug(Throwable cause) { debug(null, cause); } public void debug(String message, Throwable cause) { log(LinkinLogLevel.DEBUG.getStatus(), message, cause); } public void error(String message) { error(message, null); } public void error(String message, Throwable cause) { log(LinkinLogLevel.ERROR.getStatus(), message, cause); } /** * @建立時間: 2016年2月24日 * @相關參數: @param level * @相關參數: @param message * @相關參數: @param cause * @功能描述: 核心日誌方法,輸出日誌內容到appender * <p> * 判斷日誌類定義的日誌級別,控制一些日誌方法的執行和不執行 * 依次將日誌的信息一步一步的添加到StringBuilder中而後輸出 * </p> */ private void log(int level, String message, Throwable cause) { if (isLevelEnabled(level)) { return; } StringBuilder builder = new StringBuilder(128); appendDateInfo2Log(builder); appendLogName2Log(builder, name); appendThreadName2Log(builder); appendLevel2Log(builder, level); appendMessqge2Log(builder, message); appendCauseInfo2Log(builder, cause); writeLog(builder); } /** * @建立時間: 2016年2月24日 * @相關參數: @param level 日誌類中調用的各類輸出日誌方法的等級 * @相關參數: @return true:忽略該輸出日誌方法,false:執行該輸出日誌方法 * @功能描述: 控制一些日誌的輸出仍是忽略 * <p> * 日誌類本身配置的日誌等級VS日誌類中調用的各類輸出日誌方法的等級 * </p> */ private boolean isLevelEnabled(int level) { if (level < this.level) { return true; } return false; } /** * @建立時間: 2016年2月24日 * @相關參數: @param builder * @相關參數: @param level * @功能描述: 追加日誌等級 */ private void appendLevel2Log(StringBuilder builder, int level) { builder.append("[").append(LEVELSTATUSMAP.get(level).getDesc()).append("]").append(" "); } /** * @建立時間: 2016年2月24日 * @相關參數: @param builder * @功能描述: 追加時間信息,取值當前時間 * 注意:這裏要加鎖 */ private void appendDateInfo2Log(StringBuilder builder) { if (SHOW_DATE_INFO) { Date date = new Date(); String dateStr = ""; synchronized (DATEFORMAT) { dateStr = DATEFORMAT.format(date); } builder.append(dateStr).append(LinkinLog4jConstants.LOG_SEPARATOR); } } /** * @建立時間: 2016年2月24日 * @相關參數: @param builder * @相關參數: @param name * @功能描述: 追加日誌名字 */ private void appendLogName2Log(StringBuilder builder, String name) { if (SHOW_SHORT_NAME) { builder.append(getShortName(name)); } else if (SHOW_LOG_NAME) { builder.append(name); } builder.append(LinkinLog4jConstants.LOG_SEPARATOR); } private void appendThreadName2Log(StringBuilder builder) { if (SHOW_THREAD_NAME) { builder.append(getThreadName()); } builder.append(LinkinLog4jConstants.LOG_SEPARATOR); } /** * @建立時間: 2016年2月24日 * @相關參數: @param name * @相關參數: @return * @功能描述: 根據類名獲取簡稱 * <p> * linkin.package.className→className * </p> */ protected Object getShortName(String name) { if (shortName == null) { if (name == null) { shortName = "null"; return shortName; } int idx = name.lastIndexOf("."); if (idx < 0) { shortName = name; } else { shortName = name.substring(idx + 1); } } return shortName; } /** * @建立時間: 2016年2月24日 * @相關參數: @param builder * @相關參數: @param message * @功能描述: 追加日誌內容信息 */ private void appendMessqge2Log(StringBuilder builder, String message) { builder.append(message); } /** * @建立時間: 2016年2月24日 * @相關參數: @param builder * @相關參數: @param cause * @功能描述: 追加日誌異常 */ private void appendCauseInfo2Log(StringBuilder builder, Throwable cause) { if (Objects.nonNull(cause)) { builder.append("<"); builder.append(cause.getMessage()); builder.append(">"); builder.append(System.getProperty("line.separator")); StringWriter writer = new StringWriter(); PrintWriter printer = new PrintWriter(writer); cause.printStackTrace(printer); printer.close(); builder.append(writer.toString()); } } /** * @建立時間: 2016年2月24日 * @相關參數: @param str 全部的日誌輸出的內容 * @功能描述: 輸出日誌 * 注意:要加鎖,別打印一半被打斷 */ public synchronized void writeLog(StringBuilder str) { OUT.println(str.toString()); if (FLUSH) { OUT.flush(); } } /** * @建立時間: 2016年2月24日 * @相關參數: @return * @功能描述: 獲取當前線程 */ public synchronized String getThreadName() { if (threadName == null) { threadName = Thread.currentThread().getName(); } return threadName; } }
package linkinframe.linkinLog4j; /** * @建立做者: LinkinPark * @建立時間: 2016年2月24日 * @功能描述: 框架的默認值,配置文件中相關配置覆蓋這份默認值 */ public class LinkinLog4jConstants { // 配置文件名稱 public static final String PROP_NAME = "linkinLog4j.properties"; // 配置文件key值前綴 public static final String PROP_PREFIX = "linkinLog4j."; // 配置日誌文件名字+等級的映射 public static final String PROP_LOG_PREFIX = PROP_PREFIX + "log.level."; // 默認時間格式化器 public static final String DATE_FORMAT = PROP_PREFIX + "dateFormat"; public static final String DEFAULT_DATA_FORMAT = "HH:mm:ss"; // 默認日誌級別 public static final String ROOT_LEVEL = PROP_PREFIX + "root.level"; public static final String DEFAULT_LEVEL = LinkinLogLevel.ALL.getDesc(); // 默認顯示日誌日誌所在類名簡稱 public static final String SHOW_LOG_NAME = PROP_PREFIX + "showLogName"; public static boolean DEFAULT_SHOW_LOG_NAME = true; // 默認顯示線程 public static final String SHOW_THREAD_NAME = PROP_PREFIX + "showThreadName"; public static boolean DEFAULT_SHOW_THREAD_NAME = true; // 默認不顯示日誌所在類名全稱 public static final String SHOW_SHORT_NAME = PROP_PREFIX + "showShortName"; public static boolean DEFAULT_SHOW_SHORT_NAME = false; // 默認不顯示時間 public static final String SHOW_DATE_INFO = PROP_PREFIX + "showDateInfo"; public static boolean DEFAULT_SHOW_DATE_INFO = true; // 重定向日誌輸出路徑 public static final String LOG_FILE = PROP_PREFIX + "logFile"; public static final String DEFAULT_LOG_FILE = null; // 默認日誌刷緩衝 public static final String FLUSH = PROP_PREFIX + "logFile.flush"; public static boolean DEFAULT_FLUSH = true; // 是否追加日誌到一個文件尾部 public static final String LOG_FILE_APPEND = PROP_PREFIX + "logFile.append"; public static final boolean DEFAULT_LOG_FILE_APPEND = true; // 日誌的分割符 public static final String LOG_SEPARATOR = "-"; }
package linkinframe.linkinLog4j; import java.util.HashMap; import java.util.Map; /** * @建立做者: LinkinPark * @建立時間: 2016年2月23日 * @功能描述: 日誌等級枚舉。 * <p> * Log4J中的全部的等級以下:all→trace→debug→info→warn→error→fatal→off * 這裏本身模擬的等級以下:all→debug→info→error→off * </p> */ public enum LinkinLogLevel { ALL(Integer.MIN_VALUE, "ALL"), DEBUG(10000, "DEBUG"), INFO(20000, "INFO"), ERROR(30000, "ERROR"), OFF(Integer.MAX_VALUE, "OFF"); private final int status; private final String desc; private LinkinLogLevel(int status, String desc) { this.status = status; this.desc = desc; } public int getStatus() { return status; } public String getDesc() { return desc; } /*************** 提供2個map,分別封裝全部的枚舉 ***************/ public static Map<String, LinkinLogLevel> getLevelDescMap() { Map<String, LinkinLogLevel> levelMap = new HashMap<>(5, 1); LinkinLogLevel[] values = LinkinLogLevel.values(); for (LinkinLogLevel linkinLogLevel : values) { levelMap.put(linkinLogLevel.getDesc(), linkinLogLevel); } return levelMap; } public static Map<Integer, LinkinLogLevel> getLevelStatusMap() { Map<Integer, LinkinLogLevel> levelMap = new HashMap<>(5, 1); LinkinLogLevel[] values = LinkinLogLevel.values(); for (LinkinLogLevel linkinLogLevel : values) { levelMap.put(linkinLogLevel.getStatus(), linkinLogLevel); } return levelMap; } }
package linkinframe.linkinLog4j; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Properties; /** * @建立做者: LinkinPark * @建立時間: 2016年2月24日 * @功能描述: 工具類 */ public class LinkinLog4jHelper { public static String getProperty(Properties props, String key) { return getProperty(props, key, ""); } public static String getProperty(Properties props, String key, String defaultValue) { return props.getProperty(key, defaultValue); } public static boolean getBooleanProperty(Properties props, String key) { return getBooleanProperty(props, key, false); } public static boolean getBooleanProperty(Properties props, String key, boolean defaultValue) { String property = props.getProperty(key); if (property == null || property == "" || property == "null") { return defaultValue; } return new Boolean(property).booleanValue(); } public static InputStream getConfigInputStream(String configName) { ClassLoader classLoader = getContextClassLoader(); InputStream in = classLoader.getResourceAsStream(configName); if (in == null) { in = LinkinLog4j.class.getClassLoader().getResourceAsStream(configName); } if (in == null) { in = LinkinLog4j.class.getResourceAsStream(configName); } return in; } public static ClassLoader getContextClassLoader() { return Thread.currentThread().getContextClassLoader(); } /** * @建立時間: 2016年2月25日 * @相關參數: @param logFile * @功能描述: 文件不存在,則建立一個 */ public static void createNewFileIfNotExists(String logFile) { File file = new File(logFile); File file1 = new File(file.getParent()); if (!file.exists()) { file1.mkdirs(); try { file.createNewFile(); } catch (IOException e) { System.err.println("建立日誌出錯!!!"); } } } }
package linkinframe.linkinLog4j; import java.util.Hashtable; public class LinkinLog4jFactory { // 字符串包裝key值,加快hashTable查找效率 static Hashtable<CategoryKey, Object> hashTable = new Hashtable<>(10, 1); public static LinkinLog4j getLogger(Class<?> klass) { return getLogger(klass.getName()); } public static LinkinLog4j getLogger(String name) { CategoryKey key = new CategoryKey(name); LinkinLog4j logger; synchronized (hashTable) { Object o = hashTable.get(key); if (o == null) { logger = makeNewLoggerInstance(name); hashTable.put(key, logger); return logger; } else if (o instanceof LinkinLog4j) { return (LinkinLog4j) o; } else { return null; } } } public static LinkinLog4j makeNewLoggerInstance(String name) { return new LinkinLog4j(name); } }
package linkinframe.linkinLog4j; /** * @建立做者: LinkinPark * @建立時間: 2016年2月23日 * @功能描述: 字符串包裝,加快hashTable查找效率 */ public class CategoryKey { String name; int hashCache; CategoryKey(String name) { this.name = name; hashCache = name.hashCode(); } final public int hashCode() { return hashCache; } final public boolean equals(Object rArg) { if (this == rArg) { return true; } if (rArg != null && CategoryKey.class == rArg.getClass()) { return name.equals(((CategoryKey) rArg).name); } else { return false; } } }
################################# #該配置文件可省,框架默認向控制檯輸出日誌# ##[ALL→DEBUG→INFO→ERROR→OFF]### ################################# #是否輸出類的簡稱,只有一個類名。默認false linkinLog4j.showShortName=false #是否輸出線程名字,默認true #linkinLog4j.showThreadName=false #是否輸出時間信息,取值當前時間,默認true linkinLog4j.showDateInfo=true #時間格式化器,默認HH:mm:ss linkinLog4j.dateFormat=yyyy-MM-dd HH:mm:ss #配置全局日誌級別,默認ALL linkinLog4j.root.level=DEBUG #配置日誌重定向文件appender,默認控制檯 linkinLog4j.logFile=huhu/log/linkinLog4j.log #是否追加日誌到文件尾部,默認true linkinLog4j.logFile.append=false #是否刷新日誌在輸出流中的緩衝,默認true linkinLog4j.logFile.flush=true ################################# #支持配置包,配置類,覆蓋root默認,覆蓋包# ################################# #linkinLog4j.log.level.test.junit4test=DEBUG linkinLog4j.log.level.test.junit4test.LinkinLog4jTest1=INFO ################################# #TODO:多個線程在日誌中線程名字同樣,我去 #################################
1,不用配置文件,直接使用框架默認打印日誌。直接修改常量,使得框架找不到該配置文件就OK。
測試類代碼以下:
package test.junit4test; import org.junit.Test; import linkinframe.linkinLog4j.LinkinLog4j; import linkinframe.linkinLog4j.LinkinLog4jFactory; public class LinkinLog4jTest { LinkinLog4j log = LinkinLog4jFactory.getLogger(LinkinLog4jTest.class); @Test public void testLog() { log.debug("debug()。。。"); log.info("info()。。。"); log.error("error()。。。"); } }OK,沒有問題。junit控制檯綠條,而後控制檯輸出結果以下:
未找到輸出日誌路徑,默認使用控制檯輸出! 10:08:41-test.junit4test.LinkinLog4jTest-main-[DEBUG] debug()。。。 10:08:41-test.junit4test.LinkinLog4jTest-main-[INFO] info()。。。 10:08:41-test.junit4test.LinkinLog4jTest-main-[ERROR] error()。。。
OK,沒有問題,junit綠條,而後日誌內容被重定向了咱們制定的路徑文件下。
3,從新設置子類文件的日誌級別,可使用包配置,也可使用類名配置。咱們如今修改linkinLog4j.propertites文件,定義日誌等級:
################################# #支持配置包,配置類,覆蓋root默認,覆蓋包# ################################# #linkinLog4j.log.level.test.junit4test=DEBUG linkinLog4j.log.level.test.junit4test.LinkinLog4jTest=INFO
2016-02-25 10:12:56-test.junit4test.LinkinLog4jTest-main-[INFO] info()。。。 2016-02-25 10:12:56-test.junit4test.LinkinLog4jTest-main-[ERROR] error()。。。
總結:
雖然日誌功能在應用程序開發中是一個很是重要的部件,有些時候日誌信息的好壞能夠直接影響程序開發的進度。
可是日誌自己不涉及到任何業務邏輯,於是須要儘可能減小它的侵入性,也就說它提供的接口應該儘可能的簡單。爲了實現接口的簡單性,其中一種方法就是使用配置文件記錄LinkinLog4j的配置信息,LinkinLog4j則根據配置信息初始化每個日誌核心實例。
這些配置信息包括:
1,是否顯示日誌名稱、時間信息;若是顯示日誌打印時間,其格式如何;
2,默認的日誌級別是什麼,默認的日誌追加方式是什麼,若是沒有配置文件要給與一種約定。
3,支持單獨配置一些日誌名稱的日誌級別;能夠覆蓋包,能夠覆蓋類,實現文件日誌配置繼承。
4,若是將日誌打印到日誌文件,則日誌文件的名稱和目錄在哪裏等信息。
OK,如今日誌框架基本可使用了,功能也比較完善了,這裏核心代碼我有借鑑Log4j源碼,剩下的一些不足就是一些異常捕獲狀況,一些性能上面的提高,一些功能和方法的抽象,好比咱們這裏有輸出地Appender,日誌格式化器Layout。咱們都應該抽象成接口或者抽象類,而後衍生多個子類來嫁入咱們的框架輸出日誌。框架沒有完美,咱們閱讀源碼,最大的用處就是看懂它的設計,來吸取一些好的技巧和設計模式。接下來,我會寫幾篇關於Log4j源碼解析的博客。先這樣吧。