Spring筆記(10) - 日誌體系 logback的使用和logback.xml詳解

一、概況

  在項目開發當中,日誌對於我們開發或運維人員來說,是一個必不可少的工具。在線下我們可以通過 debug 來查找排除問題,但對於線上系統來說,我們只能通過日誌分析來查找問題,我們可以通過日誌打印來獲取我們需要的信息來判斷、分析系統運行結果是否正常或哪裏出現了問題,可以定位到具體問題和位置。

  當前流行的日誌框架有:

  • jul(java util logging)
  • log4j
  • log4j2
  • jcl(Jakarta Commons Logging)       
  • logback
  • slf4j

二、應用和探討

  1.jul(java util logging)

  java自帶的日誌記錄技術(java.util.logging.Logger),可以直接記錄日誌;功能比較太過於簡單,不支持佔位符顯示,拓展性比較差;

import java.util.logging.Logger;
public class JUL {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger("JUL");
        logger.info("java util logging");
    }
}

  2.log4j

  不支持使用佔位符,在高併發日誌量大的情況存在bug,容易導致內存、CPU衝高;

  使用 log4j 需要 pom 文件中引入 log4j 所需要的jar包和引入 log4j 的配置文件 log4j.properties

pom.xml
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
log4j.properties

log4j.rootLogger = info,stdout
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n     
import org.apache.log4j.Logger;
public class Log4j {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Log4j.class);
        logger.info("log4j");
    }
}

    • 記住 jul 和 log4j 日誌輸出格式的差別,顏色不一致:jul 輸出日誌爲紅色,log4j 輸出日誌是白色;

  3.log4j2 

  log4j2 和 log4j 是同一個作者開發,只不過log4j2是重新架構的一款日誌組件,改進了log4j的bug,以及吸取了優秀的logback的設計重新推出的一款新組件,在效率和性能上有了很大的提升;

  必須同時依賴 log4j-core 和 log4j-api,否則報錯:“ERROR StatusLogger Log4j2 could not find a logging implementation”;

pom.xml
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.13.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.9.1</version>
        </dependency>

 同時需要配置文件和測試代碼:log4j2.xml、Log4j2.java 

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</Configuration>
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4j2 {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger("Log4j2");
        logger.info("log4j2");
    }
}

 

  4.jcl(Jakarta Commons Logging)

  Spring Framework 4.x 版本依賴了commons-logging.jar;Spring Framework 5.x 版本 依賴了spring-jcl.jar;

    Spring Framework 4.x :

      

    Spring Framework 5.x :

    

    1.commons-logging.jar:Spring Framework 4.x 

pom.xml
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.20.RELEASE</version>
        </dependency><dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class JCL {
    public static void main(String[] args) {
        Log log = LogFactory.getLog("JCL");
        log.info("jakatra common logging");
    }
}

    跟上面的jul輸出格式一致;

    探討:爲什麼 jcl 的輸出格式是 jul 格式呢?

    接下來從源碼來進行分析:從 LogFactory.getLog("JCL") 開始,從下面可以看出是從工廠

LogFactorypublic static Log getLog(String name) throws LogConfigurationException {
        return getFactory().getInstance(name);
    }
   getInstance(name)是一個抽象方法,看它的實現方法:
LogFactorypublic abstract Log getInstance(String name)
        throws LogConfigurationException;
LogFactoryImplpublic Log getInstance(String name) throws LogConfigurationException {
        Log instance = (Log) instances.get(name);//開始是null
        if (instance == null) {
            instance = newInstance(name);//創建實例
            instances.put(name, instance);//instances是一個Hashtable,實例放入緩存中
        }
        return instance;
    }

    下面的代碼就一行重要:

LogFactoryImplprotected Log newInstance(String name) throws LogConfigurationException {
        Log instance;
        try {
            if (logConstructor == null) {
                instance = discoverLogImplementation(name);//創建實例
            }
            else {
                Object params[] = { name };
                instance = (Log) logConstructor.newInstance(params);
            }

            if (logMethod != null) {
                Object params[] = { this };
                logMethod.invoke(instance, params);
            }

            return instance;

        } catch (LogConfigurationException lce) {

            // this type of exception means there was a problem in discovery
            // and we've already output diagnostics about the issue, etc.;
            // just pass it on
            throw lce;

        } catch (InvocationTargetException e) {
            // A problem occurred invoking the Constructor or Method
            // previously discovered
            Throwable c = e.getTargetException();
            throw new LogConfigurationException(c == null ? e : c);
        } catch (Throwable t) {
            handleThrowable(t); // may re-throw t
            // A problem occurred invoking the Constructor or Method
            // previously discovered
            throw new LogConfigurationException(t);
        }
    }

    在下面的代碼中可以看到實例的創建是從classesToDiscover遍歷獲取來的:

LogFactoryImplprivate Log discoverLogImplementation(String logCategory)
        throws LogConfigurationException {
        if (isDiagnosticsEnabled()) {
            logDiagnostic("Discovering a Log implementation...");
        }

        initConfiguration();

        Log result = null;

        // See if the user specified the Log implementation to use
        String specifiedLogClassName = findUserSpecifiedLogClassName();

        if (specifiedLogClassName != null) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("Attempting to load user-specified log class '" +
                    specifiedLogClassName + "'...");
            }

            result = createLogFromClass(specifiedLogClassName,
                                        logCategory,
                                        true);
            if (result == null) {
                StringBuffer messageBuffer =  new StringBuffer("User-specified log class '");
                messageBuffer.append(specifiedLogClassName);
                messageBuffer.append("' cannot be found or is not useable.");

                // Mistyping or misspelling names is a common fault.
                // Construct a good error message, if we can
                informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LOG4J_LOGGER);
                informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_JDK14_LOGGER);
                informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LUMBERJACK_LOGGER);
                informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_SIMPLE_LOGGER);
                throw new LogConfigurationException(messageBuffer.toString());
            }

            return result;
        }

        if (isDiagnosticsEnabled()) {
            logDiagnostic(
                "No user-specified Log implementation; performing discovery" +
                " using the standard supported logging implementations...");
        }
        for(int i=0; i<classesToDiscover.length && result == null; ++i) {
            result = createLogFromClass(classesToDiscover[i], logCategory, true);//創建實例
        }

        if (result == null) {
            throw new LogConfigurationException
                        ("No suitable Log implementation");
        }

        return result;
    }        

    從 classesToDiscover 數組中可以看到,數組存放具體的日誌框架的類名,通過循環數組依次去匹配這些類名是否在項目中被依賴了,如果找到依賴則直接使用,有先後順序,log4j>jul。

LogFactoryImplprivate 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"
    };

    下面使用commons-logging.jar和 log4j :下面的運行結果驗證了上面的源碼

pom.xml<dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

    2.spring-jcl.jar:Spring Framework 5.x

pom.xml
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency><dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jcl</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class JCL {
    public static void main(String[] args) {
        Log log = LogFactory.getLog("JCL");
        log.info("jakatra common logging");
    }
}

   從下圖可以看出,spring-jcl 默認還是使用 jul:

  接下來還是從源碼來進行分析查看它的運行機制:從 LogFactory.getLog("JCL")開始,從下面可以看到它是從一個適配器獲取日誌對象的

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

  從下面代碼可以看出,LogAdapter 在初始化時,會執行一個 static 的代碼塊,設置 logApi 的值,依次用 log4j2,slf4j-LAL(可以將 log4j 橋接到 slf4j),slf4j 去反射判斷是否存在對應依賴,如果有則設置 logApi 爲對應值,否則默認爲 jul,然後在 createLog 時,switch-case 根據 logApi 去獲取 log 對象,默認是 jul 來實現。

LogAdapter:

    private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";//log4j2

    private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";//slf4j綁定log4j

    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;//log4j2
            }
        }
        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;
        }
    }
    //加載類:加載到指定的類返回true,否則返回false(加載不到指定類會拋出異常)
    private static boolean isPresent(String className) {
        try {
            Class.forName(className, false, LogAdapter.class.getClassLoader());
            return true;
        }
        catch (ClassNotFoundException ex) {
            return false;
        }
    }    
    public static Log createLog(String name) {
        switch (logApi) {
            case LOG4J://log4j2
                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);
        }
    }

   總結:

    commons-logging.jar(Spring Framework 4.x )封裝了一個靜態數組(支持 jul 和log4j),然後依次循環遍歷反射對應的依賴,如果對應依賴存在則返回對應實現,默認爲 jul,其中 log4j>jul

    spring-jcl.jar(Spring Framework 5.x)修改了commons-logging.jar,通過適配器獲取日誌對象,支持 jul 和 log4j2(不支持 log4j,如果項目需要 log4j,需要 slf4j 配合使用),還拓展支持 slf4j,有着更好的擴展性和兼容性;

    如下案例: 

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
log4j.properties:

log4j.rootLogger = Console,stdout
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout 
@Configuration
@ComponentScan("com.hrh.log")
public class Config {
}

public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        context.start();
    }
}

 

    當上面的依賴變爲 Spring Framework 4.x 時是可以打印日誌:

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.20.RELEASE</version>
        </dependency>

 

    Spring Framework 5.x 下log4j 配合 slf4j 使用輸出日誌,上面的pom依賴修改爲:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
        </dependency>

  5.logback

  log4j 的進化版,logback 除了具備 log4j 的所有優點之外,還解決了 log4j 不能使用佔位符的問題。

  logback-classic依賴和logback.xml配置文件

pom.xml        
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.7</version>
        </dependency>
logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
        </layout>
    </appender>
    <logger name="com.hrh.log" level="TRACE"/>
    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogBack {


    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger("logBack");
        logger.trace("Trace Level.");
        logger.debug("Debug Level.");
        logger.info("Info Level.");
        logger.warn("Warn Level.");
        logger.error("Error Level.");
        logger.info("{},it's OK.","Hi");//使用{}做佔位符
    }
}

 

  6.slf4j

   爲了方便原先直接使用日誌庫(日誌的核心功能實現: jul、log4j、log4j2、logback等)輸出日誌的形式轉換爲日誌門面(slf4j、jcl)輸出的形式提供的適配器。

  前面的 jul、log4j、log4j2、logback 是實現日誌的日誌庫,簡單日誌可以使用 jul,複雜日誌實現可以使用 logback 或 log4j2。很多時候我們的項目是從簡單到複雜一代代迭代過來的,日誌實現也是從簡單到複雜,開始我們使用jul,後面系統複雜了我們需要使用 log4j2 日誌框架,這時候我們如何將原來的日誌輸出在新的日誌架構下實現呢?

  一個死板的方法是對代碼一行行進行修改,把之前用 jul 的日誌代碼全部修改成 log4j2 的日誌接口。但是這種方式不僅效率低下,而且做的工作都是重複性的工作,實際工作中不採用該方式。

  這時候我們就需要一個叫做 slf4j(Simple Logging Facade for Java,即Java簡單日誌記錄接口集,也可以叫日誌門面)的東西了,它是一個日誌的接口規範,它對用戶提供了統一的日誌接口(使用Facade門面設計模式),屏蔽了底層不同日誌組件的差異和實現細節,使用者無需關注底層實現的具體日誌庫。

  slf4j本身不記錄日誌,通過綁定器綁定一個具體的日誌框架來完成日誌記錄。即slf4j 允許最終用戶在部署時插入所需的日誌框架,如果在項目中使用 slf4j 必須加一個依賴jar,即 slf4j-api-xxx.jar。而當我們需要更換日誌組件的時候,我們只需要更換一個具體的日誌組件 jar 包就可以了。

  比如說我們現在有個 app,通過 slf4j 打印日誌,slf4j 需要通過一個綁定器(slf4j-jdk14-1.8.0-beta2)來綁定到我們的 jul 來輸出日誌。如果這時候需要在 app 中集成 spring4(spring4是利用jcl來打印日誌的),這時候呢,考慮到系統的日誌統一,可以使用橋接器(jcl-over-slf4j)將 jcl 橋接到 slf4j 來,然後通過 binding 到 jul 來輸出日誌,保證系統日誌風格統一。

 

  下面是綁定類型(日誌門面適配器)介紹:

綁定類型 說明
slf4j-log4j12-xxx.jar 綁定log4j,依賴log4j-xxx.jar,輸出log4j日誌
slf4j-jdk14-xxx.jar 綁定jul,輸出jul日誌
slf4j-jcl-xxx.jar 綁定jcl,默認輸出jul日誌,有log4j則輸出log4j
logback-classic-xxx.jar 綁定logback,依賴logback-core-xxx.jar,輸出logback日誌

  下面是橋接類型(日誌庫適配器)介紹:

  比如左上角的第一個是將 jcl(或log4j、jul) 橋接到 slf4j,最後統一輸出爲 logback 日誌;

  所以綜上所述:

    綁定器綁定最後輸出的日誌類型;

    橋接器就是將指定日誌類型橋接到 slf4j,最後統一輸出綁定器綁定的日誌類型,可以解釋爲指定日誌類型輸出的日誌轉換爲綁定器綁定的日誌類型;

    應用宜採用日誌橋接、適配(綁定)機制,將 jul、log4j、jcl 等日誌框架橋接到 slf4j,適配性能更高的 log4j2 日誌輸出框架打印;

  小貼士:對於日誌量大的勿輸出到控制檯(Console);

  下面是幾個簡單案例實現:

1)slf4j+jul

pom.xml
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jdk14</artifactId>
            <version>1.7.21</version>
        </dependency>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4jJULLog {
    public static void main(String[] args) {
            Logger logger = LoggerFactory.getLogger(Slf4jJULLog.class);
            logger.trace("Trace Level.");
            logger.info("Info Level.");
            logger.warn("Warn Level.");
            logger.error("Error Level.");
    }
}

2)slf4j+log4j

pom.xml

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
log4j.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</Configuration>
public class Slf4jLog4JLog {
    public static void main(String[] args) {
            Logger logger = LoggerFactory.getLogger(Slf4jLog4JLog.class);
            logger.trace("Trace Level.");
            logger.info("Info Level.");
            logger.warn("Warn Level.");
            logger.error("Error Level.");
    }
}

  注意:當我們使用slf4j跟其他日誌實現來搭建日誌系統時,可能會存在一些循環引用導致棧溢出的問題。

  如下案例:

pom.xml
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>1.7.25</version>
        </dependency>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4jLog4JLog {
    public static void main(String[] args) {
            Logger logger = LoggerFactory.getLogger(Slf4jLog4JLog.class);
            logger.trace("Trace Level.");
            logger.info("Info Level.");
            logger.warn("Warn Level.");
            logger.error("Error Level.");
    }
}

   上面就是一個由於循環引用導致棧溢出的錯誤,這時因爲綁定器綁定 log4j 時在 log4j 又經過橋接器接到 slf4j,然後又經過綁定器,依次循環,最後棧溢出。

   這個問題在實際場景中較常見,比如我們一個 app 使用 slf4j 綁定器綁定到 log4j,最後通過 log4j 輸出日誌,這時有個 jar 包 X1,使用 slf4j 綁定輸出到 jul,然後 X2需要集成 X2,X2 使用 log4j 輸出日誌,X1 在集成 X2時,使用橋接器(log4j-over-slf4j),保證 X1 的日誌最後都是通過 jul 輸出的。這時候app 集成 X1 便會出現 log4j 的橋接器和綁定器共存的情況,便會出現上面棧溢出的錯誤,這時候我們就需要修改我們的 app 了。

  最後來下總結:

 三.擴展

  log4j2更多詳情參考:log4j2 的使用【超詳細圖文】

  logback更多詳情參考:logback的使用和logback.xml詳解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章