IT语言工具集Log日志

Java日志体系(slf4j)

2017-05-21  本文已影响751人  贾博岩

3 slf4j

3.1 简介

与commons-logging相同,slf4j也是一个通用的日志接口,在程序中与其他日志框架结合使用,并对外提供服务。

Simple Logging Facade for Java简称 slf4j,Java简单日志门面系统。在我们的代码中,不需要显式指定具体日志框架(例如:java.util.logging、logback、log4j),而是使用slf4j的API来记录日志便可,最终日志的格式、记录级别、输出方式等通过具体日志框架的配置来实现,因此可以在应用中灵活切换日志系统。

如果你对上面所说的,仍然不太理解。那么,简单的说slf4j可以理解为JDBC,都是提供接口服务,只不过比JDBC更为直观、简单些。在程序中,JDBC需要单独指定具体的数据库实现(例如:mysql),而slf4j并不需要。

接下来,我们讲解下关于slf4j具体的使用。

3.2 slf4j结构

上面的截图,展示的是slf4j搭配log4j使用。

Logger:slf4j日志接口类,提供了trace < debug < info < warn < error这5个级别对应的方法,主要提供了占位符{}的日志打印方式;

Log4jLoggerAdapter:Logger适配器,主要对org.apache.log4j.Logger对象的封装,占位符{}日志打印的方式在此类中实现;

LoggerFactory:日志工厂类,获取实际的日志工厂类,获取相应的日志实现对象;

lLoggerFactory:底层日志框架中日志工厂的中介,再其实现类中,通过底层日志框架中的日志工厂获取对应的日志对象;

StaticLoggerBinder:静态日志对象绑定,在编译期确定底层日志框架,获取实际的日志工厂,也就是lLoggerFactory的实现类;

3.2 使用

同为Java日志接口框架,相对于commons-logging来说,slf4j的使用有点特殊。

在第一篇的文章中,笔者介绍了commons-logging的使用,对于commons-logging来说,无需在pom.xml文件中单独引入日志实现框架,便可进行日志打印。但是,slf4j并不支持此功能,必须在pom.xml中单独引入底层日志实现。

搭配log4j使用:
首先,需要在pom.xml文件中添加依赖:

//slf4j:
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.20</version>
</dependency>

//slf4j-log4j:
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</version>
</dependency>

//log4j:
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

声明测试代码:

public class slf4j_log4jDemo {

    Logger logger = LoggerFactory.getLogger(slf4j_log4jDemo.class);

    @Test
    public void test() throws IOException {
        logger.error("Error Message!");
        logger.warn("Warn Message!");
        logger.info("Info Message!{}","你好");
        logger.debug("Debug Message!");
        logger.trace("Trace Message!");
    }
}

接下来,在classpath下定义配置文件:log4j.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>
    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
        <param name="Target" value="System.out" />
        <param name="ImmediateFlush" value="true"/>
        <param name="encoding" value="UTF-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %t %-5p (%c:%L) - %m%n"/>
        </layout>
    </appender>
    <root>
        <priority value="debug" />
        <appender-ref ref="CONSOLE" />
    </root>
</log4j:configuration>

对于slf4j来说,它只提供了一个核心模块--slf4j-api,这个模块下只有日志接口,没有具体的实现,所以在实际开发总需要单独添加底层日志实现。但是,这些底层日志类实际上跟slf4j并没有任何关系,因此slf4j又通过增加一层日志中间层来转换相应的实现,例如上文中的slf4j-log4j12。

上图,是官方文档中slf4j与其他日志框架相结合的使用情况,具体总结如下:

logback:logback-classic 、logback-core

java.util.logging.Logging:slf4j-jdk14

commons-logging:jcl-over-slf4j

其中,commons-logging比较特殊。由于commons-logging诞生的比较早,一些年限久远的系统大体上都使用了commons-logging和log4j的日志框架组合,大名鼎鼎的spring框架也依然在使用commons-logging框架。那么,此时你的新系统如果想使用slf4j该如何处理?

这会,就需要引入jcl-over-slf4j.jar包了,它会将commons-logging的“骗入”到slf4j中来,实现日志框架结合;

3.3 源码分析

以下源码基于slf4j-1.7.20、slf4j-log4j12-1.7.12和log4j-1.2.17(使用slf4j和log4j结合):

org.slf4j.LoggerFactory类:

public final class LoggerFactory {
 
    static final int UNINITIALIZED = 0;
    static final int ONGOING_INITIALIZATION = 1;
    static final int FAILED_INITIALIZATION = 2;
    static final int SUCCESSFUL_INITIALIZATION = 3;
    static final int NOP_FALLBACK_INITIALIZATION = 4;

    private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
    
    //初始化状态,默认为0;
    static int INITIALIZATION_STATE = UNINITIALIZED;

    //获取日志对象:
    public static Logger getLogger(Class<?> clazz) {
        //获取日志对象:
        Logger logger = getLogger(clazz.getName());
        ......
    }
    
     //获取日志对象,分为两个阶段:
    public static Logger getLogger(String name) {
        
        //获取日志工厂:实际为Log4jLoggerFactory:
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        
        //通过Log4jLoggerFactory获取日志对象:
        return iLoggerFactory.getLogger(name);
    }


    //获取日志工厂:
    public static ILoggerFactory getILoggerFactory() {
        
        //判断初始化状态:默认为0
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            //将初始化状态至为1:
            INITIALIZATION_STATE = ONGOING_INITIALIZATION;
            
            //slf4j初始化操作:
            performInitialization();
        }
        
        //完成初始化后,判断初始化结果:
        switch (INITIALIZATION_STATE) {
            case SUCCESSFUL_INITIALIZATION:
                //通过StaticLoggerBinder单例对象,创建LoggerFactory实例:实际为Log4jLoggerFactory
                return StaticLoggerBinder.getSingleton().getLoggerFactory();
            case NOP_FALLBACK_INITIALIZATION:
                return NOP_FALLBACK_FACTORY;
            case FAILED_INITIALIZATION:
                throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
            case ONGOING_INITIALIZATION:
                return TEMP_FACTORY;
        }
        throw new IllegalStateException("Unreachable code");
    }

    //slf4j初始化流程:
    private final static void performInitialization() {
        
        //静态绑定,获取StaticLoggerBinder对象;
        bind();

        //判断初始化状态:如果初始化成功,则进行版本检查;
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }

    //静态绑定操作:找到与slf4j相结合的日志框架;
    private final static void bind() {
        try {
            //在类路径下,查找org.slf4j.impl.StaticLoggerBinder类:
            Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            
            //遍历Set集合,并将其中StaticLoggerBinder类的路径打印出来:
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            
            //创建StaticLoggerBinder的对象:
            StaticLoggerBinder.getSingleton();
            
            //初始化完成,修改初始化状态:
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            
            //如果在类路径下有多个StaticLoggerBinder类,此方法打印出具体实例化了哪个StaticLoggerBinder类:
            reportActualBinding(staticLoggerBinderPathSet);
            
            fixSubstitutedLoggers();
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage();
            if (msg != null && msg.indexOf("org.slf4j.impl.StaticLoggerBinder.getSingleton()") != -1) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

    
    //在类路径下,查找org.slf4j.impl.StaticLoggerBinder类:
    private static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        
        try {
            //获取LoggerFactory的类加载器:
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            
            Enumeration<URL> paths;
            
            //判断类加载器是否为null:
            if (loggerFactoryClassLoader == null) {
                //查找org.slf4j.impl.StaticLoggerBinder类:
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                //查找org.slf4j.impl.StaticLoggerBinder类:
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
    
            //遍历Enumeration对象:
            while (paths.hasMoreElements()) {
                //将StaticLoggerBinder类存在的路径添加到Set集合中:
                URL path = (URL) paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException ioe) {}
        return staticLoggerBinderPathSet;
    }


    //如果存在多个StaticLoggerBinder类,就打印每个StaticLoggerBinder类的路径:
    private static void reportMultipleBindingAmbiguity(Set<URL> staticLoggerBinderPathSet) {
        //判断Set集合长度:
        if (isAmbiguousStaticLoggerBinderPathSet(staticLoggerBinderPathSet)) {
            Util.report("Class path contains multiple SLF4J bindings.");
            Iterator<URL> iterator = staticLoggerBinderPathSet.iterator();
            while (iterator.hasNext()) {
                URL path = (URL) iterator.next();
                Util.report("Found binding in [" + path + "]");
            }
            Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
        }
    }

    //如果类路径下的StaticLoggerBinder类不止一个的话,就打印出具体实例化的对象是哪个:
    private static void reportActualBinding(Set<URL> staticLoggerBinderPathSet) {
        //判断Set集合长度:
        if (isAmbiguousStaticLoggerBinderPathSet(staticLoggerBinderPathSet)) {
            Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
        }
    }

    //检查slf4j-api和slf4j-log4j12的版本是否兼容;
    private final static void versionSanityCheck() {
        try {
            //获取StaticLoggerBinder的api版本,也就是slf4j-log4j12所属版本
            String requested = StaticLoggerBinder.REQUESTED_API_VERSION;
            boolean match = false;
            
            //判断StaticLoggerBinder的api版本是否属于slf4j-api所支持的版本:
            for (int i = 0; i < API_COMPATIBILITY_LIST.length; i++) {
                if (requested.startsWith(API_COMPATIBILITY_LIST[i])) {
                    match = true;
                }
            }
            if (!match) {.....}
        } catch (java.lang.NoSuchFieldError nsfe) {
        } catch (Throwable e) {}
    }
}

org.slf4j.impl.StaticLoggerBinder类:

public class StaticLoggerBinder implements LoggerFactoryBinder {
    
    //StaticLoggerBinder构造方法:private修饰,单例;
    private StaticLoggerBinder() {
        //创建Log4jLoggerFactory对象:
        loggerFactory = new Log4jLoggerFactory();
        ....
    }
    
    //单例对象:
    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
    
    //供外部调用,得到单例的StaticLoggerBinder对象:
    public static final StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }
    
    //获取ILoggerFactory对象,实际为Log4jLoggerFactory;
    public ILoggerFactory getLoggerFactory() {
        return loggerFactory;
    }
}

org.slf4j.impl.Log4jLoggerFactory类:

public class Log4jLoggerFactory implements ILoggerFactory {

    // 线程安全的ConcurrentHashMap对象,保存日志对象;
    ConcurrentMap<String, Logger> loggerMap;

    public Log4jLoggerFactory() {
        loggerMap = new ConcurrentHashMap<String, Logger>();
    }
    
    //创造Logger对象:实际返回的是与slf4j相结合的日志对象;
    public Logger getLogger(String name) {
        
        //通过类名称获取日志对象:
        Logger slf4jLogger = loggerMap.get(name);
        
        //不为空,则返回:
        if (slf4jLogger != null) {
            return slf4jLogger;
        } else {
            //log4j日志对象:
            org.apache.log4j.Logger log4jLogger;

            if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) {
                //开始log4j的初始化过程:
                log4jLogger = LogManager.getRootLogger();
            }else {
                //开始log4j的初始化过程:
                log4jLogger = LogManager.getLogger(name);
            }
            
            //通过Log4jLoggerAdapter对象,对log4j的日志对象进行封装,使用了适配器模式:
            Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
            Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
}

具体流程总结如下:

1.结合上面的例子,当slf4j_log4jDemo测试类被加载的时候,slf4j开始了初始化操作:
    Logger logger = LoggerFactory.getLogger(slf4j_log4jDemo.class);

2.slf4j初始化操作分为2个阶段,第一阶段获取日志工厂,第二阶段通过日志工厂获取日志对象;

3.在第一阶段中,首先通过classloader查找classpath下存在的org/slf4j/impl/StaticLoggerBinder.class类,可能有多个。在我们的测试例子中,实际上找到的是slf4j-log4j12包下的org.slf4j.impl.StaticLoggerBinder类

4.其次,实例化StaticLoggerBinder对象,调用getLoggerFactory方法获取对应的loggerFactory,也就是slf4j-log4j12包下的org.slf4j.impl.Log4jLoggerFactory,并返回;

5.上面过程结束后,loggerFactory被创建,紧接着通过Log4jLoggerFactory的getLogger方法获取log4j的日志对象,使用的是最原生的方法log4j的LogManager来实现,最终返回org.apache.log4j.Logger.log4jLogger对象;

6.由于log4j的日志对象org.apache.log4j.Logger.log4jLogger与slf4j的org.slf4j.Logger日志接口并无多态关系,所以此时slf4j引入了一个org.slf4j.impl.Log4jLoggerAdapter类,该类实现了slf4j的Logger接口,再其内部维护一个log4j的日志对象log4jLogger,使用的是适配器模式,进而达到了整合;在程序中,我们使用日志api的时候,实际上都是Log4jLoggerAdapter类来完成的。

3.4 slf4j静态绑定原理

虽然commons-logging和slf4j都是日志服务接口,但是两者对于底层日志框架绑定的方式相差甚远。在第一篇日志系统的文章中,笔者已经介绍过,commons-logging是基于动态绑定来实现与日志框架的结合,也就是说在编译期间我们的程序并不知道底层的实现是什么,只有在运行期间才进行获取;

与commons-logging不同的是,slf4j是基于静态绑定来实现与日志框架的结合,在编译期间我们的程序就已经知道使用了哪种日志实现。在上面的源码中已有所提及,下面再回顾下。

具体源码,如下:

public final class LoggerFactory {

    private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

    //静态绑定操作:找到与slf4j相结合的日志框架,在编译期间完成日志绑定操作;
    private final static void bind() {
        try {
            //在类路径下,查找org.slf4j.impl.StaticLoggerBinder类:
            Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            。。。。。               
    }

    //在类路径下,查找org.slf4j.impl.StaticLoggerBinder类:
    private static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        try {
            //获取LoggerFactory的类加载器:
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            //判断类加载器是否为null:
            if (loggerFactoryClassLoader == null) {
                //查找org.slf4j.impl.StaticLoggerBinder类:
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                //查找org.slf4j.impl.StaticLoggerBinder类:
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            ......
        } catch (IOException ioe) {}
        return staticLoggerBinderPathSet;
    }
}

3.5 slf4j和commons-logging比较

(1)slf4j使用了静态绑定方式,实现了与底层日志框架的结合, 避免了commons-logging中由于类加载器不同导致的日志加载失败情况的发生;

(2)slf4j支持参数化日志打印,也就是占位符{}的方式。去除了commons-logging中的isDebugEnabled(), isInfoEnabled()等方法的日志级别检查代码,极大的提高了代码可读性;并且,占位符的方式也延缓了构建日志信息(String的开销),提高了内存的使用性;

在commons-logging中,我们经常需要些这样的代码:

if (logger.isDebugEnabled()) {
    logger.debug("我是: " + name);
}

而在slf4j中,我们可以这样写:

logger.debug("我是: {}",name);

在commons-logging中,是要符合日记级别,我们就进行字符串的拼接;而在slf4j中,我们不进行字符串拼接操作,而是使用StringBuffer来完成的替换。这不仅降低了内存消耗而且预先降低了CPU去处理字符串连接命令的时间,提高了程序的性能。

3.6 slf4j搭配commons-logging使用原理

在前面的小节中,我们提到了slf4j为了兼容老代码,是可以跟commons-logging结合使用的,需要在pom.xml文件中引入jcl-over-slf4j.jar包。具体实现过程如下:

测试代码:(引入的依旧为commons-logging对象,无需改变)

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;

public class commons_loggingDemo {
    Log log= LogFactory.getLog(commons_loggingDemo.class);
    @Test
    public void test() throws IOException {
        log.debug("Debug info.");
        log.info("Info info");
        log.warn("Warn info你好");
        log.error("Error info");
        log.fatal("Fatal info");
    }
}

引入pom依赖:(除了原有的commons-logging和log4j依赖外,还需要添加slf4j-api、jcl-over-slf4j、slf4j-log4j12依赖)

!-- commons-logging -->
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.3</version>
</dependency>

<!--将commons-logging引入到slf4j中去-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.20</version>
</dependency>

 <!--log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<!--slf4j-log4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</version>
</dependency>

 <!--slf4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.20</version>
</dependency>

日志配置文件: (均为commons-logging时期配置,无需为slf4j做任何改变)

commons-logging.properties配置文件:
#日志对象:
org.apache.commons.logging.Log=org.apache.log4j.Logger
#日志工厂:
org.apache.commons.logging.LogFactory=org.apache.commons.logging.impl.LogFactoryImpl

log4j.xml配置文件:
<log4j:configuration>
    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
        <param name="Target" value="System.out" />
        <param name="ImmediateFlush" value="true"/>
        <param name="encoding" value="UTF-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %t %-5p (%c:%L) - %m%n"/>
        </layout>
    </appender>
    <root>
        <priority value="debug" />
        <appender-ref ref="CONSOLE" />
    </root>
</log4j:configuration>

实现原理:

将commons-logging的输出引入到jcl-over-slf4j中,再转向slf4j,紧接着进入到slf4j-log4j12,最终进入到log4j;

源码分析:(请结合commons-logging初始化过程进行学习)

commons-logging中的org.apache.commons.logging.LogFactory类:

public abstract class LogFactory {
    
    protected static final String SERVICE_ID = "META-INF/services/org.apache.commons.logging.LogFactory";

    public static LogFactory getFactory() throws LogConfigurationException {
        ClassLoader contextClassLoader = getContextClassLoaderInternal();
        if (factory == null) {
            。。。忽略
            try {
                
                //在classpath下寻找 META-INF/services/org.apache.commons.logging.LogFactory 文件:(在jcl-over-slf4j.jar中) 
                final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
                
                if( is != null ) {
                    BufferedReader rd;
                    try {
                        rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                    } catch (java.io.UnsupportedEncodingException e) {
                        rd = new BufferedReader(new InputStreamReader(is));
                    }
                    
                    //读取该文件中的第一行信息:org.apache.commons.logging.impl.SLF4JLogFactory
                    String factoryClassName = rd.readLine();
                    
                    rd.close();

                    if (factoryClassName != null && ! "".equals(factoryClassName)) {
                          。。。。忽略
                        
                        //SLF4JLogFactory进行实例化(在jcl-over-slf4j.jar中)
                        factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
                    }
                } else {
                   。。。。。忽略
                }
            } catch (Exception ex) { 。。。。。忽略}
        }
        。。。。。忽略
        return factory;
    }
}

jcl-over-slf4j的org.apache.commons.logging.impl.SLF4JLogFactory中:

public class SLF4JLogFactory extends LogFactory {
    
    //获取日志对象:
    public Log getInstance(Class clazz) throws LogConfigurationException {
        return (getInstance(clazz.getName()));
    }

    public Log getInstance(String name) throws LogConfigurationException {
        Log instance = loggerMap.get(name);
        if (instance != null) {
            return instance;
        } else {
            Log newInstance;

            //此处使用的是slf4j中的LoggerFactory进行日志对象获取,此时已经完全将commons-logging引入到了slf4j中来;
            Logger slf4jLogger = LoggerFactory.getLogger(name);
            if (slf4jLogger instanceof LocationAwareLogger) {
                newInstance = new SLF4JLocationAwareLog((LocationAwareLogger) slf4jLogger);
            } else {
                newInstance = new SLF4JLog(slf4jLogger);
            }
            Log oldInstance = loggerMap.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
}

以上便完成了,commons-logging结合slf4j的过程,后续获取日志对象的逻辑与3.3节中完全一致,可参考上面的讲解。

上一篇下一篇

猜你喜欢

热点阅读