問題
在項目啟動時居砖,發(fā)現(xiàn)打印了大量的debug日志曲梗,但是src/main/resources
下明明有log4j.xml
夸溶,而且日志級別還設(shè)置的是info
矾策,為什么會打印出大量的debug日志呢?
分析
使用eclipse的Dependency Hierarchy功能可以對pom.xml
中定義的jar依賴進行可視化展示悠咱。發(fā)現(xiàn)項目中與日志相關(guān)的jar包有:
slf4j-api.jar
slf4j-log4j12.jar
log4j.jar
logback-classic.jar
logback-core.jar
那么蒸辆,猜測原因大概是:由于意外地引入了的logback的jar包,破壞了之前的slf4j + slf4j-log4j12 + log4j 的日志架構(gòu)析既,使得日志不再通過 log4j 輸出(因而log4j.xml
設(shè)置的info日志級別也隨之失效)躬贡,而是通過 logback 輸出,并且logback 默認輸出的是debug級別眼坏,因此出現(xiàn)了上述問題拂玻。
解決
解決的方法很簡單:在pom.xml
中通過<exclusions/>
排除掉 logback 相關(guān)的jar即可。
思考
對于這個小問題宰译,解決起來似乎很簡單檐蚜,但是以下幾個問題是值得我們進一步思考的:
基于 slf4j + slf4j-log4j12 + log4j 的日志架構(gòu),是如何工作的沿侈?
為什么引入了 logback 就會破壞之前的日志架構(gòu)闯第?
如何避免發(fā)生log4j和logback沖突的問題?
面向slf4j的接口API編程的最佳實踐是什么缀拭?
下面就來嘗試回答上面的幾個問題咳短。
基于 slf4j + slf4j-log4j12 + log4j 的日志架構(gòu)填帽,是如何工作的?
為了更好的了解其運行原理咙好,我搭建一個簡單的項目篡腌。見:https://github.com/wanghui0101/learning-slf4j/tree/master/learning-slf4j-sample
在/learning-slf4j-sample/src/main/webapp/WEB-INF/web.xml
中只簡單配置了一個Servlet,指向personal.wh.learning.slf4j.sample.SampleServlet
類敷扫。此類的源碼為:
package personal.wh.learning.slf4j.sample;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SampleServlet extends HttpServlet {
private static final long serialVersionUID = -870284320856607145L;
private static final Logger logger = LoggerFactory.getLogger(SampleServlet.class);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
logger.debug("Hello Slf4j ! - debug");
logger.info("Hello Slf4j ! - info");
try (PrintWriter writer = resp.getWriter()) {
writer.write("ok");
}
}
}
功能很簡單哀蘑,GET請求這個Servlet的時候,通過slf4j的org.slf4j.Logger
加載了log4j葵第,并讀取log4j.xml
绘迁,實現(xiàn)了日志的打印。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
</layout>
</appender>
<root>
<priority value="info" />
<appender-ref ref="STDOUT" />
</root>
</log4j:configuration>
由于日志級別為info卒密,因此只打印一句Hello Slf4j ! - info
缀台。
項目的lib包,與日志相關(guān)的有:
slf4j-api-1.7.25.jar
slf4j-log4j12-1.7.25.jar
log4j-1.2.17.jar(通過 slf4j-log4j12-1.7.25.jar 傳遞依賴引入)
源碼分析
接下來哮奇,我們通過源碼分析一下膛腐,slf4j是如何加載log4j,并實現(xiàn)日志打印的呢鼎俘?
org.slf4j.LoggerFactory.getLogger()
我們從org.slf4j.LoggerFactory#getLogger(Class<?> clazz)
開始:
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName()); // 通過類名獲取Logger對象
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; // 返回Logger對象
}
然后org.slf4j.LoggerFactory#getLogger(String name)
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory(); // 重點
return iLoggerFactory.getLogger(name);
}
在查看getILoggerFactory()
之前哲身,先看一下org.slf4j.ILoggerFactory
接口的定義:
public interface ILoggerFactory {
public Logger getLogger(String name); // 根據(jù)Logger的名字返回Logger對象
}
再看一下org.slf4j.Logger
接口的定義(只列出主要方法):
public interface Logger {
public String getName(); // 返回logger實例的名稱
public void trace(String format, Object... arguments);
public void debug(String format, Object... arguments);
public void info(String format, Object... arguments);
public void warn(String format, Object... arguments);
public void error(String format, Object... arguments);
public boolean isTraceEnabled();
public boolean isDebugEnabled();
public boolean isInfoEnabled();
public boolean isWarnEnabled();
public boolean isErrorEnabled();
}
就是打印各級別的日志,以及判斷當前的日志級別贸伐。
好勘天,繼續(xù)查看org.slf4j.LoggerFactory#getILoggerFactory()
的源碼
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) { // 是否是未初始化狀態(tài)?注意volatile關(guān)鍵字的使用
// 同步鎖捉邢,所有線程互斥脯丝,只允許1個線程進行下面的初始化操作
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) { // 雙重檢查
INITIALIZATION_STATE = ONGOING_INITIALIZATION; // 設(shè)置狀態(tài)為進行中
performInitialization(); // 執(zhí)行初始化
}
}
}
switch (INITIALIZATION_STATE) { // 判斷初始化狀態(tài)
case SUCCESSFUL_INITIALIZATION: // 成功初始化
// 返回StaticLoggerBinder中定義的ILoggerFactory的實現(xiàn)類
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION: // 未找到StaticLoggerBinder類
// 返回?zé)o操作(即所有實現(xiàn)的都是空)的org.slf4j.helpers.NOPLoggerFactor
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION: // 初始化失敗,拋異常
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION: // 仍是進行中的狀態(tài)
return SUBST_FACTORY;
}
// 如果以上的狀態(tài)都不是伏伐,則拋異常
throw new IllegalStateException("Unreachable code");
}
核心代碼是org.slf4j.LoggerFactory#performInitialization()
方法:
private final static void performInitialization() {
bind(); // 與具體的日志實現(xiàn)綁定
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) { // 如果成功初始化
versionSanityCheck(); // 對版本兼容性的檢查宠进,不再看了
}
}
繼續(xù)org.slf4j.LoggerFactory#bind()
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
if (!isAndroid()) { // 我們當前并不是Android環(huán)境,會執(zhí)行if語句塊的內(nèi)容
// 查找所有StaticLoggerBinder類的路徑
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
// 如果找到了多個StaticLoggerBinder類藐翎,并且不確定使用哪個時材蹬,會打印錯誤信息
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// 最關(guān)鍵的一句代碼
// 首先看StaticLoggerBinder類的路徑是 org.slf4j.impl.StaticLoggerBinder
// 注意:它并不在 slf4j-api.jar 中,而是在 slf4j-log4j12.jar 中
// 即:slf4j-api.jar 中定義的是高層接口吝镣,實現(xiàn)與具體日志框架綁定是在 slf4j-[日志框架].jar 中
// 如果項目中只有 slf4j-api.jar 的話堤器,那么就會因為找不到 StaticLoggerBinder
// 而進入到 catch (NoClassDefFoundError ncde) 代碼塊
// 此句代碼確保了一定要找到StaticLoggerBinder類,并且能夠執(zhí)行g(shù)etSingleton()方法
StaticLoggerBinder.getSingleton();
// 找到了StaticLoggerBinder赤惊,表示初始化成功
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 向控制臺打印實際綁定的日志框架
reportActualBinding(staticLoggerBinderPathSet);
......
// 如果找不到StaticLoggerBinder類,會執(zhí)行以下代碼
} 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;
}
// 如果能找StaticLoggerBinder類凰锡,但是找不到getSingleton()方法未舟,會執(zhí)行以下代碼
} 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);
}
}
小結(jié)一下:此方法的實現(xiàn)就是找org.slf4j.impl.StaticLoggerBinder
的過程圈暗。接下來關(guān)注一下具體的過程。
- org.slf4j.LoggerFactory#findPossibleStaticLoggerBinderPathSet()
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>(); // 注意:是有序的Set
try {
/*
* 通過ClassLoader加載所有的 org/slf4j/impl/StaticLoggerBinder.class
*/
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) { // 可能有多個
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path); // 添加到Set中
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet; // 返回Set
}
org/slf4j/impl/StaticLoggerBinder.class
可能有多個裕膀,但是目前這個類只在 slfj4-log4j12.jar 中有1個员串。
- org.slf4j.LoggerFactory#reportMultipleBindingAmbiguity(Set<URL>)
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
// 找到的這些StaticLoggerBinder是否是有歧義的?
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Class path contains multiple SLF4J bindings.");
for (URL path : binderPathSet) { // 如果找到多個則打印錯誤信息
Util.report("Found binding in [" + path + "]");
}
// 打印一個網(wǎng)址昼扛,用于解釋多個綁定的情況:https://www.slf4j.org/codes.html#multiple_bindings
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}
private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
return binderPathSet.size() > 1; // 找到多個StaticLoggerBinder就是有歧義的
}
由于目前只找到1個寸齐,所以沒有歧義。
- org.slf4j.LoggerFactory.reportActualBinding(Set<URL>)
private static void reportActualBinding(Set<URL> binderPathSet) {
// 當有歧義時抄谐,打印出實際綁定的那個StaticLoggerBinder
if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
}
}
至此渺鹦,綁定過程我們就分析完了。簡而言之蛹含,就是:查找并加載StaticLoggerBinder
的過程毅厚。
org.slf4j.impl.StaticLoggerBinder
接下來,我們看一下 slf4j-log4j12.jar 中的 org.slf4j.impl.StaticLoggerBinder
浦箱。
package org.slf4j.impl;
import org.apache.log4j.Level;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.Util;
import org.slf4j.spi.LoggerFactoryBinder;
public class StaticLoggerBinder implements LoggerFactoryBinder {
private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
public static final StaticLoggerBinder getSingleton() { // 構(gòu)建單例
return SINGLETON;
}
public static String REQUESTED_API_VERSION = "1.6.99"; // !final
private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();
private final ILoggerFactory loggerFactory;
private StaticLoggerBinder() {
loggerFactory = new Log4jLoggerFactory(); // 重點
try {
@SuppressWarnings("unused")
Level level = Level.TRACE;
} catch (NoSuchFieldError nsfe) {
Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
}
}
@Override
public ILoggerFactory getLoggerFactory() {
return loggerFactory; // 返回 Log4jLoggerFactory 對象
}
@Override
public String getLoggerFactoryClassStr() {
return loggerFactoryClassStr;
}
}
接下來吸耿,我們來看org.slf4j.impl.Log4jLoggerFactory
的源碼(有刪減)
package org.slf4j.impl;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.log4j.LogManager;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
// 實現(xiàn)了 slf4j-api.jar 中的 ILoggerFactory 接口
public class Log4jLoggerFactory implements ILoggerFactory {
// key - logger的名字
// value - org.slf4j.Logger接口的實現(xiàn)類,是個適配器 org.slf4j.impl.Log4jLoggerAdapter
// 考慮并發(fā)的情況酷窥,使用了 ConcurrentMap
ConcurrentMap<String, Logger> loggerMap;
public Log4jLoggerFactory() {
loggerMap = new ConcurrentHashMap<String, Logger>();
// force log4j to initialize
org.apache.log4j.LogManager.getRootLogger();
}
@Override
public Logger getLogger(String name) {
Logger slf4jLogger = loggerMap.get(name);
if (slf4jLogger != null) { // 找到直接返回
return slf4jLogger;
} else {
// 先獲取log4j的Logger對象
org.apache.log4j.Logger log4jLogger;
if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME))
log4jLogger = LogManager.getRootLogger();
else
log4jLogger = LogManager.getLogger(name);
// 通過 Log4jLoggerAdapter 來將log4j的Logger適配為slf4j的Logger
Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
}
邏輯很清楚咽安,接下來的重點就是Log4jLoggerAdapter
,這個典型的適配器模式的實現(xiàn)(有刪減):
package org.slf4j.impl;
// org.slf4j.spi.LocationAwareLogger 繼承了 org.slf4j.Logger 接口
public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements
LocationAwareLogger, Serializable {
final transient org.apache.log4j.Logger logger;
Log4jLoggerAdapter(org.apache.log4j.Logger logger) {
this.logger = logger;
}
// 實現(xiàn) org.slf4j.Logger 接口中的方法
@Override
public void info(String format, Object... argArray) {
if (logger.isInfoEnabled()) {
FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
// 實際是通過log4j的API進行打印日志
logger.log(FQCN, Level.INFO, ft.getMessage(), ft.getThrowable());
}
}
......
}
通過適配器實現(xiàn)了:當我們編程時面向的是org.slf4j.Logger
蓬推,而實際執(zhí)行是通過org.apache.log4j.Logger
執(zhí)行打印日志等操作妆棒。
總結(jié)
對 slf4j + slf4j-log4j12 + log4j 的運行原理進行一下總結(jié):
-
org.slf4j.LoggerFactory.getLogger(Class)
方法的邏輯就是通過ClassLoader查找所有的org.slf4j.impl.StaticLoggerBinder.class
,并確定最終實際使用的那一個拳氢。 -
StaticLoggerBinder
是個單例募逞,并實現(xiàn)了LoggerFactoryBinder
接口,用以實現(xiàn)slf4j與日志框架實現(xiàn)之間的綁定馋评。它可以返回一個ILoggerFactory
接口的實現(xiàn)類放接,在 slf4j-log4j12.jar 中是Log4jLoggerFactory
。 -
Log4jLoggerFactory
中必須返回一個org.slf4j.Logger
接口的實現(xiàn)類留特,這個類是Log4jLoggerAdapter
-
Log4jLoggerAdapter
是一個典型的適配器模式的實現(xiàn)纠脾,它的功能是將log4j的org.apache.log4j.Logger
適配成org.slf4j.Logger
- 當執(zhí)行
org.slf4j.Logger.info()
方法時,實際的執(zhí)行者是Log4jLoggerAdapter.info()
蜕青,內(nèi)部是通過log4j的API完成日志的打印苟蹈。
為什么引入了logback就會破壞之前的日志架構(gòu)?
通過上面的分析右核,我們知道StaticLoggerBinder
是實現(xiàn)slf4j和日志輸出框架之間綁定的橋梁慧脱,但如果classpath下有多個StaticLoggerBinder
會怎樣呢?
實驗代碼見:https://github.com/wanghui0101/learning-slf4j/tree/master/learning-slf4j-multiple-bindings
在pom.xml
中添加了logback的jar包贺喝。
此時菱鸥,訪問Servlet宗兼,控制臺會打印2條語句:
Hello Slf4j ! - debug
Hello Slf4j ! - info
并且,在項目啟動時氮采,在控制臺還會打印出5句錯誤信息:
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/xxx/learning-slf4j-multiple-bindings/WEB-INF/lib/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/xxx/learning-slf4j-multiple-bindings/WEB-INF/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
通過之前的源碼分析殷绍,我們可以得知
第1-4句是org.slf4j.LoggerFactory.reportMultipleBindingAmbiguity(Set<URL>)
打印出來的。
第5句是org.slf4j.LoggerFactory.reportActualBinding(Set<URL>)
打印出來的鹊漠。
意思就是在classpath下找到了多個StaticLoggerBinder
主到,并自動選擇了logback的ContextSelectorStaticBinder
。那么躯概,此時slf4j-log4j12.jar就不再起作用登钥,而log4j.xml
也不會起作用了。這樣楞陷,就出現(xiàn)了文章最開始提出的問題中描述的情況怔鳖。
那么,為什么會先加載logback的StaticLoggerBinder
呢固蛾?這是與ClassLoader的類加載機制雙親委派模型有關(guān)的结执。同一個ClassLoader對于同一個類只會加載一次,所以即使classpath下有多個StaticLoggerBinder.class
艾凯,也只有1個會被加載献幔,而且在我們當前的環(huán)境下恰好加載的就是logback的StaticLoggerBinder
。
因此趾诗,對于slf4j來說蜡感,查找到多個StaticLoggerBinder.class
并不會影響它的執(zhí)行,只是打印出警告信息而已恃泪。
更多關(guān)于此問題的解釋郑兴,見:http://www.slf4j.org/codes.html#multiple_bindings
如何避免類似的沖突?
由于slf4j-log4j12.jar或logback.jar中都包含org/slf4j/impl/StaticLoggerBinder.class贝乎,為了不讓slf4j加載時產(chǎn)生歧義情连,需要移除這兩者之一,使slf4j只與1個明確的日志實現(xiàn)綁定览效。
記兹匆ā:始終要保證,在classpath下锤灿,只有1個org/slf4j/impl/StaticLoggerBinder.class挽拔。
作為服務(wù)提供方,我們提供的jar如果依賴了日志相關(guān)的jar但校,應(yīng)該依賴哪些螃诅?
通過上面的分析,如作為服務(wù)提供方,依賴的日志jar包术裸,只包括slf4j-api.jar即可空执。多余的jar包(如slf4j-log4j12.jar或logback.jar),可能會引起服務(wù)使用方的日志架構(gòu)出現(xiàn)問題穗椅。
面向slf4j的接口API編程的最佳實踐是什么?
盡管可以通過 slf4j-api + slf4j-log4j12 + log4j 或 slf4j-api + logback 實現(xiàn)與底層日志實現(xiàn)框架的綁定奶栖,但這樣做的前提是項目中所有類匹表,以及依賴的所有jar,都是面向slf4j的API編程宣鄙。盡管我們可以約束項目組成員袍镀,保證所有類都是面向slf4j編程的,但項目所依賴的jar卻無法控制冻晤。
為此苇羡,slf4j還提供了橋接機制,將其它日志實現(xiàn)API鼻弧,通過slf4j设江,轉(zhuǎn)換成我們項目中綁定的日志實現(xiàn)。
舉例:Spring框架中使用了commons-logging作為了日志輸出框架攘轩,那么slf4j提供了jcl-over-slf4j.jar將commons-logging的實現(xiàn)先橋接到slf4j叉存,再通過logback輸出(如果你選擇的日志實現(xiàn)是logback的話)。但是度帮,要注意:如果使用了jcl-over-slf4j.jar歼捏,就必須排除commons-logging.jar,因為jcl-over-slf4j.jar中已經(jīng)包括了commons-logging中的所有類笨篷。
另外瞳秽,一個干凈的SpringBoot項目,為我們提供了使用slf4j的最佳實踐的范例率翅。根據(jù)
可以搭建一個干凈的SpringBoot項目练俐。它引入的日志相關(guān)jar有
jcl-over-slf4j.jar // 將commons-logging橋接到slf4j
jul-to-slf4j.jar // 將java.util.logging橋接到slf4j
log4j-over-slf4j.jar // 將log4j的API橋接到slf4j
slf4j-api.jar // slf4j的API
logback-classic.jar // logback作為項目的日志實現(xiàn)框架
logback-core.jar
這些jar被定義在 spring-boot-starter-logging/pom.xml 中(
),我們也可依此安聘,規(guī)范項目的日志框架痰洒。
更多
slf4j關(guān)于多次綁定的解釋:
http://www.slf4j.org/codes.html#multiple_bindings
slf4j橋接遺留的日志API:
https://www.slf4j.org/legacy.html
slf4j的用戶手冊: