倒數第二全的常見日誌框架分析

  • 2021 年 8 月 20 日
  • 筆記

起由

聽過太多的日誌框架了,比如log4jjcljclslf4jlogback……

之前對這些概念就很亂,不知道它們都是具體幹什麼的,而且也不知道想要使用一個日誌框架時,要怎麼引入相關的依賴

本文就搜集了常見的日誌框架,並梳理了其用法以及相關原理。

日誌的發展歷程

a. 從最早期開始,大家都是使用System.outSystem.err來列印日誌;不靈活也不可以配置;要麼全部列印,要麼全部不列印;沒有一個統一的日誌級別

b. 後來Log4j就出現了,它是Ceki Gülcü這個大佬開發的,後來Log4j成為了Apache基金會項目中的一員

c. 後來Java也推出了自己的日誌框架JUL(Java Util Logging),在package java.util.logging

d. Apache又推出了日誌介面Jakarta Commons Logging,也就是日誌抽象層,你就可以很方便的在Log4jJUL之間做切換

e. Ceki Gülcü覺得覺得JCL不好,開發了一套新的日誌門面Slf4j(Simple Logging Facade for Java)、它的實現Logback以及一些橋接包:

jcl-over-slf4j.jar :jcl ——> slf4j
slf4j-jcl.jar :slf4j ——> jcl
log4j-over-slf4j :log4j ——> slf4j
slf4j-log4j12.jar :slf4j ——> log4j
jul-to-slf4j :jul ——> slf4j
slf4j-jdk14.jar :slf4j ——> jul

f. 後來Apache直接推出新項目,不是Log4j1.x升級,而是新項目Log4j2,因為Log4j2是完全不兼容Log4j1.x的,它也搞了分離的設計,分化成log4j-apilog4j-core,這個log4j-api也是日誌介面,log4j-core是日誌實現,它也出了很多橋接包:

log4j-jcl :jcl ——> log4j2
log4j-1.2-api :log4j ——> log4j2
log4j-slf4j-impl :slf4j ——> log4j2
log4j-jul :jul ——> log4j2
log4j-to-slf4j :log4j2 ——> slf4j

基本用法

log4j

依賴:
    <dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>

配置文件:
log4j.properties
# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=DEBUG, A1
# A1 is set to be a ConsoleAppender.
log4j.appender.A1=org.apache.log4j.ConsoleAppender
# A1 uses PatternLayout.
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n
 
測試類:
import org.apache.log4j.Logger;
public class Main {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Main.class);
        logger.info("hello, world!");
    }
}
 
輸出:
0    [main] INFO  cn.eagleli.log.log4j.Main  - hello, world!

jul

測試類:
import java.util.logging.Logger;
public class Main {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Main.class.getName());
        logger.info("hello, world!");
    }
}
 
輸出:
八月 11, 2021 11:06:19 下午 cn.eagleli.log.jul.Main main
資訊: hello, world!

jcl

依賴:
    <dependencies>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
    </dependencies>

測試類:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class Main {
    public static void main(String[] args) {
        Log log = LogFactory.getLog(Main.class);
        log.info("hello, world!");
    }
}

輸出:
八月 11, 2021 11:08:25 下午 cn.eagleli.log.jcl.Main main
資訊: hello, world!

從上面輸出結果可以看出,默認會使用jul作為底層的日誌框架

如果我們想換成log4j作為底層的日誌框架,怎麼辦呢?只需要加一個依賴即可,如下:

依賴:
    <dependencies>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>

輸出:
0    [main] INFO  cn.eagleli.log.jcl.Main  - hello, world!

從結果可以看出,底層日誌框架已經變了,同樣發現,我們的程式碼沒有任何改動,只是加了一個依賴,由此可以看出介面的重要性。

slf4j

依賴:
    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>
 
測試類:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(Main.class);
        logger.info("hello, world!");
    }
}
 
輸出:
23:14:30.893 [main] INFO cn.eagleli.log.slf4j.Main - hello, world!

上面底層日誌框架使用的是logback

那如果我們想切換成jcl作為底層實現框架,怎麼辦呢?只需換一個依賴即可,如下:

依賴:
    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jcl</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>

輸出:
八月 11, 2021 11:18:27 下午 org.slf4j.impl.JCLLoggerAdapter info
資訊: hello, world!

從上面結果可以看出,底層已經切到jcl了,而jcl默認採用的是jdk日誌框架

cl-over-slf4jslf4j-jcl是不能同時使用的

因為前一個使用jcl API橋接到slf4j,後一個是使用slf4j API橋接到jcl,如果同時引用會導致循環調用,進而導致棧溢出

log4j2

依賴:
    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.14.1</version>
        </dependency>
    </dependencies>
 
測試類:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(Main.class);
        logger.error("hello, world!");
    }
}

輸出:
23:22:12.148 [main] ERROR cn.eagleli.log.log4j2.Main - hello, world!

以上我們採用的log4j2作為底層的實現,我們想要用slf4j作為底層的實現,怎麼辦呢?只需加一個依賴即可,如下:

依賴:
    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.14.1</version>
        </dependency>
    </dependencies>

輸出:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See //www.slf4j.org/codes.html#StaticLoggerBinder for further details.

從結果看出,我們底層日誌框架已經切換了,因為沒有任何slf4j的實現類框架,所以沒有輸出日誌。

怎麼做到無縫銜接的

jcl 原理

jcl無縫切換的核心程式碼如下:

public abstract class LogFactory {
    public static Log getLog(Class clazz)
        throws LogConfigurationException {
        return (getFactory().getInstance(clazz));
    }
}

public class LogFactoryImpl extends LogFactory {
    public Log getInstance(Class clazz) throws LogConfigurationException {
        return (getInstance(clazz.getName()));
    }
}

首先獲得一個LogFactory,它是可以自定義的,再從LogFactory中獲得一個Log類,Log類也是可以自定義的

LogFactory.getFactory()用來獲取一個LogFactory,核心邏輯如下:

a.從系統變數獲取
public static final String FACTORY_PROPERTY = "org.apache.commons.logging.LogFactory";
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);

b.從指定文件里獲取
protected static final String SERVICE_ID =
    "META-INF/services/org.apache.commons.logging.LogFactory";
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
 
c.從指定properties文件獲取
public static final String FACTORY_PROPERTIES = "commons-logging.properties"
Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
public static final String FACTORY_PROPERTY = "org.apache.commons.logging.LogFactory";
String factoryClass = props.getProperty(FACTORY_PROPERTY);
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
 
d.默認的LogFactory實現
public static final String FACTORY_DEFAULT = "org.apache.commons.logging.impl.LogFactoryImpl"
factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);

LogFactoryImpl.discoverLogImplementation()用來獲取一個Log,核心邏輯如下:

a.從LogFactory的attributes變數中獲取
public static final String LOG_PROPERTY = "org.apache.commons.logging.Log"
String specifiedClass = (String) getAttribute(LOG_PROPERTY);
public Object getAttribute(String name) {
    return attributes.get(name);
}

b.從系統變數中獲取
public static final String LOG_PROPERTY = "org.apache.commons.logging.Log";
specifiedClass = getSystemProperty(LOG_PROPERTY, null);

c.默認的數組列表
private static final String LOGGING_IMPL_LOG4J_LOGGER = "org.apache.commons.logging.impl.Log4JLogger";
private static final String[] classesToDiscover = {
        LOGGING_IMPL_LOG4J_LOGGER,
        "org.apache.commons.logging.impl.Jdk14Logger",
        "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
        "org.apache.commons.logging.impl.SimpleLog"
};
for(int i=0; i<classesToDiscover.length && result == null; ++i) {
    result = createLogFromClass(classesToDiscover[i], logCategory, true);
}

// 先類載入,然後利用反射創建實例
public class LogFactoryImpl extends LogFactory {
    private Log createLogFromClass(String logAdapterClassName,
                                   String logCategory,
                                   boolean affectState) 
            throws LogConfigurationException { 
                Class c = null;
                try {
                    c = Class.forName(logAdapterClassName, true, currentCL);
                } catch (ClassNotFoundException originalClassNotFoundException) {
                }
                constructor = c.getConstructor(logConstructorSignature);
                Object o = constructor.newInstance(params);
    }
}

看一兩個適配的Logger

a.
import java.util.logging.Logger;
public class Jdk14Logger implements Log, Serializable {
    public Jdk14Logger(String name) {
        this.name = name;
        logger = getLogger();
    }
    public Logger getLogger() {
        if (logger == null) {
            logger = Logger.getLogger(name);
        }
        return logger;
    }
    public void info(Object message) {
        log(Level.INFO, String.valueOf(message), null);
    }
   private void log( Level level, String msg, Throwable ex ) {
        Logger logger = getLogger();
        if (logger.isLoggable(level)) {
          // ...
        }
    }
}

b.
import org.apache.log4j.Logger;
public class Log4JLogger implements Log, Serializable {
    public Log4JLogger(String name) {
        this.name = name;
        this.logger = getLogger();
    }
    public Logger getLogger() {
        Logger result = logger;
        if (result == null) {
            synchronized(this) {
                result = logger;
                if (result == null) {
                    logger = result = Logger.getLogger(name);
                }
            }
        }
        return result;
    }
    public void info(Object message) {
        getLogger().log(FQCN, Priority.INFO, message, null );
    }
}

從上面的程式碼可以看出,這是典型的適配器模式,Jdk14Logger使用的julLogger,而Log4JLogger使用是log4jLogger

slf4j 原理

slf4j無縫切換的核心程式碼如下:

public final class LoggerFactory {
    public static Logger getLogger(String name) {
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        return iLoggerFactory.getLogger(name);
    }

    public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    performInitialization();
                }
            }
        }
        switch (INITIALIZATION_STATE) {
        case SUCCESSFUL_INITIALIZATION:
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        case NOP_FALLBACK_INITIALIZATION:
            return NOP_FALLBACK_FACTORY;
        case FAILED_INITIALIZATION:
            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
        case ONGOING_INITIALIZATION:
            // support re-entrant behavior.
            // See also //jira.qos.ch/browse/SLF4J-97
            return SUBST_FACTORY;
        }
        throw new IllegalStateException("Unreachable code");
    }

    private final static void performInitialization() {
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }

    private final static void bind() {
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            // skip check under android, see also
            // //jira.qos.ch/browse/SLF4J-328
            if (!isAndroid()) {
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            // the next line does the binding
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage();
            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                Util.report("Your binding is version 1.5.5 or earlier.");
                Util.report("Upgrade your binding to version 1.6.x.");
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        } finally {
            postBindCleanUp();
        }
    }
}

從上面的程式碼一路走過來,其實主要就是在LoggerFactory.bind()方法裡面,簡單粗暴,直接調用StaticLoggerBinder.getSingleton();org.slf4j.impl.StaticLoggerBinder這個類有沒有在classpath

從上面的圖中,我們可以看出logbackslf4j-jcl都有這個類,具體的StaticLoggerBinder程式碼就不分析了,大家可以自己看一下。

log4j2 原理

log4j2無縫切換的核心程式碼如下:

public class LogManager {
    public static Logger getLogger(final Class<?> clazz) {
        final Class<?> cls = callerClass(clazz);
        return getContext(cls.getClassLoader(), false).getLogger(cls);
    }

    public static LoggerContext getContext(final ClassLoader loader, final boolean currentContext) {
        try {
            return factory.getContext(FQCN, loader, null, currentContext);
        } catch (final IllegalStateException ex) {
            LOGGER.warn(ex.getMessage() + " Using SimpleLogger");
            return new SimpleLoggerContextFactory().getContext(FQCN, loader, null, currentContext);
        }
    }

    /**
     * Scans the classpath to find all logging implementation. Currently, only one will be used but this could be
     * extended to allow multiple implementations to be used.
     */
    static {
        // Shortcut binding to force a specific logging implementation.
        final PropertiesUtil managerProps = PropertiesUtil.getProperties();
        final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME);
        if (factoryClassName != null) {
            try {
                factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class);
            } catch (final ClassNotFoundException cnfe) {
                LOGGER.error("Unable to locate configured LoggerContextFactory {}", factoryClassName);
            } catch (final Exception ex) {
                LOGGER.error("Unable to create configured LoggerContextFactory {}", factoryClassName, ex);
            }
        }

        if (factory == null) {
            final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>();
            // note that the following initial call to ProviderUtil may block until a Provider has been installed when
            // running in an OSGi environment
            if (ProviderUtil.hasProviders()) {
                for (final Provider provider : ProviderUtil.getProviders()) {
                    final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory();
                    if (factoryClass != null) {
                        try {
                            factories.put(provider.getPriority(), factoryClass.newInstance());
                        } catch (final Exception e) {
                            LOGGER.error("Unable to create class {} specified in provider URL {}", factoryClass.getName(), provider
                                    .getUrl(), e);
                        }
                    }
                }

                if (factories.isEmpty()) {
                    LOGGER.error("Log4j2 could not find a logging implementation. "
                            + "Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                    factory = new SimpleLoggerContextFactory();
                } else if (factories.size() == 1) {
                    factory = factories.get(factories.lastKey());
                } else {
                    final StringBuilder sb = new StringBuilder("Multiple logging implementations found: \n");
                    for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) {
                        sb.append("Factory: ").append(entry.getValue().getClass().getName());
                        sb.append(", Weighting: ").append(entry.getKey()).append('\n');
                    }
                    factory = factories.get(factories.lastKey());
                    sb.append("Using factory: ").append(factory.getClass().getName());
                    LOGGER.warn(sb.toString());

                }
            } else {
                LOGGER.error("Log4j2 could not find a logging implementation. "
                        + "Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                factory = new SimpleLoggerContextFactory();
            }
            LogManagerStatus.setInitialized(true);
        }
    }
}
 

public final class ProviderUtil {

    public static Iterable<Provider> getProviders() {
        lazyInit();
        return PROVIDERS;
    }

    protected static void lazyInit() {
        // noinspection DoubleCheckedLocking
        if (instance == null) {
            try {
                STARTUP_LOCK.lockInterruptibly();
                try {
                    if (instance == null) {
                        instance = new ProviderUtil();
                    }
                } finally {
                    STARTUP_LOCK.unlock();
                }
            } catch (final InterruptedException e) {
                LOGGER.fatal("Interrupted before Log4j Providers could be loaded.", e);
                Thread.currentThread().interrupt();
            }
        }
    }

    private ProviderUtil() {
        for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
            try {
                loadProviders(classLoader);
            } catch (final Throwable ex) {
                LOGGER.debug("Unable to retrieve provider from ClassLoader {}", classLoader, ex);
            }
        }
        for (final LoaderUtil.UrlResource resource : LoaderUtil.findUrlResources(PROVIDER_RESOURCE)) {
            loadProvider(resource.getUrl(), resource.getClassLoader());
        }
    }

	protected static void loadProviders(final ClassLoader classLoader) {
		final ServiceLoader<Provider> serviceLoader = ServiceLoader.load(Provider.class, classLoader);
		for (final Provider provider : serviceLoader) {
			if (validVersion(provider.getVersions()) && !PROVIDERS.contains(provider)) {
				PROVIDERS.add(provider);
			}
		}
	}
}

其實上面的核心程式碼就是final ServiceLoader<Provider> serviceLoader = ServiceLoader.load(Provider.class, classLoader);,利用SPI獲取一個特定的Provider

從上圖可以看出,log4j-corelog4j-to-slf4j都有自己的Provider實現類,具體的程式碼就不分析了,大家可以自己看一下。

最後說說Spring中的日誌框架

Spring中的日誌框架採用的是spring-jcl,我們就來看看它,如下圖:

無縫切換的核心程式碼如下:

public abstract class LogFactory {
	public static Log getLog(Class<?> clazz) {
		return getLog(clazz.getName());
	}

	public static Log getLog(String name) {
		return LogAdapter.createLog(name);
	}
}

final class LogAdapter {
	public static Log createLog(String name) {
		switch (logApi) {
			case LOG4J:
				return Log4jAdapter.createLog(name);
			case SLF4J_LAL:
				return Slf4jAdapter.createLocationAwareLog(name);
			case SLF4J:
				return Slf4jAdapter.createLog(name);
			default:
				// Defensively use lazy-initializing adapter class here as well since the
				// java.logging module is not present by default on JDK 9. We are requiring
				// its presence if neither Log4j nor SLF4J is available; however, in the
				// case of Log4j or SLF4J, we are trying to prevent early initialization
				// of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly
				// trying to parse the bytecode for all the cases of this switch clause.
				return JavaUtilAdapter.createLog(name);
		}
	}

	private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";
	private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";
	private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger";
	private static final String SLF4J_API = "org.slf4j.Logger";
	private static final LogApi logApi;
	static {
		if (isPresent(LOG4J_SPI)) {
			if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
				// log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
				// however, we still prefer Log4j over the plain SLF4J API since
				// the latter does not have location awareness support.
				logApi = LogApi.SLF4J_LAL;
			}
			else {
				// Use Log4j 2.x directly, including location awareness support
				logApi = LogApi.LOG4J;
			}
		}
		else if (isPresent(SLF4J_SPI)) {
			// Full SLF4J SPI including location awareness support
			logApi = LogApi.SLF4J_LAL;
		}
		else if (isPresent(SLF4J_API)) {
			// Minimal SLF4J API without location awareness support
			logApi = LogApi.SLF4J;
		}
		else {
			// java.util.logging as default
			logApi = LogApi.JUL;
		}
	}

	private static boolean isPresent(String className) {
		try {
			Class.forName(className, false, LogAdapter.class.getClassLoader());
			return true;
		}
		catch (ClassNotFoundException ex) {
			return false;
		}
	}
}

從上面程式碼可以看出,spring-jcl就是根據Class.forName()來判斷各個日誌框架的核心類是否存在,然後創建對應的Log適配類。

我們的項目一般都會依賴spring-boot-starter-web,它又會依賴spring-boot-starter,又會依賴spring-boot-starter-logging

我們就來看看spring-boot-starter-logging-2.5.1,如下圖:

我們發現它沒有任何程式碼,我們再來看看它pom文件裡面的依賴:

<dependencies>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.3</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-to-slf4j</artifactId>
      <version>2.14.1</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jul-to-slf4j</artifactId>
      <version>1.7.30</version>
      <scope>compile</scope>
    </dependency>
</dependencies>

從這個依賴我們可以看出,它引用了logback-classic,也就是slf4j的實現框架

由於log4j-to-slf4jjul-to-slf4j依賴的存在,不管你使用的是log4j2還是jul,最終都會切到slf4j

所以我們可以說spring boot默認使用的日誌框架就是logback

總結

說了這麼多,我們總結一下這些日誌介面框架是怎麼做到無縫切換的:
jcl/spring-jcl:通過類載入的機制:class.forname()
slf4j:通過判斷類是否在classpath
log4j2:通過SPI的形式

參考

//segmentfault.com/a/1190000021121882
//logging.apache.org/log4j/1.2/manual.html
//commons.apache.org/proper/commons-logging/guide.html
//blog.csdn.net/chengmaoning/article/details/78072670
//www.slf4j.org/legacy.html
//logging.apache.org/log4j/2.x/runtime-dependencies.html