常见日志框架:log4j,logback,jcl(common-longging),jul(jdklog),slf4j
日志框架主要分为两类
- 实现:log4j,logback,jul
- 门面:jcl,slf4j
本文目标:
- 分享slf4j,jcl的桥接原理
- 分享logBean和logger无缝结合的思路
涉及知识:
- 类加载机制
- spi
slf4j桥接原理
SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统.按照官方的说法,SLF4J是一个用于日志系统的Facade,允许最终用户在部署其应用时使用其所希望的日志System.
#使用方法
Logger logger = LoggerFactory.getLogger(CtrTask.class);
logger.info("本次投递数量:" + users.size());
#调用过程
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
//查找实现类
performInitialization();
}
...
return StaticLoggerBinder.getSingleton().getLoggerFactory();
...
}
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
private static Set findPossibleStaticLoggerBinderPathSet() {
...
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
...
}
通过查找org/slf4j/impl/StaticLoggerBinder.class类的路径来发现实现类
当发现有多个实现类 则报异常
然后通过StaticLoggerBinder.getSingleton() 来进行初始化.
思考:当项目中有多个StaticLoggerBinder时 会加载哪一个?
由java的类加载机制可知,类加载器会按照一定的顺序逐个扫描jar包目录并加载进来.
所以先被类加载器扫描到的那个会被加载.
加载顺序:
$java_home/lib 目录下的java核心api
$java_home/lib/ext 目录下的java扩展jar包
java -classpath/-Djava.class.path所指的目录下的类与jar包
$CATALINA_HOME/common目录下按照文件夹的顺序从上往下依次加载
$CATALINA_HOME/server目录下按照文件夹的顺序从上往下依次加载
$CATALINA_BASE/shared目录下按照文件夹的顺序从上往下依次加载
项目/WEB-INF/classes下的class文件
-
项目/WEB-INF/lib下的jar文件
logBean,logger无缝结合
logger:本地日志,不适用于分布式环境,主要是链路日志方面
logBean:分布式日志,用于分布式环境,主要带有链路信息
问题:
-
项目需要logger和logBean同时使用,会有重复代码
- 第三方框架打印的日志,无法加入到分布式日志中
- logbean作为日志,代码侵入性高
需求:
- logger和logbean结合,统一日志入口
- logbean降低代码侵入性
- 无缝替换第三方框架中的日志,根据需求加入到分布式日志中
实现思路
思路 | 需求符合度 | 可行性 | 不足 | 综合 |
---|---|---|---|---|
讲logger集成到logbean中 | 1 | 无法满足2 3需求 | 不可行 | |
自定义apppender | 1 2 3 | 通过logback的appender拓展 | appender拓展性不高 无法获得上下文信息 | 可行 |
将logbean集成到logger | 12 | 通过实现Logger接口 聚合logger和logbean | 1.对于第三方框架的日志 无法直接替换 2.需要替换项目中的LogFactory为自定义类型 | 可行 |
综合上述 决定使用方案3 对于需求3考虑后续实现
方式三
通过实现Logger接口,内部聚合logger和logbean,对外统一使用logger的原生api.满足需求1,2.
public class CustomLogger implements LocationAwareLogger {
private Logger logger;
//提供getLogger方法获取logger
public static LoggerFacade getLogger(Class clazz) {
LoggerFacade loggerFacade = new LoggerFacade();
loggerFacade.logger = LoggerFactory.getLogger(clazz);
return loggerFacade;
}
...
//打印本地日志的同时 输出到logbean中
@Override
public void warn(String msg) {
logger.warn(msg);
appendExtra(msg, Level.WARN);
}
...
void appendExtra(String str, Level level) {
String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
ThreadContext threadContext = ContextContainer.retrieveServiceContext();
if (threadContext != null) {
LogBean logBean = threadContext.getLogBean();
if (logBean != null) {
logBean.getInner().getExtra().add(date + " " + level.toString() + " " + simpleName(getName()) + " -" +
" " + str);
}
}
}
}
需求3的考虑实现
问题 | 思路 |
---|---|
第三方框架的使用多种日志框架 | 通过slf4j提供的转接包 全部转接到slf4j中 |
在不修改第三方框架源码的情况下 将日志输出到logbean中 | 替换slf4j的实现,改为CustomerLogger,内部调用logback实现本地日志输出 |
slf4j转接原理参考
由之前的slf4j桥接原理可知,slf4j通过LoggerFactory来获取Logger具体实现.
所以我们可以通过LoggerFactory来下手.上代码
@Override
public ILoggerFactory getLoggerFactory() {
if (!initialized) {
return defaultLoggerContext;
}
if (contextSelectorBinder.getContextSelector() == null) {
throw new IllegalStateException(
"contextSelector cannot be null. See also " + NULL_CS_URL);
}
LoggerContext loggerContext = contextSelectorBinder.getContextSelector().getLoggerContext();
return CustomLoggerFactory.getInstance(loggerContext);
}
public class CustomLoggerFactory implements ILoggerFactory {
private static CustomLoggerFactory customLoggerFactory;
public static CustomLoggerFactory getInstance(LoggerContext loggerContext) {
if (customLoggerFactory == null) {
customLoggerFactory = new CustomLoggerFactory(loggerContext);
}
return customLoggerFactory;
}
//logback的LoggerFactory实现
private LoggerContext loggerContext;
public CustomLoggerFactory(LoggerContext loggerContext) {
this.loggerContext = loggerContext;
}
//返回CustomLogger
@Override
public Logger getLogger(String name) {
ch.qos.logback.classic.Logger logger = loggerContext.getLogger(name);
return CustomLogger.getLogger(logger);
}
public LoggerContext getLoggerContext() {
return loggerContext;
}
}
经由以上替换后,项目中通过LoggerFactory获取的到logger对象 就替换成了CustomLogger对象了.完美实现了三个需求
参考:
日志框架原理解析
jcl的桥接原理
#org.apache.commons.logging.LogFactory
public static Log getLog(Class clazz) throws LogConfigurationException {
return getFactory().getInstance(clazz);
}
public static LogFactory getFactory() throws LogConfigurationException {
ClassLoader contextClassLoader = getContextClassLoaderInternal();
...
//读取配置文件commons-logging.properties
Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
...
//1.根据环境变量org.apache.commons.logging.LogFactory作为真正的LogFactory实现
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
...
//2.根据spi获取依赖jar包中的LogFactory实现 META-INF/services/org.apache.commons.logging.LogFactory
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
...
//3.获取配置文件中的LogFactory
String factoryClass = props.getProperty(FACTORY_PROPERTY);
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
...
return factory;
}
拓展方式 | 优点 | 缺点 | |
---|---|---|---|
slf4j | 类加载机制 | 编译期查找,利用类加载机制,确保只会找到唯一实现类 | 对于多个实现的优先选择不够灵活 |
jcl | 环境变量,spi,配置文件 | 运行期查找,支持多种拓展方式,灵活性更大 | 在osgi中受classloader限制 |
对比
拓展方式 | 优点 | 缺点 | |
---|---|---|---|
slf4j | 类加载机制 | 编译期查找,利用类加载机制,确保只会找到唯一实现类 | 对于多个实现的优先选择不够灵活 |
jcl | 环境变量,spi,配置文件 | 运行期查找,支持多种拓展方式,灵活性更大 | 在osgi中受classloader限制 |