轉載請註明出處: 封裝SLF4J/Log4j,再也不到處定義logger變量懶得看實現過程的能夠直接跳到最後看實現流程圖和代碼實現。Logback和log4j是內部實現類似,因此能夠用相似的思路實現。java
自從開始使用日誌組件後, 每一個類都是這樣子的結構:apache
public class A { public static final Logger logger = LoggerFactory.getLogger(A.class); }
這是一件至關煩人事,必須對他進行封裝,使得咱們可以經過這樣的方法來調用:api
public class A { public void methodA() { Logger.debug("Nice!"); } }
開始動手後,用最簡單的方法封裝出了第一個版本:app
// cn.hjktech.slf4j.Logger public class Logger { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Logger.class);; ... public static void debug(...) { logger.debug(...); ... } ... }
看起來很美好, 但測試後發現這種方法會有一個很嚴重的問題: 咱們打印的日誌通暢都帶有調用方的信息, 如類名、方法名、行數、時間等,其中類名、方法名、行數都是極其關鍵的信息,可是使用上述的方法來輸出日誌的話,這三個信息都變成Logger這個類的信息,而不是調用方的信息, 這顯然是沒法忍受的事。框架
固然不能就這樣了事,既然正常使用的方法能輸出正確的信息,那麼確定是有辦法能夠實現的,咱們但願最終的結果是調用Logger.debug(..)
打印出來的信息都是徹底正確的。ide
此時寫個demo來debug跟進一下:工具
public class TestLog { @Test public void logTest() { // 在此處打斷點 LoggerFactory.getLogger(TestLog.class).debug("看看執行流程"); } }
發現最終輸出的日誌字符串是在PatternLayout.format
方法(Logback則是PatternLayoutBase.writeLoopOnConverters
方法)中生成的,方法代碼以下:oop
// Log4j public String format(LoggingEvent event) { // Reset working stringbuffer if(sbuf.capacity() > MAX_CAPACITY) { sbuf = new StringBuffer(BUF_SIZE); } else { sbuf.setLength(0); } PatternConverter c = head; while(c != null) { c.format(sbuf, event); c = c.next; } return sbuf.toString(); }
其中head指向一個類型爲PatternConverter
(Logback中是: Converter
)的鏈表,這個鏈表的節點是在日誌類初始化的時候,根據你日誌配置文件裏的ConversionPattern
生成的,好比個人log4j.properties
中是這樣配置的:測試
log4j.appender.SOUT_LOGGER.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm,SSS} %p [%c] [%t] (%F:%L) %l - %m%n
那麼這個鏈表的結構就是(括號中表明存儲的信息):this
DatePatternConverter(時間點) -> LiteralPatternConverter(" ") -> BasicPatternConverter(LEVEL) -> LiteralPatternConverter("[") -> CategoryPatternConverter(LoggerName) -> LiteralPatternConverter("] [") -> BasicPatternConverter(線程名) -> LiteralPatternConverter("] (") -> LocationPatternConverter(所在類) -> LiteralPatternConverter(":") -> LocationPatternConverter(所在行) -> LiteralPatternConverter(") -") -> BasicPatternConverter(日誌串) -> LiteralPatternConverter("\n") ->
根據這個鏈表生成日誌字符串相似這樣: 2016-10-17-13-42,449 DEBUG [TestLog] [main] (TestLog.java:14) - Excuse me?
那麼如今目標很明確了,咱們要使LocationPatternConverter
的輸出爲咱們真正打印紙日的類的信息,繼續跟進到PatternConverter.format(LocationPatternConverter的父類)
方法,其內部生成了一個LocationInfo
對象,該類的構造方法中以下:
for(int i = elements.length - 1; i >= 0; i--) { // 獲取第i幀的類名 String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs); if(fqnOfCallingClass.equals(thisClass)) { // 若是類名和fqnOfCallingClass相等,則認爲i + 1幀是代碼中實際調用方法的 int caller = i + 1; if (caller < elements.length) { // 記錄實際調用類的類名 className = prevClass; // 記錄實際調用的方法名 methodName = (String) getMethodNameMethod.invoke(elements[caller], noArgs); // 記錄實際調用類所在的文件名 fileName = (String) getFileNameMethod.invoke(elements[caller], noArgs); if (fileName == null) { fileName = NA; } // 記錄調用日誌方法的行數 int line = ((Integer) getLineNumberMethod.invoke(elements[caller], noArgs)).intValue(); if (line < 0) { lineNumber = NA; } else { lineNumber = String.valueOf(line); } // 拼接成最終要輸出到日誌的字符串, 如:TestLog.logTest(TestLog.java:14) StringBuffer buf = new StringBuffer(); buf.append(className); buf.append("."); buf.append(methodName); buf.append("("); buf.append(fileName); buf.append(":"); buf.append(lineNumber); buf.append(")"); this.fullInfo = buf.toString(); } return; } // 記錄上一幀的類名 prevClass = thisClass; }
其中elements
是當前方法調用棧的堆棧軌跡,這段代碼經過遍歷堆棧軌跡每一幀的類名並和fqnOfCallingClass
比較,若是相符的話,則認爲它的上一幀是實際調用方法。
以下圖中,fqnOfCallingClass
的值是org.slf4j.impl.Log4jLoggerAdapter
,而在堆棧軌跡總能夠發現類的上一個幀正好是咱們的實際調用類TestLog.logTest
:
所以,咱們如今只須要讓fqnOfCallingClass
的值變成咱們封裝的日誌類cn.hjktech.slf4j.Logger
就大功告成了。fqnOfCallingClass
是LoggingEvent.getLocationInformation
建立LocationInfo
時傳入的參數,而LoggingEvent
又是在Category.forcedLog
方法中建立的,而且繼續網上追蹤,會發現fqnOfCallingClass
的值最終來源於org.slf4j.impl.Log4jLoggerAdapter
這個類:
public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements LocationAwareLogger, Serializable { ... static final String FQCN = Log4jLoggerAdapter.class.getName(); ... }
而若是沒有配合SLF4J使用時,fqnOfCallingClass
的值則來源於org.apache.log4j.Logger
類:
public class Logger extends Category { ... private static final String FQCN = Logger.class.getName(); .... }
好了,如今咱們只須要修改這個值就好了。第一反應是使用反射去掉final修飾符,而後修改它的值,這種方法雖然對咱們本身的代碼可行,可是當引入其它框架,而且其它框架也使用的Log4j
時,就會致使它們的日誌信息出錯,由於它們並非調用的咱們封裝的Logger工具類,它們日誌的堆棧軌跡中不會有咱們工具類(如cn.hjktech.slf4j.Logger
),所以咱們須要另尋它法。
既然經過反射行不通, 那麼咱們能夠經過動態代理的方式,在構造LoggingEvent
對象以前將FQCN
這個參數的值給替換掉,在跟蹤過程當中發現Log4jLoggerAdapter
最終都是調用的org.apache.log4j.Logger.log
方法並將FQCN
最爲參數傳入,所以org.apache.log4j.Logger
這個類就是咱們要代理的類了。
瞭解JDK代理的人都知道,使用的條件是被代理類必須實現某一個接口,而org.apache.log4j.Logger.log
這個方法並非來自於某一個接口,因此咱們選擇使用Cglib
:
// cn.hjktech.slf4j.Logger public class Logger { private static org.slf4j.Logger logger; private static final String FQCN = Logger.class.getName(); static { try { Enhancer eh = new Enhancer(); eh.setSuperclass(org.apache.log4j.Logger.class); eh.setCallbackType(LogInterceptor.class); Class c = eh.createClass(); Enhancer.registerCallbacks(c, new LogInterceptor[]{new LogInterceptor()}); Constructor<org.apache.log4j.Logger> constructor = c.getConstructor(String.class); org.apache.log4j.Logger loggerProxy= constructor.newInstance(Logger.class.getName()); ... } catch (...) { throw new RuntimeException("初始化Logger失敗", e); } } private static class LogInterceptor implements MethodInterceptor { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { // 只攔截log方法。 if (objects.length != 4 || !method.getName().equals("log")) return methodProxy.invokeSuper(o, objects); // 替換傳給log方法的第一個參數爲咱們自定義的FQCN objects[0] = FQCN; return methodProxy.invokeSuper(o, objects); } } }
如今咱們已經有了被代理的loggerProxy
對象了,咱們還須要將這個對象賦值給Log4jLoggerAdapter
的logger
成員變量,logger
成員變量是在Log4jLoggerAdapter
的構造方法中被做爲參數傳入的,它的來源以下圖:
從上圖中能夠看到,LogManager.getLoggerRepository
方法返回的對象中持有defaultFactory
對象,所以我還須要代理這個對象,將它產生的'logger'對象替換成咱們的'logger'就大功告成了,而且makeNewLoggerInstance
方法是在LoggerFactory
接口中定義的,因此咱們只須要使用JDK的動態代理就能夠完成了。實現代碼以下:
static { try { ... LoggerRepository loggerRepository = LogManager.getLoggerRepository(); org.apache.log4j.spi.LoggerFactory lf = ReflectionUtil.getFieldValue(loggerRepository, "defaultFactory"); Object loggerFactoryProxy = Proxy.newProxyInstance( LoggerFactory.class.getClassLoader(), new Class[]{LoggerFactory.class}, new NewLoggerHandler(loggerProxy) ); ReflectionUtil.setFieldValue(loggerRepository, "defaultFactory", loggerFactoryProxy); logger = org.slf4j.LoggerFactory.getLogger(Logger.class.getName()); ReflectionUtil.setFieldValue(loggerRepository, "defaultFactory", lf); } catch (...) { throw new RuntimeException("初始化Logger失敗", e); } } private static class NewLoggerHandler implements InvocationHandler { private final org.apache.log4j.Logger proxyLogger; public NewLoggerHandler(org.apache.log4j.Logger proxyLogger) { this.proxyLogger = proxyLogger; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return proxyLogger; } }
咱們最終實現方案以下:
Logger的代碼以下:
public class Logger { private static org.slf4j.Logger logger; private static final String FQCN = Logger.class.getName(); static { try { Enhancer eh = new Enhancer(); eh.setSuperclass(org.apache.log4j.Logger.class); eh.setCallbackType(LogInterceptor.class); Class c = eh.createClass(); Enhancer.registerCallbacks(c, new LogInterceptor[]{new LogInterceptor()}); Constructor<org.apache.log4j.Logger> constructor = c.getConstructor(String.class); org.apache.log4j.Logger loggerProxy = constructor.newInstance(Logger.class.getName()); LoggerRepository loggerRepository = LogManager.getLoggerRepository(); org.apache.log4j.spi.LoggerFactory lf = ReflectionUtil.getFieldValue(loggerRepository, "defaultFactory"); Object loggerFactoryProxy = Proxy.newProxyInstance( LoggerFactory.class.getClassLoader(), new Class[]{LoggerFactory.class}, new NewLoggerHandler(loggerProxy) ); ReflectionUtil.setFieldValue(loggerRepository, "defaultFactory", loggerFactoryProxy); logger = org.slf4j.LoggerFactory.getLogger(Logger.class.getName()); ReflectionUtil.setFieldValue(loggerRepository, "defaultFactory", lf); } catch ( IllegalAccessException | NoSuchMethodException | InvocationTargetException | InstantiationException e) { throw new RuntimeException("初始化Logger失敗", e); } } private static class LogInterceptor implements MethodInterceptor { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { // 只攔截log方法。 if (objects.length != 4 || !method.getName().equals("log")) return methodProxy.invokeSuper(o, objects); objects[0] = FQCN; return methodProxy.invokeSuper(o, objects); } } private static class NewLoggerHandler implements InvocationHandler { private final org.apache.log4j.Logger proxyLogger; public NewLoggerHandler(org.apache.log4j.Logger proxyLogger) { this.proxyLogger = proxyLogger; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return proxyLogger; } } // 剩下的Logger須要封裝的方法能夠根據本身的須要來實現 // 我我的認爲slf4j的api足夠好用了,因此大部分只是寫了一些相似下面的代碼 public static void debug(String msg) { logger.debug(msg); } }
ReflectionUtil的代碼以下:
public class ReflectionUtil { public static <T> T getFieldValue(@NotNull Object object, @NotNull String fullName) throws IllegalAccessException { return getFieldValue(object, fullName, false); } public static <T> T getFieldValue(@NotNull Object object, @NotNull String fieldName, boolean traceable) throws IllegalAccessException { Field field; String[] fieldNames = fieldName.split("\\."); for (String targetField : fieldNames) { field = searchField(object.getClass(), targetField, traceable); if (field == null) return null; object = getValue(object, field); } return (T) object; } private static Field searchField(Class c, String targetField, boolean traceable) { do { Field[] fields = c.getDeclaredFields(); for (Field f : fields) { if (f.getName().equals(targetField)) { return f; } } c = c.getSuperclass(); traceable = traceable && c != Object.class; } while (traceable); return null; } private static <T> T getValue(Object target, Field field) throws IllegalAccessException { if (!field.isAccessible()) field.setAccessible(true); return (T) field.get(target); } public static boolean setFieldValue(@NotNull Object target, @NotNull String fieldName, @NotNull Object value) throws IllegalAccessException { return setFieldValue(target, fieldName, value, false); } public static boolean setFieldValue(@NotNull Object target, @NotNull String fieldName, @NotNull Object value, boolean traceable) throws IllegalAccessException { Field field = searchField(target.getClass(), fieldName, traceable); if (field != null) return setValue(field, target, value); return false; } private static boolean setValue(Field field, Object target, Object value) throws IllegalAccessException { if (!field.isAccessible()) field.setAccessible(true); field.set(target, value); return true; } }
public class TestLog { @Test public void logTest() { Logger.debug((Marker)null, "這是調用封裝的Logger輸出日誌"); LoggerFactory.getLogger(TestLog.class).info("常規方法輸出日誌"); } }
輸出結果:2016-10-19-15-00,308 DEBUG [cn.hjktech.slf4j.Logger] [main] (TestLog.java:13) TestLog.logTest(TestLog.java:13) - 這是調用封裝的Logger輸出日誌
2016-10-19-15-00,311 INFO [TestLog] [main] (TestLog.java:14) TestLog.logTest(TestLog.java:14) - 常規方法輸出日誌