如果想把项目中的日志实现统一成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?
- 首先排除这个依赖包中的日志包
<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
-
此时就需要引入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);
}
}
- 此时在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真正的庐山真面目
- 首先包名类名都是一致的
- 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及以上级别的日志信息。