 - 日志体系)
源码仓库Gitee·GitHub一、概况在项目开发当中日志对于我们开发或运维人员来说是一个必不可少的工具。在线下我们可以通过 debug 来查找排除问题但对于线上系统来说我们只能通过日志分析来查找问题我们可以通过日志打印来获取我们需要的信息来判断、分析系统运行结果是否正常或哪里出现了问题可以定位到具体问题和位置。当前流行的日志框架有jul(java util logging)log4jlog4j2jcl(Jakarta Commons Logging)logbackslf4j二、应用和探讨1.jul(java util logging)java自带的日志记录技术java.util.logging.Logger可以直接记录日志功能比较太过于简单不支持占位符显示拓展性比较差import java.util.logging.Logger; public class JUL { public static void main(String[] args) { Logger logger Logger.getLogger(JUL); logger.info(java util logging); } }2.log4j不支持使用占位符在高并发日志量大的情况存在bug容易导致内存、CPU冲高使用 log4j 需要 pom 文件中引入 log4j 所需要的jar包和引入 log4j 的配置文件 log4j.propertiespom.xml dependency groupIdlog4j/groupId artifactIdlog4j/artifactId version1.2.12/version /dependencylog4j.properties log4j.rootLogger info,stdout log4j.appender.stdout org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target System.out log4j.appender.stdout.layout org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%nimport org.apache.log4j.Logger; public class Log4j { public static void main(String[] args) { Logger logger Logger.getLogger(Log4j.class); logger.info(log4j); } }记住 jul 和 log4j 日志输出格式的差别颜色不一致jul 输出日志为红色log4j 输出日志是白色3.log4j2log4j2 和 log4j 是同一个作者开发只不过log4j2是重新架构的一款日志组件改进了log4j的bug以及吸取了优秀的logback的设计重新推出的一款新组件在效率和性能上有了很大的提升必须同时依赖 log4j-core 和 log4j-api否则报错“ERROR StatusLogger Log4j2 could not find a logging implementation”pom.xml dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-api/artifactId version2.13.2/version /dependency dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.9.1/version /dependency同时需要配置文件和测试代码log4j2.xml、Log4j2.java?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n / /Console /Appenders Loggers Root levelinfo AppenderRef refConsole / /Root /Loggers /Configurationimport org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Log4j2 { public static void main(String[] args) { Logger logger LogManager.getLogger(Log4j2); logger.info(log4j2); } }4.jcl(Jakarta Commons Logging)Spring Framework 4.x 版本依赖了commons-logging.jarSpring Framework 5.x 版本 依赖了spring-jcl.jarSpring Framework 4.x Spring Framework 5.x 1.commons-logging.jarSpring Framework 4.xpom.xml dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version4.3.20.RELEASE/version /dependency 或 dependency groupIdcommons-logging/groupId artifactIdcommons-logging/artifactId version1.2/version /dependencyimport org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class JCL { public static void main(String[] args) { Log log LogFactory.getLog(JCL); log.info(jakatra common logging); } }跟上面的jul输出格式一致探讨为什么 jcl 的输出格式是 jul 格式呢接下来从源码来进行分析从 LogFactory.getLog(JCL) 开始从下面可以看出是从工厂LogFactory public static Log getLog(String name) throws LogConfigurationException { return getFactory().getInstance(name); }getInstance(name)是一个抽象方法看它的实现方法LogFactory public abstract Log getInstance(String name) throws LogConfigurationException;LogFactoryImpl public Log getInstance(String name) throws LogConfigurationException { Log instance (Log) instances.get(name);//开始是null if (instance null) { instance newInstance(name);//创建实例 instances.put(name, instance);//instances是一个Hashtable实例放入缓存中 } return instance; }下面的代码就一行重要LogFactoryImpl protected Log newInstance(String name) throws LogConfigurationException { Log instance; try { if (logConstructor null) { instance discoverLogImplementation(name);//创建实例 } else { Object params[] { name }; instance (Log) logConstructor.newInstance(params); } if (logMethod ! null) { Object params[] { this }; logMethod.invoke(instance, params); } return instance; } catch (LogConfigurationException lce) { // this type of exception means there was a problem in discovery // and weve already output diagnostics about the issue, etc.; // just pass it on throw lce; } catch (InvocationTargetException e) { // A problem occurred invoking the Constructor or Method // previously discovered Throwable c e.getTargetException(); throw new LogConfigurationException(c null ? e : c); } catch (Throwable t) { handleThrowable(t); // may re-throw t // A problem occurred invoking the Constructor or Method // previously discovered throw new LogConfigurationException(t); } }在下面的代码中可以看到实例的创建是从classesToDiscover遍历获取来的LogFactoryImpl private Log discoverLogImplementation(String logCategory) throws LogConfigurationException { if (isDiagnosticsEnabled()) { logDiagnostic(Discovering a Log implementation...); } initConfiguration(); Log result null; // See if the user specified the Log implementation to use String specifiedLogClassName findUserSpecifiedLogClassName(); if (specifiedLogClassName ! null) { if (isDiagnosticsEnabled()) { logDiagnostic(Attempting to load user-specified log class specifiedLogClassName ...); } result createLogFromClass(specifiedLogClassName, logCategory, true); if (result null) { StringBuffer messageBuffer new StringBuffer(User-specified log class ); messageBuffer.append(specifiedLogClassName); messageBuffer.append( cannot be found or is not useable.); // Mistyping or misspelling names is a common fault. // Construct a good error message, if we can informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LOG4J_LOGGER); informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_JDK14_LOGGER); informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LUMBERJACK_LOGGER); informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_SIMPLE_LOGGER); throw new LogConfigurationException(messageBuffer.toString()); } return result; } if (isDiagnosticsEnabled()) { logDiagnostic( No user-specified Log implementation; performing discovery using the standard supported logging implementations...); } for(int i0; iclassesToDiscover.length result null; i) { result createLogFromClass(classesToDiscover[i], logCategory, true);//创建实例 } if (result null) { throw new LogConfigurationException (No suitable Log implementation); } return result; }从 classesToDiscover 数组中可以看到数组存放具体的日志框架的类名,通过循环数组依次去匹配这些类名是否在项目中被依赖了如果找到依赖则直接使用有先后顺序log4jjul。LogFactoryImpl private static final String LOGGING_IMPL_LOG4J_LOGGER org.apache.commons.logging.impl.Log4JLogger; private static final String[] classesToDiscover { LOGGING_IMPL_LOG4J_LOGGER, org.apache.commons.logging.impl.Jdk14Logger, org.apache.commons.logging.impl.Jdk13LumberjackLogger, org.apache.commons.logging.impl.SimpleLog };下面使用commons-logging.jar和 log4j 下面的运行结果验证了上面的源码pom.xml dependency groupIdlog4j/groupId artifactIdlog4j/artifactId version1.2.12/version /dependency dependency groupIdcommons-logging/groupId artifactIdcommons-logging/artifactId version1.2/version /dependency2.spring-jcl.jarSpring Framework 5.xpom.xml dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.2.6.RELEASE/version /dependency 或 dependency groupIdorg.springframework/groupId artifactIdspring-jcl/artifactId version5.2.6.RELEASE/version /dependencyimport org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class JCL { public static void main(String[] args) { Log log LogFactory.getLog(JCL); log.info(jakatra common logging); } }从下图可以看出spring-jcl 默认还是使用 jul接下来还是从源码来进行分析查看它的运行机制从 LogFactory.getLog(JCL)开始从下面可以看到它是从一个适配器获取日志对象的public static Log getLog(String name) { return LogAdapter.createLog(name);从下面代码可以看出LogAdapter 在初始化时会执行一个 static 的代码块设置 logApi 的值依次用 log4j2slf4j-LAL可以将 log4j 桥接到 slf4jslf4j 去反射判断是否存在对应依赖如果有则设置 logApi 为对应值否则默认为 jul然后在 createLog 时switch-case 根据 logApi 去获取 log 对象默认是 jul 来实现。LogAdapter: private static final String LOG4J_SPI org.apache.logging.log4j.spi.ExtendedLogger;//log4j2 private static final String LOG4J_SLF4J_PROVIDER org.apache.logging.slf4j.SLF4JProvider;//slf4j绑定log4j private static final String SLF4J_SPI org.slf4j.spi.LocationAwareLogger; private static final String SLF4J_API org.slf4j.Logger; private static final LogApi logApi; static { if (isPresent(LOG4J_SPI)) { if (isPresent(LOG4J_SLF4J_PROVIDER) isPresent(SLF4J_SPI)) { // log4j-to-slf4j bridge - well rather go with the SLF4J SPI; // however, we still prefer Log4j over the plain SLF4J API since // the latter does not have location awareness support. logApi LogApi.SLF4J_LAL; } else { // Use Log4j 2.x directly, including location awareness support logApi LogApi.LOG4J;//log4j2 } } else if (isPresent(SLF4J_SPI)) { // Full SLF4J SPI including location awareness support logApi LogApi.SLF4J_LAL; } else if (isPresent(SLF4J_API)) { // Minimal SLF4J API without location awareness support logApi LogApi.SLF4J; } else { // java.util.logging as default logApi LogApi.JUL; } } //加载类加载到指定的类返回true否则返回false加载不到指定类会抛出异常 private static boolean isPresent(String className) { try { Class.forName(className, false, LogAdapter.class.getClassLoader()); return true; } catch (ClassNotFoundException ex) { return false; } } public static Log createLog(String name) { switch (logApi) { case LOG4J://log4j2 return Log4jAdapter.createLog(name); case SLF4J_LAL: return Slf4jAdapter.createLocationAwareLog(name); case SLF4J: return Slf4jAdapter.createLog(name); default: // Defensively use lazy-initializing adapter class here as well since the // java.logging module is not present by default on JDK 9. We are requiring // its presence if neither Log4j nor SLF4J is available; however, in the // case of Log4j or SLF4J, we are trying to prevent early initialization // of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly // trying to parse the bytecode for all the cases of this switch clause. return JavaUtilAdapter.createLog(name); } }总结commons-logging.jarSpring Framework 4.x 封装了一个静态数组支持 jul 和log4j然后依次循环遍历反射对应的依赖如果对应依赖存在则返回对应实现默认为 jul其中 log4jjulspring-jcl.jarSpring Framework 5.x修改了commons-logging.jar通过适配器获取日志对象支持 jul 和 log4j2不支持 log4j如果项目需要 log4j需要 slf4j 配合使用还拓展支持 slf4j有着更好的扩展性和兼容性如下案例dependency groupIdlog4j/groupId artifactIdlog4j/artifactId version1.2.12/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.2.6.RELEASE/version /dependencylog4j.properties log4j.rootLogger Console,stdout log4j.appender.stdout org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target System.out log4j.appender.stdout.layout org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n log4j.appender.CONSOLEorg.apache.log4j.ConsoleAppender log4j.appender.CONSOLE.layoutorg.apache.log4j.PatternLayoutConfiguration ComponentScan(com.hrh.log) public class Config { } public class Test { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(Config.class); context.start(); } }当上面的依赖变为 Spring Framework 4.x 时是可以打印日志dependency groupIdlog4j/groupId artifactIdlog4j/artifactId version1.2.12/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version4.3.20.RELEASE/version /dependencySpring Framework 5.x 下log4j 配合 slf4j 使用输出日志上面的pom依赖修改为dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.2.6.RELEASE/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version1.7.21/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-log4j12/artifactId version1.7.21/version /dependency5.logbacklog4j 的进化版logback 除了具备 log4j 的所有优点之外还解决了 log4j 不能使用占位符的问题。logback-classic依赖和logback.xml配置文件pom.xml dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.1.7/version /dependencylogback.xml ?xml version1.0 encodingUTF-8? configuration appender nameSTDOUT classch.qos.logback.core.ConsoleAppender layout classch.qos.logback.classic.PatternLayout Pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/Pattern /layout /appender logger namecom.hrh.log levelTRACE/ root leveldebug appender-ref refSTDOUT / /root /configurationimport org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LogBack { public static void main(String[] args) { Logger logger LoggerFactory.getLogger(logBack); logger.trace(Trace Level.); logger.debug(Debug Level.); logger.info(Info Level.); logger.warn(Warn Level.); logger.error(Error Level.); logger.info({},its OK.,Hi);//使用{}做占位符 } }6.slf4j为了方便原先直接使用日志库日志的核心功能实现 jul、log4j、log4j2、logback等输出日志的形式转换为日志门面slf4j、jcl输出的形式提供的适配器。前面的 jul、log4j、log4j2、logback 是实现日志的日志库简单日志可以使用 jul复杂日志实现可以使用 logback 或 log4j2。很多时候我们的项目是从简单到复杂一代代迭代过来的日志实现也是从简单到复杂开始我们使用jul后面系统复杂了我们需要使用 log4j2 日志框架这时候我们如何将原来的日志输出在新的日志架构下实现呢一个死板的方法是对代码一行行进行修改把之前用 jul 的日志代码全部修改成 log4j2 的日志接口。但是这种方式不仅效率低下而且做的工作都是重复性的工作实际工作中不采用该方式。这时候我们就需要一个叫做 slf4jSimple Logging Facade for Java即Java简单日志记录接口集也可以叫日志门面的东西了它是一个日志的接口规范它对用户提供了统一的日志接口使用Facade门面设计模式屏蔽了底层不同日志组件的差异和实现细节使用者无需关注底层实现的具体日志库。slf4j本身不记录日志通过绑定器绑定一个具体的日志框架来完成日志记录。即slf4j 允许最终用户在部署时插入所需的日志框架如果在项目中使用 slf4j 必须加一个依赖jar即 slf4j-api-xxx.jar。而当我们需要更换日志组件的时候我们只需要更换一个具体的日志组件 jar 包就可以了。比如说我们现在有个 app通过 slf4j 打印日志slf4j 需要通过一个绑定器slf4j-jdk14-1.8.0-beta2来绑定到我们的 jul 来输出日志。如果这时候需要在 app 中集成 spring4spring4是利用jcl来打印日志的这时候呢考虑到系统的日志统一可以使用桥接器jcl-over-slf4j将 jcl 桥接到 slf4j 来然后通过 binding 到 jul 来输出日志保证系统日志风格统一。下面是绑定类型日志门面适配器介绍绑定类型说明slf4j-log4j12-xxx.jar绑定log4j依赖log4j-xxx.jar输出log4j日志slf4j-jdk14-xxx.jar绑定jul输出jul日志slf4j-jcl-xxx.jar绑定jcl默认输出jul日志有log4j则输出log4jlogback-classic-xxx.jar绑定logback依赖logback-core-xxx.jar输出logback日志下面是桥接类型日志库适配器介绍比如左上角的第一个是将 jcl或log4j、jul 桥接到 slf4j最后统一输出为 logback 日志所以综上所述绑定器绑定最后输出的日志类型桥接器就是将指定日志类型桥接到 slf4j最后统一输出绑定器绑定的日志类型可以解释为指定日志类型输出的日志转换为绑定器绑定的日志类型应用宜采用日志桥接、适配绑定机制将 jul、log4j、jcl 等日志框架桥接到 slf4j适配性能更高的 log4j2 日志输出框架打印小贴士对于日志量大的勿输出到控制台Console下面是几个简单案例实现1slf4jjulpom.xml dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version1.7.21/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-jdk14/artifactId version1.7.21/version /dependencyimport org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Slf4jJULLog { public static void main(String[] args) { Logger logger LoggerFactory.getLogger(Slf4jJULLog.class); logger.trace(Trace Level.); logger.info(Info Level.); logger.warn(Warn Level.); logger.error(Error Level.); } }2slf4jlog4jpom.xml dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version1.7.21/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-log4j12/artifactId version1.7.21/version /dependency dependency groupIdlog4j/groupId artifactIdlog4j/artifactId version1.2.17/version /dependencylog4j.xml ?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n / /Console /Appenders Loggers Root levelinfo AppenderRef refConsole / /Root /Loggers /Configurationpublic class Slf4jLog4JLog { public static void main(String[] args) { Logger logger LoggerFactory.getLogger(Slf4jLog4JLog.class); logger.trace(Trace Level.); logger.info(Info Level.); logger.warn(Warn Level.); logger.error(Error Level.); } }注意当我们使用slf4j跟其他日志实现来搭建日志系统时可能会存在一些循环引用导致栈溢出的问题。如下案例pom.xml dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version1.7.21/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-log4j12/artifactId version1.7.21/version /dependency dependency groupIdorg.slf4j/groupId artifactIdlog4j-over-slf4j/artifactId version1.7.25/version /dependencyimport org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Slf4jLog4JLog { public static void main(String[] args) { Logger logger LoggerFactory.getLogger(Slf4jLog4JLog.class); logger.trace(Trace Level.); logger.info(Info Level.); logger.warn(Warn Level.); logger.error(Error Level.); } }上面就是一个由于循环引用导致栈溢出的错误这时因为绑定器绑定 log4j 时在 log4j 又经过桥接器接到 slf4j然后又经过绑定器依次循环最后栈溢出。这个问题在实际场景中较常见比如我们一个 app 使用 slf4j 绑定器绑定到 log4j最后通过 log4j 输出日志这时有个 jar 包 X1使用 slf4j 绑定输出到 jul然后 X2需要集成 X2X2 使用 log4j 输出日志X1 在集成 X2时使用桥接器log4j-over-slf4j保证 X1 的日志最后都是通过 jul 输出的。这时候app 集成 X1 便会出现 log4j 的桥接器和绑定器共存的情况便会出现上面栈溢出的错误这时候我们就需要修改我们的 app 了。最后来下总结三.扩展log4j2更多详情参考log4j2 的使用【超详细图文】logback更多详情参考logback的使用和logback.xml详解