log4j-over-slf4j的jar包适配原理解析

  • Post author:
  • Post category:其他


如果想把项目中的日志实现统一成slf4j的话,则需要把第三方一些依赖包中的日志包去掉,例如Spring中的jcl,或者其他的像早期的log4j,如果直接排除,则程序肯定会运行报错,此时需要引入适配包,这个适配包就是一个狸猫换太子包,这个包有着和jcl和log4j一摸一样的包名和类名,所以在程序动态运行过程中,只需要关心classpath下有没有这个类即可,并不需要知道这个类在哪个jar包,正因如此,才能实现狸猫换太子的功能。下面写一个测试程序:

  • 项目目录结构如下

    在这里插入图片描述

最开始是先有的jcl这个包,然后这个包中依赖log4j这个包,在这个包中编写了一个类,里面使用了log4j的Logger输出日志信息

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

public class Runner {

    private static Logger logger = LogManager.getLogger(Runner.class);

    public static void run(){
        logger.info("application is running...");
    }

}

log4j的配置文件如下

### set log levels ###
log4j.rootLogger=DEBUG

### direct log messages to stdout ###
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.Target=System.out
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-2p %m%n

log4j.logger.com.sp=DEBUG,A1
  • 创建app项目,app项目依赖jcl这个包
    <dependency>
            <groupId>org.example</groupId>
            <artifactId>jcl</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

在app包中编写一个测试类,调用Runner的run方法

import com.sp.Runner;

public class Main {

    public static void main(String[] args) {
        Runner.run();
    }

}

控制台输出如下

在这里插入图片描述

app包就好比我们自己写的应用程序,jcl就比如依赖的第三方框架,如spring,而jcl依赖的log4j就是第三方框架依赖的日志包。但是现在的这个依赖包依赖的是log4j,如何统一成slf4j?

  1. 首先排除这个依赖包中的日志包
    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>jcl</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>log4j</groupId>
                    <artifactId>log4j</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

如果这样直接排除,则项目无法启动,因为第三方包中依赖的class文件找不到,此时抛出了一个经典异常。如果遇到 NoClassDefFoundError 这个异常可考虑依赖的相关jar包是否都已引入。

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/log4j/LogManager
	at com.sp.Runner.<clinit>(Runner.java:8)
	at com.sci.app.Main.main(Main.java:9)
Caused by: java.lang.ClassNotFoundException: org.apache.log4j.LogManager
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 2 more
  1. 此时就需要引入log4j的适配包,并且这些适配包的类名和包名需要和log4j保持一致,调用的方法以及形参也要保持一致,这样在程序运行时才不会报错。

    在这里插入图片描述
public class LogManager {
    
    public static Logger getLogger(Class clazz){
        return new Logger();
    }
    
}

public class Logger {

    public void info(Object message){
        System.out.println("slf4j  INFO ===> " + message);
    }

}
  1. 此时在app中引入 log4j-over-slf4j
    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>jcl</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>log4j</groupId>
                    <artifactId>log4j</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.example</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

    </dependencies>

此时重新运行,amazing !

在这里插入图片描述

适配包的作用就是编写一个些与原日志依赖同包名,同类名,同方法名的类,然后在这些类中使用slf4j 作为门面来获取logger,至于这里的logger是slf4j的哪个实现,那就要看项目中引入了那个实现类了,slf4j的实现有logback,log4j-impl(兼容log4j的),jul-impl(兼容jul),log4j2。

  • 下面来看一下log4j-over-slf4j真正的庐山真面目
  1. 首先包名类名都是一致的

在这里插入图片描述

  1. LogManager.getLogger(Class clazz);
public class LogManager {
    public LogManager() {
    }

    public static Logger getRootLogger() {
        return Log4jLoggerFactory.getLogger("ROOT");
    }

    public static Logger getLogger(String name) {
        return Log4jLoggerFactory.getLogger(name);
    }

    public static Logger getLogger(Class clazz) {
        return Log4jLoggerFactory.getLogger(clazz.getName());
    }

    public static Logger getLogger(String name, LoggerFactory loggerFactory) {
        return loggerFactory.makeNewLoggerInstance(name);
    }

    public static Enumeration getCurrentLoggers() {
        return (new Vector()).elements();
    }

    public static void shutdown() {
    }

    public static void resetConfiguration() {
    }
}

获取Logger调用到了 Log4jLoggerFactory.getLogger(clazz.getName());

通过这个方法,获取到了Logger,对于那些第三方框架来说,获取的还是org.apache.log4j下的logger,但是看这个Logger的内部有一个slf4j.Logger的成员变量,在调用log4j的相关方法时,其实时调用了成员变量的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 oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
public class Category {
    private static final String CATEGORY_FQCN = Category.class.getName();
    private String name;
    protected Logger slf4jLogger;
    private LocationAwareLogger locationAwareLogger;
    private static Marker FATAL_MARKER = MarkerFactory.getMarker("FATAL");

    Category(String name) {
        this.name = name;
        this.slf4jLogger = LoggerFactory.getLogger(name);
        if (this.slf4jLogger instanceof LocationAwareLogger) {
            this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
        }

    }
   void differentiatedLog(Marker marker, String fqcn, int level, Object message, Throwable t) {
        String m = this.convertToString(message);
        if (this.locationAwareLogger != null) {
            this.locationAwareLogger.log(marker, fqcn, level, m, (Object[])null, t);
        } else {
            switch(level) {
            case 0:
                this.slf4jLogger.trace(marker, m);
                break;
            case 10:
                this.slf4jLogger.debug(marker, m);
                break;
            case 20:
                this.slf4jLogger.info(marker, m);
                break;
            case 30:
                this.slf4jLogger.warn(marker, m);
                break;
            case 40:
                this.slf4jLogger.error(marker, m);
            }
        }

    }

而这个logger具体时什么级别,能打印哪些信息,这些东西在 this.slf4jLogger = LoggerFactory.getLogger(name); 时就可以获取到,logger内部维护了能打印哪些级别的信息等。比如传入的classname是 org.springboot.autoconfig.DataSourceAutoConfig,但是org.springboot.autoconfig包设置的打印级别是warn,那么DataSourceAutoConfig维护的logger成员变量就只能打印warn及以上级别的日志信息。



版权声明:本文为qq_43750656原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。