Java日志體系(slf4j)

3 slf4j

3.1 簡介

與commons-logging相同,slf4j也是一個通用的日志接口,在程序中與其他日志框架結合使用,并對外提供服務。

Simple Logging Facade for Java簡稱 slf4j,Java簡單日志門面系統。在我們的代碼中,不需要顯式指定具體日志框架(例如:java.util.logging、logback、log4j),而是使用slf4j的API來記錄日志便可,最終日志的格式、記錄級別、輸出方式等通過具體日志框架的配置來實現,因此可以在應用中靈活切換日志系統。

如果你對上面所說的,仍然不太理解。那么,簡單的說slf4j可以理解為JDBC,都是提供接口服務,只不過比JDBC更為直觀、簡單些。在程序中,JDBC需要單獨指定具體的數據庫實現(例如:mysql),而slf4j并不需要。

接下來,我們講解下關于slf4j具體的使用。

3.2 slf4j結構

上面的截圖,展示的是slf4j搭配log4j使用。

Logger:slf4j日志接口類,提供了trace < debug < info < warn < error這5個級別對應的方法,主要提供了占位符{}的日志打印方式;

Log4jLoggerAdapter:Logger適配器,主要對org.apache.log4j.Logger對象的封裝,占位符{}日志打印的方式在此類中實現;

LoggerFactory:日志工廠類,獲取實際的日志工廠類,獲取相應的日志實現對象;

lLoggerFactory:底層日志框架中日志工廠的中介,再其實現類中,通過底層日志框架中的日志工廠獲取對應的日志對象;

StaticLoggerBinder:靜態日志對象綁定,在編譯期確定底層日志框架,獲取實際的日志工廠,也就是lLoggerFactory的實現類;

3.2 使用

同為Java日志接口框架,相對于commons-logging來說,slf4j的使用有點特殊。

在第一篇的文章中,筆者介紹了commons-logging的使用,對于commons-logging來說,無需在pom.xml文件中單獨引入日志實現框架,便可進行日志打印。但是,slf4j并不支持此功能,必須在pom.xml中單獨引入底層日志實現。

搭配log4j使用:
首先,需要在pom.xml文件中添加依賴:

//slf4j:
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.20</version>
</dependency>

//slf4j-log4j:
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</version>
</dependency>

//log4j:
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

聲明測試代碼:

public class slf4j_log4jDemo {

    Logger logger = LoggerFactory.getLogger(slf4j_log4jDemo.class);

    @Test
    public void test() throws IOException {
        logger.error("Error Message!");
        logger.warn("Warn Message!");
        logger.info("Info Message!{}","你好");
        logger.debug("Debug Message!");
        logger.trace("Trace Message!");
    }
}

接下來,在classpath下定義配置文件:log4j.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>
    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
        <param name="Target" value="System.out" />
        <param name="ImmediateFlush" value="true"/>
        <param name="encoding" value="UTF-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %t %-5p (%c:%L) - %m%n"/>
        </layout>
    </appender>
    <root>
        <priority value="debug" />
        <appender-ref ref="CONSOLE" />
    </root>
</log4j:configuration>

對于slf4j來說,它只提供了一個核心模塊--slf4j-api,這個模塊下只有日志接口,沒有具體的實現,所以在實際開發總需要單獨添加底層日志實現。但是,這些底層日志類實際上跟slf4j并沒有任何關系,因此slf4j又通過增加一層日志中間層來轉換相應的實現,例如上文中的slf4j-log4j12。

上圖,是官方文檔中slf4j與其他日志框架相結合的使用情況,具體總結如下:

logback:logback-classic 、logback-core

java.util.logging.Logging:slf4j-jdk14

commons-logging:jcl-over-slf4j

其中,commons-logging比較特殊。由于commons-logging誕生的比較早,一些年限久遠的系統大體上都使用了commons-logging和log4j的日志框架組合,大名鼎鼎的spring框架也依然在使用commons-logging框架。那么,此時你的新系統如果想使用slf4j該如何處理?

這會,就需要引入jcl-over-slf4j.jar包了,它會將commons-logging的“騙入”到slf4j中來,實現日志框架結合;

3.3 源碼分析

以下源碼基于slf4j-1.7.20、slf4j-log4j12-1.7.12和log4j-1.2.17(使用slf4j和log4j結合):

org.slf4j.LoggerFactory類:

public final class LoggerFactory {
 
    static final int UNINITIALIZED = 0;
    static final int ONGOING_INITIALIZATION = 1;
    static final int FAILED_INITIALIZATION = 2;
    static final int SUCCESSFUL_INITIALIZATION = 3;
    static final int NOP_FALLBACK_INITIALIZATION = 4;

    private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
    
    //初始化狀態,默認為0;
    static int INITIALIZATION_STATE = UNINITIALIZED;

    //獲取日志對象:
    public static Logger getLogger(Class<?> clazz) {
        //獲取日志對象:
        Logger logger = getLogger(clazz.getName());
        ......
    }
    
     //獲取日志對象,分為兩個階段:
    public static Logger getLogger(String name) {
        
        //獲取日志工廠:實際為Log4jLoggerFactory:
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        
        //通過Log4jLoggerFactory獲取日志對象:
        return iLoggerFactory.getLogger(name);
    }


    //獲取日志工廠:
    public static ILoggerFactory getILoggerFactory() {
        
        //判斷初始化狀態:默認為0
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            //將初始化狀態至為1:
            INITIALIZATION_STATE = ONGOING_INITIALIZATION;
            
            //slf4j初始化操作:
            performInitialization();
        }
        
        //完成初始化后,判斷初始化結果:
        switch (INITIALIZATION_STATE) {
            case SUCCESSFUL_INITIALIZATION:
                //通過StaticLoggerBinder單例對象,創建LoggerFactory實例:實際為Log4jLoggerFactory
                return StaticLoggerBinder.getSingleton().getLoggerFactory();
            case NOP_FALLBACK_INITIALIZATION:
                return NOP_FALLBACK_FACTORY;
            case FAILED_INITIALIZATION:
                throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
            case ONGOING_INITIALIZATION:
                return TEMP_FACTORY;
        }
        throw new IllegalStateException("Unreachable code");
    }

    //slf4j初始化流程:
    private final static void performInitialization() {
        
        //靜態綁定,獲取StaticLoggerBinder對象;
        bind();

        //判斷初始化狀態:如果初始化成功,則進行版本檢查;
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }

    //靜態綁定操作:找到與slf4j相結合的日志框架;
    private final static void bind() {
        try {
            //在類路徑下,查找org.slf4j.impl.StaticLoggerBinder類:
            Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            
            //遍歷Set集合,并將其中StaticLoggerBinder類的路徑打印出來:
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            
            //創建StaticLoggerBinder的對象:
            StaticLoggerBinder.getSingleton();
            
            //初始化完成,修改初始化狀態:
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            
            //如果在類路徑下有多個StaticLoggerBinder類,此方法打印出具體實例化了哪個StaticLoggerBinder類:
            reportActualBinding(staticLoggerBinderPathSet);
            
            fixSubstitutedLoggers();
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage();
            if (msg != null && msg.indexOf("org.slf4j.impl.StaticLoggerBinder.getSingleton()") != -1) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

    
    //在類路徑下,查找org.slf4j.impl.StaticLoggerBinder類:
    private static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        
        try {
            //獲取LoggerFactory的類加載器:
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            
            Enumeration<URL> paths;
            
            //判斷類加載器是否為null:
            if (loggerFactoryClassLoader == null) {
                //查找org.slf4j.impl.StaticLoggerBinder類:
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                //查找org.slf4j.impl.StaticLoggerBinder類:
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
    
            //遍歷Enumeration對象:
            while (paths.hasMoreElements()) {
                //將StaticLoggerBinder類存在的路徑添加到Set集合中:
                URL path = (URL) paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException ioe) {}
        return staticLoggerBinderPathSet;
    }


    //如果存在多個StaticLoggerBinder類,就打印每個StaticLoggerBinder類的路徑:
    private static void reportMultipleBindingAmbiguity(Set<URL> staticLoggerBinderPathSet) {
        //判斷Set集合長度:
        if (isAmbiguousStaticLoggerBinderPathSet(staticLoggerBinderPathSet)) {
            Util.report("Class path contains multiple SLF4J bindings.");
            Iterator<URL> iterator = staticLoggerBinderPathSet.iterator();
            while (iterator.hasNext()) {
                URL path = (URL) iterator.next();
                Util.report("Found binding in [" + path + "]");
            }
            Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
        }
    }

    //如果類路徑下的StaticLoggerBinder類不止一個的話,就打印出具體實例化的對象是哪個:
    private static void reportActualBinding(Set<URL> staticLoggerBinderPathSet) {
        //判斷Set集合長度:
        if (isAmbiguousStaticLoggerBinderPathSet(staticLoggerBinderPathSet)) {
            Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
        }
    }

    //檢查slf4j-api和slf4j-log4j12的版本是否兼容;
    private final static void versionSanityCheck() {
        try {
            //獲取StaticLoggerBinder的api版本,也就是slf4j-log4j12所屬版本
            String requested = StaticLoggerBinder.REQUESTED_API_VERSION;
            boolean match = false;
            
            //判斷StaticLoggerBinder的api版本是否屬于slf4j-api所支持的版本:
            for (int i = 0; i < API_COMPATIBILITY_LIST.length; i++) {
                if (requested.startsWith(API_COMPATIBILITY_LIST[i])) {
                    match = true;
                }
            }
            if (!match) {.....}
        } catch (java.lang.NoSuchFieldError nsfe) {
        } catch (Throwable e) {}
    }
}

org.slf4j.impl.StaticLoggerBinder類:

public class StaticLoggerBinder implements LoggerFactoryBinder {
    
    //StaticLoggerBinder構造方法:private修飾,單例;
    private StaticLoggerBinder() {
        //創建Log4jLoggerFactory對象:
        loggerFactory = new Log4jLoggerFactory();
        ....
    }
    
    //單例對象:
    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
    
    //供外部調用,得到單例的StaticLoggerBinder對象:
    public static final StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }
    
    //獲取ILoggerFactory對象,實際為Log4jLoggerFactory;
    public ILoggerFactory getLoggerFactory() {
        return loggerFactory;
    }
}

org.slf4j.impl.Log4jLoggerFactory類:

public class Log4jLoggerFactory implements ILoggerFactory {

    // 線程安全的ConcurrentHashMap對象,保存日志對象;
    ConcurrentMap<String, Logger> loggerMap;

    public Log4jLoggerFactory() {
        loggerMap = new ConcurrentHashMap<String, Logger>();
    }
    
    //創造Logger對象:實際返回的是與slf4j相結合的日志對象;
    public Logger getLogger(String name) {
        
        //通過類名稱獲取日志對象:
        Logger slf4jLogger = loggerMap.get(name);
        
        //不為空,則返回:
        if (slf4jLogger != null) {
            return slf4jLogger;
        } else {
            //log4j日志對象:
            org.apache.log4j.Logger log4jLogger;

            if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) {
                //開始log4j的初始化過程:
                log4jLogger = LogManager.getRootLogger();
            }else {
                //開始log4j的初始化過程:
                log4jLogger = LogManager.getLogger(name);
            }
            
            //通過Log4jLoggerAdapter對象,對log4j的日志對象進行封裝,使用了適配器模式:
            Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
            Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
}

具體流程總結如下:

1.結合上面的例子,當slf4j_log4jDemo測試類被加載的時候,slf4j開始了初始化操作:
    Logger logger = LoggerFactory.getLogger(slf4j_log4jDemo.class);

2.slf4j初始化操作分為2個階段,第一階段獲取日志工廠,第二階段通過日志工廠獲取日志對象;

3.在第一階段中,首先通過classloader查找classpath下存在的org/slf4j/impl/StaticLoggerBinder.class類,可能有多個。在我們的測試例子中,實際上找到的是slf4j-log4j12包下的org.slf4j.impl.StaticLoggerBinder類

4.其次,實例化StaticLoggerBinder對象,調用getLoggerFactory方法獲取對應的loggerFactory,也就是slf4j-log4j12包下的org.slf4j.impl.Log4jLoggerFactory,并返回;

5.上面過程結束后,loggerFactory被創建,緊接著通過Log4jLoggerFactory的getLogger方法獲取log4j的日志對象,使用的是最原生的方法log4j的LogManager來實現,最終返回org.apache.log4j.Logger.log4jLogger對象;

6.由于log4j的日志對象org.apache.log4j.Logger.log4jLogger與slf4j的org.slf4j.Logger日志接口并無多態關系,所以此時slf4j引入了一個org.slf4j.impl.Log4jLoggerAdapter類,該類實現了slf4j的Logger接口,再其內部維護一個log4j的日志對象log4jLogger,使用的是適配器模式,進而達到了整合;在程序中,我們使用日志api的時候,實際上都是Log4jLoggerAdapter類來完成的。

3.4 slf4j靜態綁定原理

雖然commons-logging和slf4j都是日志服務接口,但是兩者對于底層日志框架綁定的方式相差甚遠。在第一篇日志系統的文章中,筆者已經介紹過,commons-logging是基于動態綁定來實現與日志框架的結合,也就是說在編譯期間我們的程序并不知道底層的實現是什么,只有在運行期間才進行獲取;

與commons-logging不同的是,slf4j是基于靜態綁定來實現與日志框架的結合,在編譯期間我們的程序就已經知道使用了哪種日志實現。在上面的源碼中已有所提及,下面再回顧下。

具體源碼,如下:

public final class LoggerFactory {

    private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

    //靜態綁定操作:找到與slf4j相結合的日志框架,在編譯期間完成日志綁定操作;
    private final static void bind() {
        try {
            //在類路徑下,查找org.slf4j.impl.StaticLoggerBinder類:
            Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            。。。。。               
    }

    //在類路徑下,查找org.slf4j.impl.StaticLoggerBinder類:
    private static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        try {
            //獲取LoggerFactory的類加載器:
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            //判斷類加載器是否為null:
            if (loggerFactoryClassLoader == null) {
                //查找org.slf4j.impl.StaticLoggerBinder類:
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                //查找org.slf4j.impl.StaticLoggerBinder類:
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            ......
        } catch (IOException ioe) {}
        return staticLoggerBinderPathSet;
    }
}

3.5 slf4j和commons-logging比較

(1)slf4j使用了靜態綁定方式,實現了與底層日志框架的結合, 避免了commons-logging中由于類加載器不同導致的日志加載失敗情況的發生;

(2)slf4j支持參數化日志打印,也就是占位符{}的方式。去除了commons-logging中的isDebugEnabled(), isInfoEnabled()等方法的日志級別檢查代碼,極大的提高了代碼可讀性;并且,占位符的方式也延緩了構建日志信息(String的開銷),提高了內存的使用性;

在commons-logging中,我們經常需要些這樣的代碼:

if (logger.isDebugEnabled()) {
    logger.debug("我是: " + name);
}

而在slf4j中,我們可以這樣寫:

logger.debug("我是: {}",name);

在commons-logging中,是要符合日記級別,我們就進行字符串的拼接;而在slf4j中,我們不進行字符串拼接操作,而是使用StringBuffer來完成的替換。這不僅降低了內存消耗而且預先降低了CPU去處理字符串連接命令的時間,提高了程序的性能。

3.6 slf4j搭配commons-logging使用原理

在前面的小節中,我們提到了slf4j為了兼容老代碼,是可以跟commons-logging結合使用的,需要在pom.xml文件中引入jcl-over-slf4j.jar包。具體實現過程如下:

測試代碼:(引入的依舊為commons-logging對象,無需改變)

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;

public class commons_loggingDemo {
    Log log= LogFactory.getLog(commons_loggingDemo.class);
    @Test
    public void test() throws IOException {
        log.debug("Debug info.");
        log.info("Info info");
        log.warn("Warn info你好");
        log.error("Error info");
        log.fatal("Fatal info");
    }
}

引入pom依賴:(除了原有的commons-logging和log4j依賴外,還需要添加slf4j-api、jcl-over-slf4j、slf4j-log4j12依賴)

!-- commons-logging -->
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.3</version>
</dependency>

<!--將commons-logging引入到slf4j中去-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.20</version>
</dependency>

 <!--log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<!--slf4j-log4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</version>
</dependency>

 <!--slf4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.20</version>
</dependency>

日志配置文件: (均為commons-logging時期配置,無需為slf4j做任何改變)

commons-logging.properties配置文件:
#日志對象:
org.apache.commons.logging.Log=org.apache.log4j.Logger
#日志工廠:
org.apache.commons.logging.LogFactory=org.apache.commons.logging.impl.LogFactoryImpl

log4j.xml配置文件:
<log4j:configuration>
    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
        <param name="Target" value="System.out" />
        <param name="ImmediateFlush" value="true"/>
        <param name="encoding" value="UTF-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %t %-5p (%c:%L) - %m%n"/>
        </layout>
    </appender>
    <root>
        <priority value="debug" />
        <appender-ref ref="CONSOLE" />
    </root>
</log4j:configuration>

實現原理:

將commons-logging的輸出引入到jcl-over-slf4j中,再轉向slf4j,緊接著進入到slf4j-log4j12,最終進入到log4j;

源碼分析:(請結合commons-logging初始化過程進行學習)

commons-logging中的org.apache.commons.logging.LogFactory類:

public abstract class LogFactory {
    
    protected static final String SERVICE_ID = "META-INF/services/org.apache.commons.logging.LogFactory";

    public static LogFactory getFactory() throws LogConfigurationException {
        ClassLoader contextClassLoader = getContextClassLoaderInternal();
        if (factory == null) {
            。。。忽略
            try {
                
                //在classpath下尋找 META-INF/services/org.apache.commons.logging.LogFactory 文件:(在jcl-over-slf4j.jar中) 
                final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
                
                if( is != null ) {
                    BufferedReader rd;
                    try {
                        rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                    } catch (java.io.UnsupportedEncodingException e) {
                        rd = new BufferedReader(new InputStreamReader(is));
                    }
                    
                    //讀取該文件中的第一行信息:org.apache.commons.logging.impl.SLF4JLogFactory
                    String factoryClassName = rd.readLine();
                    
                    rd.close();

                    if (factoryClassName != null && ! "".equals(factoryClassName)) {
                          。。。。忽略
                        
                        //SLF4JLogFactory進行實例化(在jcl-over-slf4j.jar中)
                        factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
                    }
                } else {
                   。。。。。忽略
                }
            } catch (Exception ex) { 。。。。。忽略}
        }
        。。。。。忽略
        return factory;
    }
}

jcl-over-slf4j的org.apache.commons.logging.impl.SLF4JLogFactory中:

public class SLF4JLogFactory extends LogFactory {
    
    //獲取日志對象:
    public Log getInstance(Class clazz) throws LogConfigurationException {
        return (getInstance(clazz.getName()));
    }

    public Log getInstance(String name) throws LogConfigurationException {
        Log instance = loggerMap.get(name);
        if (instance != null) {
            return instance;
        } else {
            Log newInstance;

            //此處使用的是slf4j中的LoggerFactory進行日志對象獲取,此時已經完全將commons-logging引入到了slf4j中來;
            Logger slf4jLogger = LoggerFactory.getLogger(name);
            if (slf4jLogger instanceof LocationAwareLogger) {
                newInstance = new SLF4JLocationAwareLog((LocationAwareLogger) slf4jLogger);
            } else {
                newInstance = new SLF4JLog(slf4jLogger);
            }
            Log oldInstance = loggerMap.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
}

以上便完成了,commons-logging結合slf4j的過程,后續獲取日志對象的邏輯與3.3節中完全一致,可參考上面的講解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容