封装SLF4J/Log4j,不再处处定义logger变量

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

自从开始使用日志组件后, 每个类都是这样子的结构:


 
 
  1. public class A { 
  2.   public static final Logger logger = LoggerFactory.getLogger(A.class); 
  3.  
  4. }  

这是一件相当烦人事,必须对他进行封装,使得我们能够通过这样的方法来调用:


 
 
  1. public class A { 
  2.     public void methodA() { 
  3.         Logger.debug("Nice!"); 
  4.     } 
  5. }  

最简单的版本

开始动手后,用最简单的方法封装出了第一个版本:


 
 
  1. // cn.hjktech.slf4j.Logger 
  2. public class Logger { 
  3.     private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Logger.class);; 
  4.     ... 
  5.     public static void debug(...) { 
  6.         logger.debug(...); 
  7.         ... 
  8.     } 
  9.     ... 
  10. }  

看起来很美好, 但测试后发现这种方法会有一个很严重的问题: 我们打印的日志通畅都带有调用方的信息, 如类名、方法名、行数、时间等,其中类名、方法名、行数都是极其关键的信息,但是使用上述的方法来输出日志的话,这三个信息都变成Logger这个类的信息,而不是调用方的信息, 这显然是无法忍受的事。

当然不能就这样了事,既然正常使用的方法能输出正确的信息,那么肯定是有办法可以实现的,我们希望最终的结果是调用Logger.debug(..)打印出来的信息都是完全正确的。

分析源码

此时写个demo来debug跟进一下:


 
 
  1. public class TestLog { 
  2.     @Test 
  3.     public void logTest() { 
  4.         // 在此处打断点 
  5.         LoggerFactory.getLogger(TestLog.class).debug("看看执行流程"); 
  6.     } 
  7. }  

发现最终输出的日志字符串是在PatternLayout.format方法(Logback则是PatternLayoutBase.writeLoopOnConverters方法)中生成的,方法代码如下:


 
 
  1. // Log4j 
  2. public String format(LoggingEvent event) { 
  3.     // Reset working stringbuffer 
  4.     if(sbuf.capacity() > MAX_CAPACITY) { 
  5.         sbuf = new StringBuffer(BUF_SIZE); 
  6.     } else { 
  7.         sbuf.setLength(0); 
  8.     } 
  9.  
  10.     PatternConverter c = head; 
  11.  
  12.     while(c != null) { 
  13.         c.format(sbuf, event); 
  14.         c = c.next
  15.     } 
  16.     return sbuf.toString(); 
  17. }  

其中head指向一个类型为PatternConverter(Logback中是: Converter)的链表,这个链表的节点是在日志类初始化的时候,根据你日志配置文件里的ConversionPattern生成的,比如我的log4j.properties中是这样配置的:


 
 
  1. log4j.appender.SOUT_LOGGER.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm,SSS} %p [%c] [%t] (%F:%L) %l - %m%n 

那么这个链表的结构就是(括号中代表存储的信息):


 
 
  1. DatePatternConverter(时间点)           -> LiteralPatternConverter(" ")   ->  
  2. BasicPatternConverter(LEVEL)          -> LiteralPatternConverter("[")   ->  
  3. CategoryPatternConverter(LoggerName)  -> LiteralPatternConverter("] [") ->  
  4. BasicPatternConverter(线程名)          -> LiteralPatternConverter("] (") -> 
  5. LocationPatternConverter(所在类)       -> LiteralPatternConverter(":")   ->  
  6. LocationPatternConverter(所在行)       -> LiteralPatternConverter(") -") ->  
  7. BasicPatternConverter(日志串)          -> LiteralPatternConverter("\n")  ->  

根据这个链表生成日志字符串类似这样:


 
 
  1. 2016-10-17-13-42,449 DEBUG [TestLog] [main] (TestLog.java:14) - Excuse me? 

那么现在目标很明确了,我们要使LocationPatternConverter的输出为我们真正打印纸日的类的信息,继续跟进到PatternConverter.format(LocationPatternConverter的父类)方法,其内部生成了一个LocationInfo对象,该类的构造方法中如下:


 
 
  1. for(int i = elements.length - 1; i >= 0; i--) { 
  2.     // 获取第i帧的类名 
  3.     String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs); 
  4.     if(fqnOfCallingClass.equals(thisClass)) { 
  5.         // 如果类名和fqnOfCallingClass相等,则认为i + 1帧是代码中实际调用方法的 
  6.         int caller = i + 1; 
  7.         if (caller < elements.length) { 
  8.             // 记录实际调用类的类名 
  9.             className = prevClass; 
  10.             // 记录实际调用的方法名 
  11.             methodName = (String) getMethodNameMethod.invoke(elements[caller], noArgs); 
  12.             // 记录实际调用类所在的文件名 
  13.             fileName = (String) getFileNameMethod.invoke(elements[caller], noArgs); 
  14.             if (fileName == null) { 
  15.                 fileName = NA; 
  16.             } 
  17.             // 记录调用日志方法的行数 
  18.             int line = ((Integer) getLineNumberMethod.invoke(elements[caller], noArgs)).intValue(); 
  19.             if (line < 0) { 
  20.                 lineNumber = NA; 
  21.             } else { 
  22.                 lineNumber = String.valueOf(line); 
  23.             } 
  24.             // 拼接成最终要输出到日志的字符串, 如:TestLog.logTest(TestLog.java:14) 
  25.             StringBuffer buf = new StringBuffer(); 
  26.             buf.append(className); 
  27.             buf.append("."); 
  28.             buf.append(methodName); 
  29.             buf.append("("); 
  30.             buf.append(fileName); 
  31.             buf.append(":"); 
  32.             buf.append(lineNumber); 
  33.             buf.append(")"); 
  34.             this.fullInfo = buf.toString(); 
  35.         } 
  36.         return
  37.     } 
  38.     // 记录上一帧的类名 
  39.     prevClass = thisClass; 
  40. }  

其中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这个类:


 
 
  1. public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements LocationAwareLogger, Serializable { 
  2.     ... 
  3.     static final String FQCN = Log4jLoggerAdapter.class.getName(); 
  4.     ... 
  5. }  

而如果没有配合SLF4J使用时,fqnOfCallingClass的值则来源于org.apache.log4j.Logger类:


 
 
  1. public class Logger extends Category { 
  2.     ... 
  3.     private static final String FQCN = Logger.class.getName(); 
  4.     .... 
  5. }  

代理Logger类来修改FQCN

好了,现在我们只需要修改这个值就行了。第一反应是使用反射去掉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:


 
 
  1. // cn.hjktech.slf4j.Logger 
  2. public class Logger { 
  3.     private static org.slf4j.Logger logger; 
  4.     private static final String FQCN = Logger.class.getName(); 
  5.  
  6.     static { 
  7.         try { 
  8.             Enhancer eh = new Enhancer(); 
  9.             eh.setSuperclass(org.apache.log4j.Logger.class); 
  10.             eh.setCallbackType(LogInterceptor.class); 
  11.             Class c = eh.createClass(); 
  12.             Enhancer.registerCallbacks(c, new LogInterceptor[]{new LogInterceptor()}); 
  13.             Constructor<org.apache.log4j.Logger> constructor = c.getConstructor(String.class); 
  14.             org.apache.log4j.Logger loggerProxy= constructor.newInstance(Logger.class.getName()); 
  15.             ... 
  16.         } catch (...) { 
  17.             throw new RuntimeException("初始化Logger失败", e); 
  18.         } 
  19.     } 
  20.  
  21.     private static class LogInterceptor implements MethodInterceptor { 
  22.         public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { 
  23.             // 只拦截log方法。 
  24.             if (objects.length != 4 || !method.getName().equals("log")) 
  25.                 return methodProxy.invokeSuper(o, objects); 
  26.             // 替换传给log方法的第一个参数为我们自定义的FQCN 
  27.             objects[0] = FQCN; 
  28.             return methodProxy.invokeSuper(o, objects); 
  29.         } 
  30.     } 
  31. }  

代理defaultFactory

现在我们已经有了被代理的loggerProxy对象了,我们还需要将这个对象赋值给Log4jLoggerAdapter的logger成员变量,

logger成员变量是在Log4jLoggerAdapter的构造方法中被作为参数传入的,它的来源如下图:

从上图中可以看到,LogManager.getLoggerRepository方法返回的对象中持有defaultFactory对象,因此我还需要代理这个对象,将它产生的'logger'对象替换成我们的'logger'就大功告成了,并且makeNewLoggerInstance方法是在LoggerFactory接口中定义的,所以我们只需要使用JDK的动态代理就可以完成了。实现代码如下:


 
 
  1. static { 
  2.     try { 
  3.         ... 
  4.         LoggerRepository loggerRepository = LogManager.getLoggerRepository(); 
  5.         org.apache.log4j.spi.LoggerFactory lf = ReflectionUtil.getFieldValue(loggerRepository, "defaultFactory"); 
  6.         Object loggerFactoryProxy = Proxy.newProxyInstance( 
  7.             LoggerFactory.class.getClassLoader(), 
  8.             new Class[]{LoggerFactory.class}, 
  9.             new NewLoggerHandler(loggerProxy) 
  10.         ); 
  11.  
  12.         ReflectionUtil.setFieldValue(loggerRepository, "defaultFactory", loggerFactoryProxy); 
  13.             logger = org.slf4j.LoggerFactory.getLogger(Logger.class.getName()); 
  14.         ReflectionUtil.setFieldValue(loggerRepository, "defaultFactory", lf); 
  15.     } catch (...) { 
  16.         throw new RuntimeException("初始化Logger失败", e); 
  17.     } 
  18.  
  19. private static class NewLoggerHandler implements InvocationHandler { 
  20.     private final org.apache.log4j.Logger proxyLogger; 
  21.  
  22.     public NewLoggerHandler(org.apache.log4j.Logger proxyLogger) { 
  23.         this.proxyLogger = proxyLogger; 
  24.     } 
  25.  
  26.     @Override 
  27.     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
  28.         return proxyLogger; 
  29.     } 
  30. }  

实现流程和最终代码

我们最终实现方案如下:

Logger的代码如下:


 
 
  1. public class Logger { 
  2.     private static org.slf4j.Logger logger; 
  3.     private static final String FQCN = Logger.class.getName(); 
  4.      
  5.     static { 
  6.         try { 
  7.             Enhancer eh = new Enhancer(); 
  8.             eh.setSuperclass(org.apache.log4j.Logger.class); 
  9.             eh.setCallbackType(LogInterceptor.class); 
  10.             Class c = eh.createClass(); 
  11.             Enhancer.registerCallbacks(c, new LogInterceptor[]{new LogInterceptor()}); 
  12.  
  13.             Constructor<org.apache.log4j.Logger> constructor = c.getConstructor(String.class); 
  14.             org.apache.log4j.Logger loggerProxy = constructor.newInstance(Logger.class.getName()); 
  15.  
  16.             LoggerRepository loggerRepository = LogManager.getLoggerRepository(); 
  17.             org.apache.log4j.spi.LoggerFactory lf = ReflectionUtil.getFieldValue(loggerRepository, "defaultFactory"); 
  18.             Object loggerFactoryProxy = Proxy.newProxyInstance( 
  19.                 LoggerFactory.class.getClassLoader(), 
  20.                 new Class[]{LoggerFactory.class}, 
  21.                 new NewLoggerHandler(loggerProxy) 
  22.             ); 
  23.  
  24.             ReflectionUtil.setFieldValue(loggerRepository, "defaultFactory", loggerFactoryProxy); 
  25.             logger = org.slf4j.LoggerFactory.getLogger(Logger.class.getName()); 
  26.             ReflectionUtil.setFieldValue(loggerRepository, "defaultFactory", lf); 
  27.         } catch ( 
  28.             IllegalAccessException | 
  29.                 NoSuchMethodException | 
  30.                 InvocationTargetException | 
  31.                 InstantiationException e) { 
  32.             throw new RuntimeException("初始化Logger失败", e); 
  33.         } 
  34.     } 
  35.  
  36.     private static class LogInterceptor implements MethodInterceptor { 
  37.         public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { 
  38.             // 只拦截log方法。 
  39.             if (objects.length != 4 || !method.getName().equals("log")) 
  40.                 return methodProxy.invokeSuper(o, objects); 
  41.             objects[0] = FQCN; 
  42.             return methodProxy.invokeSuper(o, objects); 
  43.         } 
  44.     } 
  45.  
  46.     private static class NewLoggerHandler implements InvocationHandler { 
  47.         private final org.apache.log4j.Logger proxyLogger; 
  48.  
  49.         public NewLoggerHandler(org.apache.log4j.Logger proxyLogger) { 
  50.             this.proxyLogger = proxyLogger; 
  51.         } 
  52.  
  53.         @Override 
  54.         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
  55.             return proxyLogger; 
  56.         } 
  57.     } 
  58.  
  59.     // 剩下的Logger需要封装的方法可以根据自己的需要来实现 
  60.     // 我个人认为slf4j的api足够好用了,所以大部分只是写了一些类似下面的代码 
  61.     public static void debug(String msg) { 
  62.         logger.debug(msg); 
  63.     } 
  64. }  

ReflectionUtil的代码如下:


 
 
  1. public class ReflectionUtil { 
  2.     public static <T> T getFieldValue(@NotNull Object object, 
  3.                                       @NotNull String fullName) throws IllegalAccessException { 
  4.         return getFieldValue(object, fullName, false); 
  5.     } 
  6.  
  7.     public static <T> T getFieldValue(@NotNull Object object, 
  8.                                       @NotNull String fieldName, 
  9.                                       boolean traceable) throws IllegalAccessException { 
  10.         Field field; 
  11.         String[] fieldNames = fieldName.split("\\."); 
  12.         for (String targetField : fieldNames) { 
  13.             field = searchField(object.getClass(), targetField, traceable); 
  14.             if (field == null
  15.                 return null
  16.  
  17.             object = getValue(object, field); 
  18.         } 
  19.  
  20.         return (T) object; 
  21.     } 
  22.  
  23.     private static Field searchField(Class c, String targetField, boolean traceable) { 
  24.         do { 
  25.             Field[] fields = c.getDeclaredFields(); 
  26.             for (Field f : fields) { 
  27.                 if (f.getName().equals(targetField)) { 
  28.                     return f; 
  29.                 } 
  30.             } 
  31.             c = c.getSuperclass(); 
  32.             traceable = traceable && c != Object.class; 
  33.         } while (traceable); 
  34.  
  35.         return null
  36.     } 
  37.  
  38.     private static <T> T getValue(Object target, Field field) throws IllegalAccessException { 
  39.         if (!field.isAccessible()) 
  40.             field.setAccessible(true); 
  41.         return (T) field.get(target); 
  42.     } 
  43.  
  44.     public static boolean setFieldValue(@NotNull Object target, 
  45.                                         @NotNull String fieldName, 
  46.                                         @NotNull Object value) throws IllegalAccessException { 
  47.         return setFieldValue(target, fieldName, value, false); 
  48.     } 
  49.  
  50.     public static boolean setFieldValue(@NotNull Object target, 
  51.                                         @NotNull String fieldName, 
  52.                                         @NotNull Object value, 
  53.                                         boolean traceable) throws IllegalAccessException { 
  54.         Field field = searchField(target.getClass(), fieldName, traceable); 
  55.         if (field != null
  56.             return setValue(field, target, value); 
  57.         return false
  58.     } 
  59.  
  60.     private static boolean setValue(Field field, Object target, Object value) throws IllegalAccessException { 
  61.         if (!field.isAccessible()) 
  62.             field.setAccessible(true); 
  63.         field.set(target, value); 
  64.         return true
  65.     } 
  66. }  

测试


 
 
  1. public class TestLog { 
  2.  
  3.     @Test 
  4.     public void logTest() { 
  5.         Logger.debug((Marker)null"这是调用封装的Logger输出日志"); 
  6.         LoggerFactory.getLogger(TestLog.class).info("常规方法输出日志"); 
  7.     } 
  8. }  

输出结果:


 
 
  1. 2016-10-19-15-00,308 DEBUG [cn.hjktech.slf4j.Logger] [main] (TestLog.java:13) TestLog.logTest(TestLog.java:13) - 这是调用封装的Logger输出日志  
  2. 2016-10-19-15-00,311 INFO [TestLog] [main] (TestLog.java:14) TestLog.logTest(TestLog.java:14) - 常规方法输出日志  



作者:疯狂的爱因斯坦

来源:51CTO

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
12天前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
119 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
4月前
|
存储 SQL Java
Spring Boot使用slf4j进行日志记录
本节课主要对 slf4j 做了一个简单的介绍,并且对 Spring Boot 中如何使用 slf4j 输出日志做了详细的说明,着重分析了 logback.xml 文件中对日志相关信息的配置,包括日志的不同级别...
|
22天前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
86 1
|
1月前
|
数据采集 监控 Java
SpringBoot日志全方位超详细手把手教程,零基础可学习 日志如何配置及SLF4J的使用......
本文是关于SpringBoot日志的详细教程,涵盖日志的定义、用途、SLF4J框架的使用、日志级别、持久化、文件分割及格式配置等内容。
130 0
SpringBoot日志全方位超详细手把手教程,零基础可学习 日志如何配置及SLF4J的使用......
|
3月前
|
存储 监控 Java
Java日志通关(三) - Slf4j 介绍
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。
|
3月前
|
存储 监控 Java
Java日志通关(三) - Slf4j 介绍
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。
|
3月前
|
Java 应用服务中间件 HSF
Java应用结构规范问题之AllLoggers接口获取异常日志的Logger实例的问题如何解决
Java应用结构规范问题之AllLoggers接口获取异常日志的Logger实例的问题如何解决
|
3月前
|
JavaScript Java API
Java日志通关(二) - Slf4j+Logback 整合及排包
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第二篇。
|
3月前
|
消息中间件 监控 搜索推荐
OpenFeign日志组件Logger原理与应用
该文章详细解释了如何在OpenFeign中配置并使用请求和响应的GZIP压缩功能。
|
3月前
|
Java
OpenFeign 各种用法、 logger 日志记录
OpenFeign 各种用法、 logger 日志记录
53 0