Java中日志生态是比较乱的,因此在项目中如何使用日志成了一个开发者必须考虑的问题,本文针对工作中日志包使用做一个总结。
日志包虽然很多,但大体上分为三类
那么最佳实践自然是一套门面,一套实现,其他都为桥接,如下图所示,这种方式下结构非常清晰,且日志实现类可以随时更换,不会影响到现有应用,目前主流组合有 slf4j + logback + 各种桥接,slf4j + log4j2 + 各种桥接,配置时可以作为参考。
也因此日志该如何配置即变成了如何保证应用中有且仅有一套门面,一套实现日志框架。
在分析之前,我们大致可以想象到,门面日志相当于定义了一套输出日志的标准API,桥接类相当于复写了对应实现类,然后在内部将对应日志行为转接到slf4j,接下来以slf4j+log4j2为例,描述这一流程。
如下代码所示,在slf4j中 org.slf4j.LoggerFactory#bind
方法会使用 StaticLoggerBinder.getSingleton()
完成实现类日志绑定,而 StaticLoggerBinder
由对应实现类日志提供,比如使用log4j2实现时,则由 log4j-slf4j-impl
jar提供该类。
Set<URL> staticLoggerBinderPathSet = null; // 检查是否存在多个实现类,存在则报警 if (!isAndroid()) { staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); } // 完成日志实现类绑定 StaticLoggerBinder.getSingleton(); // 置为绑定成功状态 INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION; reportActualBinding(staticLoggerBinderPathSet);
桥接的目地是获取到 ILoggerFactory
,由其提供对应日志类 Logger
,完成绑定输出。
不同的类桥接方式不太一样,以JUL为例,其桥接包 jul-to-slf4j
提供了 SLF4JBridgeHandler
类,该类继承了 java.util.logging.Handler
,针对JUL日志的日志输出会转到 org.slf4j.Logger
输出,从而实现了桥接。另外 log4j-over-slf4j
的实现方式,则是完全复写log4j库,提供一样的Class接口,但是内部日志输出使用的是 org.slf4j.Logger
,从而完成了桥接。
原理实际上很简单,这其中容易配错的就是桥接方向,比如同时引入了 jcl-over-slf4j 与 slf4j-jcl,前者是桥接jcl到slf4j,后者是桥接slf4j到jcl,那么必然 死循环 ,造成内存溢出。
按照一套门面,一套实现,其他都为桥接的标准,日志配置可以分为三个步骤:第一步,引入门面日志框架;第二步,排除多余的日志实现类框架;第三步,引入相关桥接包,排除多余桥接包。按照简单的三个步骤策略,可以轻松配置日志,另外在整个过程中可以画图辅助,这样能够快速帮助你定位到问题所在。
解决日志配置问题后,管理也是个大问题,随着应用迭代增多,如果不加以控制,日志文件实际上会越来越多,因此收口到一个工具类是非常必要的选择。比如针对报警事件,可以收口到一个alarm.log的日志,使用不同的marker区分,针对监控日志可以收口到monitor.log,使用marker区分,错误日志则全部收口到error.log,防止多处打印。
工具类实现,首推策略枚举模式,管理方便,调用简单,还便于在日志上增加各种属性配置,如清单二代码实例所示,该LogUtils不但提供了枚举调用方式,还提供了静态方法调用方式,方便外部存在 logger对象时调用,灵活性极其自由。
public enum LogUtils { /** * 通用监控日志 */ MONITOR(LoggerFactory.getLogger("monitor")), /** * 通用报警日志 */ ALARM(LoggerFactory.getLogger("alarm")), ; public final Logger logger; LogUtils(Logger logger) { this.logger = logger; } /** * 输出info日志 * @param msgTemplate 日志模板 * @param params 日志参数 */ public void info(String msgTemplate, Object... params) { if (logger.isInfoEnabled()) { logger.info(msgTemplate, params); } } /** * 输出info日志 * @param msgTemplate 日志模板 * @param params 日志参数 */ public void info(Marker marker, String msgTemplate, Object... params) { if (logger.isInfoEnabled()) { logger.info(msgTemplate, params); } } /** * 输出error日志 * @param msgTemplate 日志模板 * @param params 日志参数 */ public void error(String msgTemplate, Object... params) { if (logger.isErrorEnabled()) { logger.error(msgTemplate, params); } } /** * 输出info日志 * @param logger 日志文件 * @param msgTemplate 日志模板 * @param params 日志参数 */ public static void info(Logger logger, String msgTemplate, Object... params) { if (logger.isInfoEnabled()) { logger.info(msgTemplate, params); } } /** * 输出error日志 * @param logger 日志文件 * @param msgTemplate 日志模板 * @param params 日志参数 */ public static void error(Logger logger, String msgTemplate, Object... params) { if (logger.isErrorEnabled()) { logger.error(msgTemplate, params); } } 。。。。。等方法 }
Java -- 字符串常量池介绍