04、slf4j(日志门面)(二)

简介: 04、slf4j(日志门面)(二)

三、原理分析


3.1、初始绑定日志实现原理


在slf4j-api中我们通常使用下面的方法来获取logger实例:


public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);


获得的实例实际上跟我们导入jar包有关,那么它是如何进行初始配置的呢?看下面源码:


public final class LoggerFactory {
   public static Logger getLogger(Class<?> clazz) {
        //1、调用一个重载方法,传入类名
        Logger logger = getLogger(clazz.getName());//见2
        if (DETECT_LOGGER_NAME_MISMATCH) {
            Class<?> autoComputedCallingClass = Util.getCallingClass();
            if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
                Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                                autoComputedCallingClass.getName()));
                Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
            }
        }
        return logger;
    }
    //2、根据类名来获取logger实例
    public static Logger getLogger(String name) {
        //获取ILoggerFactory接口的实现类(接口方法是getLogger())
        ILoggerFactory iLoggerFactory = getILoggerFactory();//见3
        return iLoggerFactory.getLogger(name);
    }
    //3、获取ILoggerFactory的实例
    public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    //执行初始化方法
                    performInitialization();//见4
                }
            }
        }
        ...
    }
    //4、执行初始化操作
    private final static void performInitialization() {
        bind();//见5
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }
    //5、绑定操作
    private final static void bind() {
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            if (!isAndroid()) {
                //下面两行比较关键,这行是查找可能的静态日志执行器路径使用Set来接收
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();//见6
                //查看set中是否超过1个路径,若是则进行窗口输出提示信息
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);//见7
            }
            //
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            ....
    }
    //6、这里是查找有关org/slf4j/impl/StaticLoggerBinder.class路径,都放置到set中返回
    static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        // use Set instead of list in order to deal with bug #138
        // LinkedHashSet appropriate here because it preserves insertion order
        // during iteration
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            if (loggerFactoryClassLoader == null) {
                //从类加载器中进行查找是否有org/slf4j/impl/StaticLoggerBinder.class路径
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            //这里依旧是根据查找到的路径继续往下延伸查找并添加到Set中去
            while (paths.hasMoreElements()) {
                URL path = paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException ioe) {
            Util.report("Error getting resources from path", ioe);
        }
        return staticLoggerBinderPathSet;
    }
}


52行的查找org/slf4j/impl/StaticLoggerBinder.class路径操作存放到Set中返回。


看到上面方法6中是不是有个疑惑,查找org/slf4j/impl/StaticLoggerBinder.class这个相关路径有什么用,与我们要加载对应的日志实现有什么关系呢?


我们在本次源码过程中引入两个日志实现框架slf4j-log4j12、slf4j-jdk14(这两个都是slf4j为较早出现的日志设置的适配器),引入jar包之后,我们尝试搜索一下StaticLoggerBinder这个类:



好家伙原来slf4j实现的相关适配器的名称都叫StaticLoggerBinder啊,我们继续看下去,看下jdk14的吧(就是JUL):



该工厂类中的实例loggerFactory是获取了一个JDK14的工厂类,那么我们继续看向适配器中内容:



重写了getLogger()方法,其中实例化了JUL的logger实例,调用有参构造传入并且创建了一个JDK14的适配器,我们再看下这个适配器中都做了些什么:



好家伙其中包含了各个日志等级的方法,其中都包含了JUL的日志操作。



53行中调用的reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);来报告多个绑定日志实现框架


public final class LoggerFactory {
    //7、来进行报告含有多个日志框架的路径
    private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
        //该方法判断是否set中数量>1
        if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {//见8
            //若是超过1个的话就会报该问题(就是我们之前1.4中的注意点报错)
            Util.report("Class path contains multiple SLF4J bindings.");
            for (URL path : binderPathSet) {
                Util.report("Found binding in [" + path + "]");
            }
            Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
        }
    }
    //8、判断set中的容量大小是否>1
    private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
        return binderPathSet.size() > 1;
    }
}



该方法就是来检测是否有多个日志实现框架导入,若是有则报出提示信息。



四、桥接旧的日志实现框架


介绍桥接器

直接举场景来说明:对于一些老项目直接使用的是Log4j或JUL的日志实现框架,并没有使用到日志门面来进行管理日志框架,当项目需要迭代升级时,我们想把原先的日志实现框架切换为logback,此时会出现一个问题,若是我们直接将对应的日志jar包更改为logback,那么项目中会出现大量报错,因为原先引入的包是import org.apache.log4j.Logger;,此时就会出现问题,我们需要重新修改大量的代码,需要耗费大量的时间与精力。


解决方案:在slf4j中可以使用桥接器从而让我们不用修改一行代码实现日志框架的切换。在slf4j中附带了几个桥接木块,这些模块对于log4j、JCL和JUL的API调用重定向(其实就是全限定名与原来的完全相同)。


下图包含了对应的解决方案:



可用log4j-over-slf4j.jar替代Log4j


4.1、log4j-over-slf4j桥接器使用


解决过程

问题描述


模拟场景:老项目直接使用的是org.apache.log4j.Logger,现今项目迭代升级,需要使用Logback日志框架。


我们首先将log4jjar包移除,之后引入logback-classic依赖坐标,此时就会出现下方情况:





解决方案:使用桥接器log4j-over-slf4j


引入坐标依赖:


<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>



不用修改任何代码即可替换日志框架。


原理分析



从导入的包来看,其中的全限定类名与原本Log4j的一毛一样,接着看是如何达到无缝衔接的。

同样是下方的获取logger实例方法:


import org.apache.log4j.Logger;
public class LogTest {
    public static final Logger LOGGER = Logger.getLogger(LogTest.class);
}


查看源码:


//即引入的log4j-over-slf4j坐标
package org.apache.log4j;
public class Logger extends Category {
    //1、
    public static Logger getLogger(Class clazz) {
        return getLogger(clazz.getName());//见2
    }
    //2、
    public static Logger getLogger(String name) {
        return Log4jLoggerFactory.getLogger(name);//见3
    }
    //4、有参构造
    protected Logger(String name) {
        super(name);//调用的是Category的有参构造 去5
    }
}
//log4j的工厂类
class Log4jLoggerFactory {
    //3、获取logger实例
    public static Logger getLogger(String name) {
        Logger instance = (Logger)log4jLoggers.get(name);
        if (instance != null) {
            return instance;
        } else {
            //重要的点来了:注意看这个方法
            Logger newInstance = new Logger(name);//回到上面的Logger类中的4方法
            Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
}
//Loger类的父类
public class Category {
    protected Logger slf4jLogger;
    //5、有参构造
    Category(String name) {
        this.name = name;
        //注意这个方法,LoggerFactory.getLogger()获取的是slf4j的对应Logger
        this.slf4jLogger = LoggerFactory.getLogger(name);
        if (this.slf4jLogger instanceof LocationAwareLogger) {
            this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
        }
    }
}


简单来说就是slf4j的开发者提供了一个与Log4j的全限定类名相同的一个包,其中的方法名称与Log4j的都相同,在getLogger()方法中实际获取到了slf4j对应的Logger实例。

如下图:log4j是灰色表示的是移除掉,使用log4j-over-slf4j




4.2、jul、jcl桥接器


当使用单独的日志实现框架想要替换成如logback日志框架时可使用对应的桥接器来进行替代:


<!-- jul -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<!--jcl -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>


替换了之后别忘了引入对应要替换的日志实现框架。



三个slf4j日志实现框架与桥接器不能同时使用

以下的jar包不能同时出现:


log4j-over-slf4j.jar(桥接器)和slf4j-log4j12.jar

jcl-over-slf4j.jar和 slf4j-jcl.jar

jul-to-slf4j.jar和slf4j-jdk14.jar

为什么不能同时出现呢?我们借第一组进行查看:


①首先看下相应的依赖导入


<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>




log4j-over-slf4j坐标依赖




slf4j-log4j12坐标依赖,注意该包的全限定类名与log4j的权限定类名一致。


②我们运行下程序查看一下


mport org.apache.log4j.Logger;
public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = Logger.getLogger(LogTest.class);
    public static void main(String[] args) {
        LOGGER.error("error");
    }
}



为什么会出现这种情况呢,看下面原理图一下子懂了:



其实说白了就是log4j与log4j-over-slf4j的权限定包名都是相同的,当在slf4j-log4j12的适配器中若是找到log4j-over-slf4j中的log4j时此时就会出现无限死循环,也就导致栈溢出了。

前后引入jar包位置改变不会报错


不过我发现了一个问题:如果pom.xml中的坐标前后位置,两个jar包都导入了也不会报错


<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>


先引入slf4j-log4j12再引入log4j-over-slf4j运行就不会报错,可能是在slf4j-log4j12的适配器中找Logger时,pom.xml中log4j首先被加载到了所以就不会报错了。

说明:尽管这样引入不会报错,我们也一定不要这样子引入桥接器与slf4j的日志实现框架,这样也没必要。



总结


1、对于slf4j切换日志框架我们实际上就只需要引入slf4j提供的各个日志实现依赖即可(对应坐标其中包含了slf-api以及对应实现框架依赖)。


2、对于一开始就没有进行使用日志门面而只是单单使用日志框架的项目,若是想要不修改代码进行切换日志框架,我们就要考虑使用slf4j桥接器来进行日志框架切换。


3、slf4j提供的日志框架实现尽量不要与桥接器同时使用,否则极有可能会报错!

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
Java API Apache
03、JCL(日志门面)
03、JCL(日志门面)
03、JCL(日志门面)
|
Java 数据库连接 API
日志门面与日志实现框架介绍
日志门面与日志实现框架介绍
日志门面与日志实现框架介绍
|
Java API Maven
04、slf4j(日志门面)(一)
04、slf4j(日志门面)(一)
04、slf4j(日志门面)(一)
|
Java Apache
什么是日志门面? SpringBoot整合log4j2 ,日志落地
什么是日志门面? SpringBoot整合log4j2 ,日志落地
什么是日志门面? SpringBoot整合log4j2 ,日志落地
|
23天前
|
安全 Linux 网络安全
/var/log/secure日志详解
Linux系统的 `/var/log/secure` 文件记录安全相关消息,包括身份验证和授权尝试。它涵盖用户登录(成功或失败)、`sudo` 使用、账户锁定解锁及其他安全事件和PAM错误。例如,SSH登录成功会显示&quot;Accepted password&quot;,失败则显示&quot;Failed password&quot;。查看此文件可使用 `tail -f /var/log/secure`,但通常只有root用户有权访问。
69 4
|
24天前
|
运维 监控 数据可视化
日志服务 HarmonyOS NEXT 日志采集最佳实践
鸿蒙操作系统(HarmonyOS)上的日志服务(SLS)SDK 提供了针对 IoT、移动端到服务端的全场景日志采集、处理和分析能力,旨在满足万物互联时代下应用的多元化设备接入、高效协同和安全可靠运行的需求。
116756 10
|
25天前
|
监控 Linux 网络安全
/var/log/auth.log日志说明
`/var/log/auth.log`是Linux系统记录身份验证和授权事件的日志文件,包括登录尝试、SSH连接、sudo操作等。系统管理员可通过它监控用户登录、检查失败尝试、跟踪SSH活动、查看sudo/su操作及PAM活动。日志内容可能因系统配置而异,可能存在于其他日志文件中。分析这些日志可使用`tail`、`grep`等命令或专用日志分析工具。了解系统和其服务详情有助于提取有用信息。
38 2
|
25天前
|
安全 Ubuntu Unix
/var/log/syslog日志说明
`/var/log/syslog`是Unix和Linux的日志文件,记录系统事件和消息,由`syslogd`或`rsyslogd`生成。日志条目含时间戳、主机名、PID、日志级别(如DEBUG、ERROR)和事件描述。内容涵盖系统启动/关闭、硬件错误、网络、用户登录、安全事件等。查看日志可使用`cat`、`tail`、`less`或`grep`命令。不过,不同Linux发行版可能有变,如Ubuntu使用`journald`和`journalctl`。
42 3
|
1天前
|
XML Java Maven
Springboot整合与使用log4j2日志框架【详解版】
该文介绍了如何在Spring Boot中切换默认的LogBack日志系统至Log4j2。首先,需要在Maven依赖中排除`spring-boot-starter-logging`并引入`spring-boot-starter-log4j2`。其次,创建`log4j2-spring.xml`配置文件放在`src/main/resources`下,配置包括控制台和文件的日志输出、日志格式和文件切分策略。此外,可通过在不同环境的`application.yml`中指定不同的log4j2配置文件。最后,文章提到通过示例代码解释了日志格式中的各种占位符含义。
|
1天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
8 1