基于slf4j和common-logging的日志框架

前言

用了这么长时间的日志框架,有时候用得很顺利,有的时候又很迷惑,比如在项目中引入一个新的jar包,会出现日志框架冲突的时候,要不就是日志莫名的打印不出来,要不就是项目启动异常。今天花点时间来整理,梳理一下这些日志框架之间的关系。

日志框架概览

目前使用比较广泛的日志接口是common-logging和slf4j,common-logging是apache提供的一个通用的日志接口,slf4j是log4j的作者创作的日志接口。

小故事:apache说服log4j以及其他的日志来按照commons-logging的标准编写,但是由于commons-logging的类加载有点问题,log4j的作者就创作了slf4j。至于到底使用哪个,由用户来决定。

所为日志接口,就是common-logging或者slf4j只定义了日志的接口,而日志输出的具体实现,交给实现了日志接口的框架,比如log4j、logback等。

slf4j日志接口

slf4j全称为Simple Logging Facade for JAVA,java简单日志门面,这是从其他的博主文章中摘录出来的,我借用过来记录一下:
基于slf4j和common-logging的日志框架_第1张图片
从这张图中就能看的很清楚,不同的日志框架(包括:log4j、logback等)的基础是slf4j,也就是说slf4j是一个日志门面,slf4j只是提供了一套标准的日志接口,没有具体的实现,它只提供了一个slf4j-api-*.jar包,负责日志实现的工作交给了各个日志框架。各个日志框架在实现的时候,又有点区别,一个明显的区别就是是否需要引入一个连接层jar包,比如log4j日志框架,需要引入一个连接层slf4j-log412.jar,而像logback日志框架,就不需要引入一个连接层jar,引入和不引入连接层实质上的区别是什么呢?我们后面在分析。
参照这个图,我们就清楚的知道,如果在项目中使用log4j作为日志框架,就需要引入3个jar包

<dependency>
	<groupId>log4jgroupId>
	<artifactId>log4jartifactId>
	<version>1.2.17version>
dependency>
<dependency>
	<groupId>org.slf4jgroupId>
	<artifactId>slf4j-apiartifactId>
	<version>1.7.25version>
dependency>
<dependency>
	<groupId>org.slf4jgroupId>
	<artifactId>slf4j-log4j12artifactId>
	<version>1.7.25version>
dependency>

如果是log4j2日志框架,就回引入不同的jar,毕竟log4j2和log4j架构还是有区别的。
如果是使用logback作为日志框架,需要引入下面的jar包,没有什么中间的连接层jar包。

<dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-apiartifactId>
    <version>1.7.7version>
dependency>
<dependency>
    <groupId>ch.qos.logbackgroupId>
    <artifactId>logback-coreartifactId>
    <version>1.1.7version>
dependency>
<dependency>
    <groupId>ch.qos.logbackgroupId>
    <artifactId>logback-classicartifactId>
    <version>1.1.7version>
dependency>

使用slf4j的常见代码

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestLog {
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	public static void main(String[] s){
		logger.info("test");
	}
}

slf4j查找日志实现框架的过程,以logback日志框架为例进行简单的分析:
slf4j正式加载日志框架的动作是在类实例化完成的最后阶段,也就是给下面这条语句赋值的时候:

private Logger logger = LoggerFactory.getLogger(this.getClass());

LoggerFactory是slf4j-api 包中的类,在这个LoggerFactory里有几处关键的地方:

package org.slf4j;
public final class LoggerFactory {
    private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
    
	public static Logger getLogger(String name) {
		//③创建Logger工厂
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        //⑤创建Logger
        return iLoggerFactory.getLogger(name);
    }
    private static Set findPossibleStaticLoggerBinderPathSet() {
        LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet();
        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration paths;
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
            	//①加载指定的binder
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            //省略代码
        } catch (IOException var4) {
            Util.report("Error getting resources from path", var4);
        }
        return staticLoggerBinderPathSet;
    }

    public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == 0) {
            INITIALIZATION_STATE = 1;
            performInitialization();
        }
        switch(INITIALIZATION_STATE) {
        case 1:
            return TEMP_FACTORY;
        case 2:
            throw new IllegalStateException("org.slf4j.LoggerFactory could not be successfully initialized. See also http://www.slf4j.org/codes.html#unsuccessfulInit");
        case 3:
        	//②加载日志工厂类
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        case 4:
            return NOP_FALLBACK_FACTORY;
        default:
            throw new IllegalStateException("Unreachable code");
        }
    }
}

上面3个方法的调用顺序是getLogger()->getILoggerFactory()->findPossibleStaticLoggerBinderPathSet()
先看看①处代码,加载了一个指定的Binder类StaticLoggerBinder.class,这个类有两个特点,

  • 第一个特点是,这个类显然是写死在代码里面的,
  • 第二个特点是,这个类不是slf4j-api包里面的类,

那这个类在哪儿呢?这就得在logback的jar包中去发现了,看看下面这张截图
基于slf4j和common-logging的日志框架_第2张图片
这就很明显了,这个StaticLoggerBinder类成了slf4j和logback日志框架的一个桥梁,这个Binder类做了什么事情,不是现在我们关注的重点,
接着看②处代码,StaticLoggerBinder.getSingleton()获取了StaticLoggerBinder的一个实现单例,getLoggerFactory做了什么事情,看看代码

package org.slf4j.impl;

public class StaticLoggerBinder implements LoggerFactoryBinder {
    private LoggerContext defaultLoggerContext = new LoggerContext();
	//④返回Logger工厂类
    public ILoggerFactory getLoggerFactory() {
        if (!this.initialized) {
            return this.defaultLoggerContext;
        } else if (this.contextSelectorBinder.getContextSelector() == null) {
            throw new IllegalStateException("contextSelector cannot be null. See also http://logback.qos.ch/codes.html#null_CS");
        } else {
            return this.contextSelectorBinder.getContextSelector().getLoggerContext();
        }
    }
    void init() {
        try {
            try {
            	//⑥加载日志的配置文件
                (new ContextInitializer(this.defaultLoggerContext)).autoConfig();
            } catch (JoranException var2) {
                Util.report("Failed to auto configure default logger context", var2);
            }
            //省略代码
        } catch (Throwable var3) {
            Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", var3);
        }

    }
}

④代码说明:StaticLoggerBinder类中getLoggerFactory方法,就是返回了一个LoggerContext,这个是什么东西,接着看代码

package ch.qos.logback.classic;
import org.slf4j.ILoggerFactory;
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle {
}

看到这个类的定义就知道了,LoggerContext是处于logback-classic.jar包中的一个ILoggerFactory的实现类,ILoggerFactory是slf4j-api中定义的接口
基于slf4j和common-logging的日志框架_第3张图片
回头看看上面③处的代码,LoggerFactory就是这样被创建出来的,⑤处的代码就是调用ch.qos.logback.classic.LoggerContext.getLogger()方法了,这就是logback要完成的事情了。

顺便说一下日志配置文件logback.xml在哪儿被加载的?代码⑥处,也是在StaticLoggerBinder类中完成的,ContextInitializer().autoConfig()加载配置文件logback.xml

从上面的分析看得出StaticLoggerBinder至少做了两件重要的事情:

  1. 创建ILoggerFactory的实现类LoggerContext
  2. 加载日志框架的配置文件logback.xml

SLF4J 会在编译时会绑定org.slf4j.impl.StaticLoggerBinder,该类里面实现对具体日志方案的绑定接入。任何一种基于slf4j 的实现都要有一个这个类。
log4j日志框架也是一样的道理,它也实现了这个StaticLoggerBinder类,从上面的我们讲到log4j需要引入的jar包就知道,引入了slf4j-log4j12这个包,这个包是干什么的呢?我们不在此处分析,看图:
基于slf4j和common-logging的日志框架_第4张图片
虽然不知道slf4j-log4j12这个包是干什么的,但是现在至少分析出来可以得出结论,没有这个包,StaticLoggerBinder类就缺失,log4j就不能和slf4j集成。

common-logging日志接口

使用common-logging的常见代码

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
public class Test {
	private Log logger = LogFactory.getLog(this.getClass());
	public static void main(String[] s){
		logger.info("test");
	}
}

动态查找原理:
Log 是一个接口声明。LogFactory 的内部会去装载具体的日志系统,并获得实现该Log 接口的实现类。LogFactory 内部装载日志系统的流程如下:

  • 首先,寻找org.apache.commons.logging.LogFactory 属性配置。
  • 否则,利用JDK1.3 开始提供的service 发现机制,会扫描classpah 下的META-INF/services/org.apache.commons.logging.LogFactory文件,若找到则装载里面的配置,使用里面的配置。
  • 否则,从Classpath 里寻找commons-logging.properties ,找到则根据里面的配置加载。
  • 否则,使用默认的配置:如果能找到Log4j 则默认使用log4j 实现,如果没有则使用JDK14Logger 实现,再没有则使用commons-logging 内部提供的SimpleLog 实现。

从上述加载流程来看,只要引入了log4j 并在classpath 配置了log4j.xml ,则commons-logging 就会使log4j 使用正常,而代码里不需要依赖任何log4j 的代码。

slf4j与common-logging比较

  1. common-logging通过动态查找的机制,在程序运行时自动找出真正使用的日志库。由于它使用了ClassLoader寻找和载入底层的日志库, 导致了象OSGI这样的框架无法正常工作,因为OSGI的不同的插件使用自己的ClassLoader。 OSGI的这种机制保证了插件互相独立,然而却使Apache Common-Logging无法工作。
  2. slf4j在编译时静态绑定真正的Log库,因此可以再OSGI中使用。另外,SLF4J 支持参数化的log字符串,避免了之前为了减少字符串拼接的性能损耗而不得不写的if(logger.isDebugEnable()),现在你可以直接写:logger.debug(“current user is: {}”, user)。拼装消息被推迟到了它能够确定是不是要显示这条消息的时候,但是获取参数的代价并没有幸免。

日志框架切换

现实情况,假如我们现在的项目,正在使用的是slf4j日志接口,日志实现框架是logback,运行一段时间之后,引入了一个其他组件,这个组件使用的是common-logging作为日志接口,日志实现框架是log4j,一个项目中不能使用两套日志框架,必须得保留一个,logback作为项目目前使用的框架,优先考虑保留,自然就得把log4j排除掉,这个时候怎么实现呢?如果强行把common-logging的jar包和log4j的jar包排除,程序启动肯定是会报错的。
基于slf4j和common-logging的日志框架_第5张图片
解决的方案需要引入一种桥接模式,关键在于一个jar包jcl-over-slf4j.jar,就是commons-logging通过jcl-over-slf4j来选择slf4j作为底层的日志输出对象,而slf4j又选择logback来作为底层的日志输出对象。几个关键步骤:

  1. 引入jcl-over-slf4j的jar包
  2. 去掉commons-logging的jar包,使用jcl-over-slf4j将commons-logging的底层日志输出切换到slf4j
  3. 去掉log4j的jar包
    jcl-over-slf4j.jar里面到底做了哪些事情,首先看看包里面有哪些类?
    基于slf4j和common-logging的日志框架_第6张图片
  • 第一个需要注意的地方,路径org.apache.commons.logging.LogFactory,org.apache.commons.logging.Log和和common-logging.jar包的路径是一样的。这就是为什么我们去掉了commons-logging的jar包之后,程序不会报错的原因。程序依然是可以正常编译的,对应程序来说,我们删除了commons-logging.jar包是无感的。
  • 第二个需要注意的地方,Log的实现类换成了SLF4JLog,SLF4JLog implements Log,SLF4JLogFactory继承了LogFactory

看看整体的类图

基于slf4j和common-logging的日志框架_第7张图片 基于slf4j和common-logging的日志框架_第8张图片

再次回顾一下我们的日志使用程序,使用common-logging的常见代码

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
public class LogTest {
	private Log logger = LogFactory.getLog(this.getClass());
	public static void main(String[] s){
		logger.info("test");
	}
}

LogFactory.getLog()实质上是调用到了SLF4JLogFactory.getInstance(),getInstance方法返回的是Log的实现类SLF4JLog,所以引用变量logger实际指向的实例是SLF4JLog,当我们执行logger.info(“test”)的时候,被SLF4JLog转发到了logback日志框架去输出日志。
整个大致类图:
基于slf4j和common-logging的日志框架_第9张图片
其他的日志框架的装换,用到了相似的方法,不再一一分析。别的日志接口,转成slf4j的方法,这张图总结得很好,借用过来记录一下。
基于slf4j和common-logging的日志框架_第10张图片

参考:
Java日志框架SLF4J和log4j以及logback的联系和区别
java日志组件介绍
SLF4j 和 common-logging

你可能感兴趣的:(散落笔记)