日志门面框架原理分享

常见日志框架: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类的路径来发现实现类


日志门面框架原理分享_第1张图片
image.png

当发现有多个实现类 则报异常


image.png

然后通过StaticLoggerBinder.getSingleton() 来进行初始化.

思考:当项目中有多个StaticLoggerBinder时 会加载哪一个?

由java的类加载机制可知,类加载器会按照一定的顺序逐个扫描jar包目录并加载进来.

所以先被类加载器扫描到的那个会被加载.

加载顺序

  1. $java_home/lib 目录下的java核心api

  2. $java_home/lib/ext 目录下的java扩展jar包

  3. java -classpath/-Djava.class.path所指的目录下的类与jar包

  4. $CATALINA_HOME/common目录下按照文件夹的顺序从上往下依次加载

  5. $CATALINA_HOME/server目录下按照文件夹的顺序从上往下依次加载

  6. $CATALINA_BASE/shared目录下按照文件夹的顺序从上往下依次加载

  7. 项目/WEB-INF/classes下的class文件

  8. 项目/WEB-INF/lib下的jar文件

logBean,logger无缝结合

logger:本地日志,不适用于分布式环境,主要是链路日志方面
logBean:分布式日志,用于分布式环境,主要带有链路信息

问题:

  • 项目需要logger和logBean同时使用,会有重复代码


    日志门面框架原理分享_第2张图片
    image.png
  • 第三方框架打印的日志,无法加入到分布式日志中
  • logbean作为日志,代码侵入性高

需求:

  1. logger和logbean结合,统一日志入口
  2. logbean降低代码侵入性
  3. 无缝替换第三方框架中的日志,根据需求加入到分布式日志中
实现思路
思路 需求符合度 可行性 不足 综合
讲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对象了.完美实现了三个需求

日志门面框架原理分享_第3张图片
image.png

参考:
日志框架原理解析

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限制

你可能感兴趣的:(日志门面框架原理分享)