日志组件是每个应用系统必备的组件,日志组件有多种开源实现,不同的项目可能选择了不同的日志组件。
有这么一个场景,你的应用系统中可能采用了多种开源框架或者组件或者SDK,如果这些第三方组件中强依赖了某种日志组件,和你现有系统中的日志组件不一致,而你为了用这个第三方的组件,只能被迫转换日志组件或者使用两套日志组件同时工作,这就不太优雅了。
一般的开源框架也考虑到了这个情况,今天我们来看下Dubbo框架的日志体系是怎么实现的,以后你在写框架或者SDK时就知道怎么做了。
Dubbo对日志体系的抽象
Dubbo对应用系统日志的适配
Dubbo所有的日志实现都是通过LoggerFactory类来获取的,我们不妨看下它的逻辑:
public class LoggerFactory {
//用来存放所有Logger实例的,每个类一个,key是类全称
//FailsafeLogger是一个增强的Logger,增加了一个disable判断
private static final ConcurrentMap LOGGERS = new ConcurrentHashMap<>();
//当前正在使用的日志组件适配器
private static volatile LoggerAdapter LOGGER_ADAPTER;
// search common-used logging frameworks
static {
//这句话表明了通过设置哪个变量来决定使用哪个日志框架
String logger = System.getProperty("dubbo.application.logger", "");
switch (logger) {
case "slf4j":
setLoggerAdapter(new Slf4jLoggerAdapter());
break;
case "jcl":
setLoggerAdapter(new JclLoggerAdapter());
break;
case "log4j":
setLoggerAdapter(new Log4jLoggerAdapter());
break;
case "jdk":
setLoggerAdapter(new JdkLoggerAdapter());
break;
case "log4j2":
setLoggerAdapter(new Log4j2LoggerAdapter());
break;
default:
//没有指定的情况下,按如下顺序遍历加载来初始化,第一个成功的是谁就使用谁
List> candidates = Arrays.asList(
Log4jLoggerAdapter.class,
Slf4jLoggerAdapter.class,
Log4j2LoggerAdapter.class,
JclLoggerAdapter.class,
JdkLoggerAdapter.class
);
for (Class extends LoggerAdapter> clazz : candidates) {
try {
setLoggerAdapter(clazz.newInstance());
break;
} catch (Throwable ignored) {
}
}
}
}
private LoggerFactory() {
}
//这个方法可以改变 适配器,加载你自定义的日志框架
public static void setLoggerAdapter(frameworkModel frameworkModel, String loggerAdapter) {
if (loggerAdapter != null && loggerAdapter.length() > 0) {
setLoggerAdapter(frameworkModel.getExtensionLoader(LoggerAdapter.class).getExtension(loggerAdapter));
}
}
//改变当前的日志框架
public static void setLoggerAdapter(LoggerAdapter loggerAdapter) {
if (loggerAdapter != null) {
if (loggerAdapter == LOGGER_ADAPTER) {
return;
}
loggerAdapter.getLogger(LoggerFactory.class.getName());
LoggerFactory.LOGGER_ADAPTER = loggerAdapter;
for (Map.Entry entry : LOGGERS.entrySet()) {
//变更所有已经缓存的Logger实例
entry.getValue().setLogger(LOGGER_ADAPTER.getLogger(entry.getKey()));
}
}
}
//获取Logger实例并缓存
public static Logger getLogger(Class> key) {
return LOGGERS.computeIfAbsent(key.getName(), name -> new FailsafeLogger(LOGGER_ADAPTER.getLogger(name)));
}
//获取Logger实例并缓存
public static Logger getLogger(String key) {
return LOGGERS.computeIfAbsent(key, k -> new FailsafeLogger(LOGGER_ADAPTER.getLogger(k)));
}
//获取日志级别
public static Level getLevel() {
return LOGGER_ADAPTER.getLevel();
}
//设置日志级别
public static void setLevel(Level level) {
LOGGER_ADAPTER.setLevel(level);
}
public static File getFile() {
return LOGGER_ADAPTER.getFile();
}
}
可以看到日志组件的加载逻辑是这样的:
1)读取变量dubbo.application.logger决定使用哪个日志框架
2)如果没有设置或者不在预制列表中则按Log4j、Slf4j、Log4j2、Jcl、JdkLogger的顺序来加载实例化适配器,第一个成功的即为要使用的日志框架,所以如果你的系统中同时带了 Log4j、Slf4j的jar的话,它会采用Log4j,它的优先级更高。
3)如果你自定义了日志框架,则通过方法setLoggerAdapter(frameworkModel frameworkModel, String loggerAdapter)来改变当前的适配器,并且会把所有已缓存的Logger实例重新替换一遍
4)通过public static void setLevel(Level level)方法可以知道,Dubbo预留了不重启服务随时改变日志级别的接口,但是它只是上层抽象,到底能不能生效还需要看适配器的行为以及日志框架实现方是否预留了接口,通过查看源码发现情况是这样的:
Dubbo日志体系的自定义扩展
上面说到了自定义日志组件,Dubbo只适配了最常用的日志组件,万一你使用了自己开发的日志组件或者一个小众的日志组件怎么集成进来让Dubbo也使用你的日志组件呢?这就用到了Dubbo的SPI机制了。
下面是Dubbo内部自带的扩展:
我们在外部扩展的流程如下:
1)在你的工程的meta-INF/dubbo 目录下新建一个同样的
org.apache.dubbo.common.logger.LoggerAdapter文件
2)内容模仿上面的添加即可
myLog=com.x.xx.xxx.MyLoggerAdapter
3)MyLoggerAdapter实现
org.apache.dubbo.common.logger.LoggerAdapter接口即可
4)如何生效?
将属性dubbo.application.logger设置为myLog:
System.setProperty("dubbo.application.logger","myLog");
Dubbo日志体系中的设计模式
1)适配器模式,这应该是典型的适配器模式应用了,通过适配器将已存在的第三方日志框架接口适配到Dubbo日志体系中来。
2)装饰器模式
3)工厂方法模式
LoggerFactory通过名字也可以看出来
总结
写给第三方使用的SDK时最好也可以做个类似的抽象与适配,原来你的SDK也可以高大尚。



