日誌框架體系整理( 基礎 )

紫邪情發表於2022-05-24

0、前言

  • 本篇部落格的初衷是為了特定的人弄的,這些內容在網上都可以找到原文,因此:這篇部落格只是保證能夠讓特定的人看懂,其他人看得懂就看,看不懂拉倒,同時,這篇部落格中細節說明沒有、執行截圖沒有、特別備註沒有......

1、JUL

  • 指的是Java Util Logging包,它是java原生的日誌框架,使用時不需要另外引用第三方的類庫,相對其他的框架使用方便,學習簡單,主要是使用在小型應用中


1.1、JUL的組成結構

image

  • Logger:被稱為記錄器,應用程式通過獲取Logger物件,抵用其API來發布日誌資訊。Logger通常被認為是訪問日誌系統的入口程式。

  • Handler:處理器,每個Logger都會關聯一個或者是一組Handler,Logger會將日誌交給關聯的Handler去做處理,由Handler負責將日誌做記錄。Handler具體實現了日誌的輸出位置,比如可以輸出到控制檯或者是檔案中等等。

  • Filter:過濾器,根據需要定製哪些資訊會被記錄,哪些資訊會被略過。

  • Formatter:格式化元件,它負責對日誌中的資料和資訊進行轉換和格式化,所以它決定了我們輸出日誌最終的形式。

  • Level:日誌的輸出級別,每條日誌訊息都有一個關聯的級別。我們根據輸出級別的設定,用來展現最終所呈現的日誌資訊。根據不同的需求,去設定不同的級別。



1.2、入門

  • 可以直接去JDK文件中檢視java.util.logging這個包

image


public class JULTest {

    @Test
    public void test01(){
        /*
            日誌入口程式 java.util.logging.Logger
         */
        // Logger物件的建立方式,不能直接new物件
        // 取得物件的方法引數,需要引入當前類的全路徑字串(當前我們先這麼用,以後根據包結構有Logger父子關係,以後詳細介紹)
        Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest");

        /*
            對於日誌的輸出,有兩種方式

            第一種方式:
                直接呼叫日誌級別相關的方法,方法中傳遞日誌輸出資訊
                假設現在我們要輸出info級別的日誌資訊
         */
        //logger.info("輸出info資訊1");
        /*
            第二種方式:
                呼叫通用的log方法,然後在裡面通過Level型別來定義日誌的級別引數,以及搭配日誌輸出資訊的引數
         */
        //logger.log(Level.INFO,"輸出info資訊2");


        /*
            輸出學生資訊
                姓名
                年齡
         */
        /*String name = "zs";
        int age = 23;
        logger.log(Level.INFO,"學生的姓名為:"+name+";年齡為:"+age);*/

        /*
            對於輸出訊息中,字串的拼接弊端很多
            1.麻煩
            2.程式效率低
            3.可讀性不強
            4.維護成本高

            我們應該使用動態生成資料的方式,生產日誌
            我們使用的就是佔位符的方式來進行操作
         */
        String name = "zs";
        int age = 23;
        logger.log(Level.INFO,"學生的姓名:{0},年齡:{1}",new Object[]{name,age});
    }
}


1.3、日誌級別

    @Test
    public void test02(){

        /*
            日誌的級別(通過原始碼檢視,非常簡單)

              SEVERE : 錯誤 --- 最高階的日誌級別
              WARNING : 警告
              INFO : (預設級別)訊息
              			原始碼:Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->350行左右有一個 defaultlevel屬性
              CONFIG : 配置
              FINE : 詳細資訊(少)
              FINER : 詳細資訊(中)
              FINEST : 詳細資訊 (多) --- 最低階的日誌級別

            兩個特殊的級別
               OFF 可用來關閉日誌記錄
               ALL 啟用所有訊息的日誌記錄

            對於日誌的級別,重點關注的是new物件的時候的第二個引數,是一個數值(原始碼中有)
            OFF Integer.MAX_VALUE 整型最大值
            SEVERE 1000
            WARNING 900
            ...
            ...
            FINEST 300
            ALL Integer.MIN_VALUE 整型最小值

            這個數值的意義在於,如果設定的日誌的級別是INFO -- 800
            那麼最終展現的日誌資訊,必須是數值大於800的所有的日誌資訊
            最終展現的就是
            SEVERE
            WARNING
            INFO
         */

        Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest");

        /*
            通過列印結果,我們看到了僅僅只是輸出了info級別以及比info級別高的日誌資訊
            比info級別低的日誌資訊沒有輸出出來
            證明了info級別的日誌資訊,它是系統預設的日誌級別
            在預設日誌級別info的基礎上,列印比它級別高的資訊
         */

        /*
            如果僅僅只是通過以下形式來設定日誌級別
            那麼不能夠起到效果
            將來需要搭配處理器handler共同設定才會生效
         */
        logger.setLevel(Level.CONFIG);

        logger.severe("severe資訊");
        logger.warning("warning資訊");
        logger.info("info資訊");
        logger.config("config資訊");
        logger.fine("fine資訊");
        logger.finer("finer資訊");
        logger.finest("finest資訊");

    }



1.4、自定義日誌級別

1.4.1、輸出資訊在console上

    @Test
    public void test03(){

        /*
            自定義日誌的級別
         */
        // 日誌記錄器
        Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest");

        /*
            將預設的日誌列印方式關閉掉
            引數設定為false,列印日誌的方式就不會按照父logger預設的方式去進行操作
        */
        logger.setUseParentHandlers(false);

        /*
            處理器Handler
            這裡使用的是控制檯日誌處理器,取得處理器物件
        */
        ConsoleHandler handler = new ConsoleHandler();
        // 建立日誌格式化元件物件
        SimpleFormatter formatter = new SimpleFormatter();

        // 在處理器中設定輸出格式
        handler.setFormatter(formatter);
        // 在記錄器中新增處理器
        logger.addHandler(handler);

        /* 
            設定日誌的列印級別
            此處必須將日誌記錄器和處理器的級別進行統一的設定,才會達到日誌顯示相應級別的效果
            logger.setLevel(Level.CONFIG);
            handler.setLevel(Level.CONFIG);
        */

        // 設定如下的all級別之後,那麼下面所有的輸出資訊都會列印到控制檯,在需要的時候改成對應的日誌級別即可
        logger.setLevel(Level.ALL);
        handler.setLevel(Level.ALL);

        logger.severe("severe資訊");
        logger.warning("warning資訊");
        logger.info("info資訊");
        logger.config("config資訊");
        logger.fine("fine資訊");
        logger.finer("finer資訊");
        logger.finest("finest資訊");

    }



1.4.2、輸出資訊在磁碟上

    @Test
    public void test04() throws IOException {

        /*
            將日誌輸出到具體的磁碟檔案中
            這樣做相當於是做了日誌的持久化操作
         */
        Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest");
        logger.setUseParentHandlers(false);

        // 檔案日誌處理器
        FileHandler handler = new FileHandler("D:\\test\\" + this.getClass().getSimpleName() + ".log");
        SimpleFormatter formatter = new SimpleFormatter();
        handler.setFormatter(formatter);
        logger.addHandler(handler);

        // 也可以同時在控制檯和檔案中進行列印
        ConsoleHandler handler2 = new ConsoleHandler();
        handler2.setFormatter(formatter);
        // 可以在記錄器中同時新增多個處理器,這樣磁碟和控制檯中都可以輸出對應級別的日誌資訊了
        logger.addHandler(handler2);

        logger.setLevel(Level.ALL);
        handler.setLevel(Level.ALL);
        handler2.setLevel(Level.CONFIG);

        logger.severe("severe資訊");
        logger.warning("warning資訊");
        logger.info("info資訊");
        logger.config("config資訊");
        logger.fine("fine資訊");
        logger.finer("finer資訊");
        logger.finest("finest資訊");


        /*
            總結:
                使用者使用Logger來進行日誌的記錄,Logger可以持有多個處理器Handler
                (日誌的記錄使用的是Logger,日誌的輸出使用的是Handler)
                新增了哪些handler物件,就相當於需要根據所新增的handler
                將日誌輸出到指定的位置上,例如控制檯、檔案..
         */
    }



1.5、logger的父子關係

    @Test
    public void test05(){

        /*
            Logger之間的父子關係
                JUL中Logger之間是存在"父子"關係的
                值得注意的是,這種父子關係不是我們普遍認為的類之間的繼承關係
                關係是通過樹狀結構儲存的
         */



        /*
            從下面建立的兩個logger物件看來
            我們可以認為logger1是logger2的父親
         */

        /* 
            父親是RootLogger,名稱預設是一個空的字串
            RootLogger可以被稱之為所有logger物件的頂層logger
        */
        // 這就是父
        Logger logger1 = Logger.getLogger("cn.zixieqing.jul.test");

        // 這是子,甚至cn.zixieqing.jul也是這個logger2的父
        Logger logger2 = Logger.getLogger("cn.zixieqing.jul.test.JULTest");

        //System.out.println(logger2.getParent()==logger1); //true

        System.out.println("logger1的父Logger引用為:"
                +logger1.getParent()+"; 名稱為"+logger1.getName()+"; 父親的名稱為"+logger1.getParent().getName());


        System.out.println("logger2的父Logger引用為:"
                +logger2.getParent()+"; 名稱為"+logger2.getName()+"; 父親的名稱為"+logger2.getParent().getName());


        /*
            父親所做的設定,也能夠同時作用於兒子
            對logger1做日誌列印相關的設定,然後我們使用logger2進行日誌的列印
         */
        // 父親做設定
        logger1.setUseParentHandlers(false);
        ConsoleHandler handler = new ConsoleHandler();
        SimpleFormatter formatter = new SimpleFormatter();
        handler.setFormatter(formatter);
        logger1.addHandler(handler);
        handler.setLevel(Level.ALL);
        logger1.setLevel(Level.ALL);

        // 兒子做列印 - 結果就是:兒子logger2沒做配置,但是父親logger1做了,所以兒子logger2的輸出級別就是父親的級別
        logger2.severe("severe資訊");
        logger2.warning("warning資訊");
        logger2.info("info資訊");
        logger2.config("config資訊");
        logger2.fine("fine資訊");
        logger2.finer("finer資訊");
        logger2.finest("finest資訊");
    }


小結:看原始碼的結果


    // JUL在初始化時會建立一個頂層RootLogger作為所有Logger的父Logger,java.util.logging.LogManager$RootLogger,預設的名稱為空串

    // 檢視原始碼Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->350行左右:
    owner.rootLogger = owner.new RootLogger();
    RootLogger是LogManager的內部類

	/*
		以上的RootLogger物件作為樹狀結構的根節點存在的
        將來自定義的父子關係通過路徑來進行關聯
        父子關係,同時也是節點之間的掛載關係
	*/

    // 350行左右
    owner.addLogger(owner.rootLogger);
    addLogger ----> LoggerContext cx = getUserContext(); 
    /* 
		LoggerContext一種用來儲存節點的Map關係,WeakHashMap<Object, LoggerContext> contextsMap
		點進WeakHashMap<Object, LoggerContext>的LoggerContext,裡面的結構為:Hashtable<String,LoggerWeakRef> namedLoggers 
	*/

	// 再點開Hashtable<String,LoggerWeakRef> namedLoggers 的LoggerWeakRef,得到的資訊如下:
    private String                name;       // for namedLoggers cleanup,這玩意就是節點,也就是說父子關係也就是節點掛載關係
    private LogNode               node;       // for loggerRef cleanup
    private WeakReference<Logger> parentRef;  // for kids cleanup



1.6、使用配置檔案

1.6.1、預設配置檔案所在地

  • 檢視原始碼
	Logger.getLogger() --->demandLogger -----> getLogManager() -----> ensureLogManagerInitialized()--->345行左右:

	有這麼一行程式碼:owner.readPrimordialConfiguration();

	點選readPrimordialConfiguration(),在340行左右有一句readConfiguration();

	點選readConfiguration();在1290行左右,有如下的程式碼
	    String fname = System.getProperty("java.util.logging.config.file");
        if (fname == null) {
            fname = System.getProperty("java.home");
            if (fname == null) {
                throw new Error("Can't find java.home ??");
            }
            File f = new File(fname, "lib");
            f = new File(f, "logging.properties");
            fname = f.getCanonicalPath();
        }


結論:預設配置檔案所在地

  • java.home( 即:安裝JDK的目錄 ) --> 找到jre資料夾 --> lib --> logging.properties

分析一下上述目錄中的logging.properties檔案,找打這個目錄下的此檔案開啟

# RootManager預設使用的處理器
# 若是想要配置多個處理器,可以使用逗號進行分開,如:java.util.logging.ConsoleHandler,java.util.logging.FileHandler
handlers= java.util.logging.ConsoleHandler

# RootManager的預設日誌級別,這是全域性的日誌級別
# 若不手動進行級別配置,那麼預設使用INFO及更高的級別進行輸出
.level= INFO

# 檔案處理器設定
# 日誌檔案的路徑
# 	  %h/java%u.log h指的是使用者目錄 - 不分window還是linux
#	  java%u.log是生成的檔名,其中:u相當於是自增,從0開始的
#	  				如:java0.log、java1.log、java2.log......這裡生成多少份由java.util.logging.FileHandler.count = 1這個配置決定
java.util.logging.FileHandler.pattern = %h/java%u.log
# 日誌檔案的限制 - 預設50000位元組
java.util.logging.FileHandler.limit = 50000
# 日誌檔案的數量 - 預設1份
java.util.logging.FileHandler.count = 1
# 日誌檔案的格式,預設XML格式,也可以採用SimpleFormatter
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# 控制檯處理器設定
# 控制檯預設級別
java.util.logging.ConsoleHandler.level = INFO
# 控制檯預設輸出格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# 這是這個源文件中舉的一個例子,意思是像下面這樣,可將日誌級別設定到具體的某個包下
# com.xyz.foo.level = SEVERE



1.6.2、修改成更友好點的配置檔案

# 自定義Logger
cn.zixieqing.handlers=java.util.logging.FileHandler
# 自定義Logger日誌級別
cn.zixieqing.level=SERVER
# 遮蔽父logger的日誌設定,相當於前面玩的logger.setUseParentHandlers(false);
cn.zixieqing.useParentHandlers=false

# RootManager預設使用的處理器
# 若是想要配置多個處理器,可以使用逗號進行分開,如:java.util.logging.ConsoleHandler,java.util.logging.FileHandler
handlers= java.util.logging.ConsoleHandler

# RootManager的預設日誌級別,這是全域性的日誌級別
.level= SERVER

# 檔案處理器設定
# 日誌檔案的路徑
# 	  %h/java%u.log h指的是使用者目錄 - 不分window還是linux
#	  java%u.log是生成的檔名,其中:u相當於是自增,從0開始的
#	  				如:java0.log、java1.log、java2.log......這裡生成多少份由java.util.logging.FileHandler.count = 1這個配置決定
java.util.logging.FileHandler.pattern = %h/java%u.log
# 日誌檔案的限制 - 50000位元組
java.util.logging.FileHandler.limit = 50000
# 日誌檔案的數量
java.util.logging.FileHandler.count = 1
# 日誌檔案的格式
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

# 控制檯處理器設定
# 控制檯預設級別
java.util.logging.ConsoleHandler.level = SERVER
# 控制檯預設輸出格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# 下一次生成的日誌檔案預設是覆蓋了上一此的檔案 - 改為在原日誌上進行追加
java.util.logging.FileHandler.append=true



1.6.3、使用自定義的配置檔案

    @Test
    public void test06() throws Exception {

        // 載入自定義的配置檔案
        InputStream input = new FileInputStream("D:\\test\\logging.properties");

        // 取得日誌管理器物件
        LogManager logManager = LogManager.getLogManager();

        // 讀取自定義的配置檔案
        logManager.readConfiguration(input);

        Logger logger = Logger.getLogger("cn.zixieqing.jul.test.JULTest");

        logger.severe("severe資訊");
        logger.warning("warning資訊");
        logger.info("info資訊");
        logger.config("config資訊");
        logger.fine("fine資訊");
        logger.finer("finer資訊");
        logger.finest("finest資訊");
    }



1.7、總結:JUL原理

  • 1、初始化LogManager

    • LogManager載入logging.properties配置檔案
    • 新增Logger到LogManager
  • 2、從單例的LogManager獲取Logger,即:LogManager.getLogManager();

  • 3、設定日誌級別Level,在列印的過程中使用到了日誌記錄的LogRecord類,原始碼找尋如下:

    • 1、點選logger.severe("severe資訊");中的severe,當然點選其他warning、info、config也是可以進去的,之後會看到如下的程式碼
      	    public void severe(String msg) {
              	log(Level.SEVERE, msg);
          	}
      
      2、點選上述的log(),看到如下的程式碼
      	    public void log(Level level, String msg) {
                  if (!isLoggable(level)) {
                      return;
                  }
      			// 這裡就是目的地
                  LogRecord lr = new LogRecord(level, msg);
                  doLog(lr);
              }
      
      
  • 4、Filter作為過濾器提供了日誌級別之外更細粒度的控制

  • 5、Handler日誌處理器,決定日誌的輸出位置,例如控制檯、檔案...

  • 6、Formatter是用來格式化輸出的



2、LOG4J

  • 全稱:log for java

2.1、LOG4J的組成

  • Loggers (日誌記錄器):控制日誌的輸出以及輸出級別(JUL做日誌級別Level)
  • Appenders(輸出控制器):指定日誌的輸出方式(輸出到控制檯、檔案等)
  • Layout(日誌格式化器):控制日誌資訊的輸出格式


2.1.1、Loggers日誌記錄器

  • 負責收集處理日誌記錄,例項的命名就是類的全限定名,如:cn.zixieqing.test,同時:Logger的名字大小寫敏感,其命名有繼承機制( 和JUL中的父子關係一樣,以包路徑來區分的 );另外:root logger是所有logger的根 - 上輩所做的日誌屬性設定,會直接的影響到子輩
    // root logger的獲取
    Logger.getRootLogger();


日誌級別

  • DEBUG
  • INFO
  • WARN
  • ERROR
  • ........

  • 大小關係:ERROR > WARN > INFO > DEBUG

  • 輸出規則:輸出日誌的規則是:只輸出級別不低於設定級別的日誌資訊
    • 如:假設Loggers級別設定為INFO,則INFO、WARN、ERROR級別的日誌資訊都會輸出,而級別比INFO低的DEBUG則不會輸出


2.1.2、Appenders輸出控制器

  • 允許把日誌輸出到不同的地方,如控制檯(Console)、檔案(Files)等,可以根據時間或者檔案大小產生新的檔案,可以以流的形式傳送到其它地方等等

  • 常用Appenders輸出控制器型別:
    • ConsoleAppender 將日誌輸出到控制檯
    • FileAppender 將日誌輸出到檔案中
    • DailyRollingFileAppender 根據指定時間輸出到一個新的檔案, 將日誌輸出到一個日誌檔案
    • RollingFileAppender 根據指定檔案大小,當檔案大小達到指定大小時,會自動把檔案改名,產生一個新的檔案,將日誌資訊輸出到一個日誌檔案
    • JDBCAppender 把日誌資訊儲存到資料庫中


2.2、玩LOG4J

2.2.1、入門

  • 依賴
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>


public class Log4jTest01 {

    @Test
    public void test01(){

        // 載入初始化配置
        BasicConfigurator.configure();
        // 注意:這個logger是apache下的,前面玩的JUL中的是java。util.logging包下的
        Logger logger = Logger.getLogger(Log4jTest01.class);

        logger.fatal("fatal資訊");
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }
}
  • 注意載入初始化資訊:BasicConfigurator.configure();,不加這一句程式碼就報錯 - 沒有新增Appenders輸出控制器,加上不報錯是因為:原始碼中有這樣一句程式碼rootManager.addAppenders( XxxxAppender( PatternLayout layout ) ),configure原始碼如下:
    public static void configure() {
        Logger root = Logger.getRootLogger();
        root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n")));
    }


LOG4J日誌級別

  • Log4j提供了8個級別的日誌輸出,分別為如下級別:
    • ALL 最低等級 用於開啟所有級別的日誌記錄
    • TRACE 程式推進下的追蹤資訊,這個追蹤資訊的日誌級別非常低,一般情況下是不會使用的
    • DEBUG 指出細粒度資訊事件對除錯應用程式是非常有幫助的,主要是配合開發,在開發過程中列印一些重要的執行資訊,在沒有進行設定的情況下,預設的日誌輸出級別
    • INFO 訊息的粗粒度級別執行資訊
    • WARN 表示警告,程式在執行過程中會出現的有可能會發生的隱形的錯誤
      • 注意,有些資訊不是錯誤,但是這個級別的輸出目的就是為了給程式設計師以提示
    • ERROR 系統的錯誤資訊,發生的錯誤不影響系統的執行
      • 一般情況下,如果不想輸出太多的日誌,則使用該級別即可
    • FATAL 表示嚴重錯誤,它是那種一旦發生系統就不可能繼續執行的嚴重錯誤
      • 如果這種級別的錯誤出現了,表示程式可以停止執行了
    • OFF 最高等級的級別,使用者關閉所有的日誌記錄


2.2.2、分析原始碼

        BasicConfigurator.configure();

        Logger logger = Logger.getLogger(Log4jTest01.class);

        logger.fatal("fatal資訊");
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");



分析 BasicConfigurator.configure();點選configure()

  • 得到的程式碼如下
    public static void configure() {
        Logger root = Logger.getRootLogger();
        root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n")));
    }

  • 從中可以得到幾個資訊:

    • 1、建立根節點的物件Logger root = Logger.getRootLogger();
    • 2、根節點新增了ConsoleAppender物件(表示預設列印到控制檯,自定義的格式化輸出PatternLayout)
  • 那麼想要自定義配置檔案來實現上述原始碼的功能呢?通過上面這個原始碼分析,我們需要具備如下的條件:

    • 們的配置檔案需要提供Logger、Appender、Layout這3個元件資訊


分析Logger logger = Logger.getLogger(Log4jTest01.class);點選getLogger()

    public static Logger getLogger(Class clazz) {
        return LogManager.getLogger(clazz.getName());
    }

  • 發現:LogManager.getLogger(clazz.getName());,其中:LogManagerj就是日誌管理器


檢視LogManager

  • 首先看到如下資訊,一堆常量
    public static final String DEFAULT_CONFIGURATION_FILE = "log4j.properties";
    static final String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml";
    /** @deprecated */
    public static final String DEFAULT_CONFIGURATION_KEY = "log4j.configuration";
    /** @deprecated */
    public static final String CONFIGURATOR_CLASS_KEY = "log4j.configuratorClass";
    /** @deprecated */
    public static final String DEFAULT_INIT_OVERRIDE_KEY = "log4j.defaultInitOverride";
    private static Object guard = null;
    private static RepositorySelector repositorySelector;

  • 這些東西代表的就是不同形式(不同字尾名)的配置檔案,其中log4j.properties屬性使我們最常使用的,因為它語法簡單、使用方便


log4j.properties的載入時機
載入 - 那就是static
觀察LogManager中的程式碼,找到其中的靜態程式碼塊static

  • 發現如下的一堆原始碼
    static {
        Hierarchy h = new Hierarchy(new RootLogger(Level.DEBUG));
        repositorySelector = new DefaultRepositorySelector(h);
        String override = OptionConverter.getSystemProperty("log4j.defaultInitOverride", (String)null);
        if (override != null && !"false".equalsIgnoreCase(override)) {
            LogLog.debug("Default initialization of overridden by log4j.defaultInitOverrideproperty.");
        } else {
            String configurationOptionStr = OptionConverter.getSystemProperty("log4j.configuration", (String)null);
            String configuratorClassName = OptionConverter.getSystemProperty("log4j.configuratorClass", (String)null);
            URL url = null;
            if (configurationOptionStr == null) {
                url = Loader.getResource("log4j.xml");
                if (url == null) {
                    // 前面就是檔案格式的一堆判斷,這裡才是log4j.propertie格式做的事情
                    url = Loader.getResource("log4j.properties");
                }
            } else {
                try {
                    url = new URL(configurationOptionStr);
                } catch (MalformedURLException var7) {
                    url = Loader.getResource(configurationOptionStr);
                }
            }

            if (url != null) {
                LogLog.debug("Using URL [" + url + "] for automatic log4j configuration.");

                try {
                    // 這裡又是一個資訊:selectAndConfigure()翻譯就是選擇配置檔案
                    OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository());
                } catch (NoClassDefFoundError var6) {
                    LogLog.warn("Error during default initialization", var6);
                }
            } else {
                LogLog.debug("Could not find resource: [" + configurationOptionStr + "].");
            }
        }
    }

  • 從原始碼中,發現url = Loader.getResource("log4j.properties");

    • 從這句程式碼得到的資訊:系統預設是從當前的類路徑下找到log4j.properties,而若是maven工程,那麼:就應該在resources路徑下去找
  • 同時在上面的原始碼中發現 OptionConverter.selectAndConfigure(url, configuratorClassName, getLoggerRepository());



檢視selectAndConfigure()

  • 發現如下原始碼
    public static void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) {
        Configurator configurator = null;
        String filename = url.getFile();
        if (clazz == null && filename != null && filename.endsWith(".xml")) {
            clazz = "org.apache.log4j.xml.DOMConfigurator";
        }

        if (clazz != null) {
            LogLog.debug("Preferred configurator class: " + clazz);
            configurator = (Configurator)instantiateByClassName(clazz, Configurator.class, (Object)null);
            if (configurator == null) {
                LogLog.error("Could not instantiate configurator [" + clazz + "].");
                return;
            }
        } else {
            // 有用資訊在這裡,即 new PropertyConfigurator();建立了一個properties配置物件
            configurator = new PropertyConfigurator();
        }

        ((Configurator)configurator).doConfigure(url, hierarchy);
    }



檢視PropertyConfigurator類

  • 首先看到的就是如下的常量資訊
    static final String CATEGORY_PREFIX = "log4j.category.";
    static final String LOGGER_PREFIX = "log4j.logger.";
    static final String FACTORY_PREFIX = "log4j.factory";
    static final String ADDITIVITY_PREFIX = "log4j.additivity.";
    static final String ROOT_CATEGORY_PREFIX = "log4j.rootCategory";
	// 這是一個重要資訊
    static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";
	// 這也是一個重要資訊
    static final String APPENDER_PREFIX = "log4j.appender.";
    static final String RENDERER_PREFIX = "log4j.renderer.";
    static final String THRESHOLD_PREFIX = "log4j.threshold";
    private static final String THROWABLE_RENDERER_PREFIX = "log4j.throwableRenderer";
    private static final String LOGGER_REF = "logger-ref";
    private static final String ROOT_REF = "root-ref";
    private static final String APPENDER_REF_TAG = "appender-ref";
    public static final String LOGGER_FACTORY_KEY = "log4j.loggerFactory";
    private static final String RESET_KEY = "log4j.reset";
    private static final String INTERNAL_ROOT_NAME = "root";

  • 通過前面的基礎,從這原始碼中,發現有兩個資訊是要進行配置的
static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger";
static final String APPENDER_PREFIX = "log4j.appender.";

  • 那麼這二者是怎麼進行配置的?


找尋static final String APPENDER_PREFIX = "log4j.appender."中的appender配置方式
直接在當前原始碼頁面搜尋appender

  • 發現如下的原始碼
    Appender parseAppender(Properties props, String appenderName) {
        Appender appender = this.registryGet(appenderName);
        if (appender != null) {
            LogLog.debug("Appender \"" + appenderName + "\" was already parsed.");
            return appender;
        } else {
            // 重要資訊就在這裡,這裡告知了一件事:上面找到的log4j.appender.配置方式為如下的方式
            String prefix = "log4j.appender." + appenderName;
            // 這也是重要資訊,layout日誌格式化的配置方式
            String layoutPrefix = prefix + ".layout";
            appender = (Appender)OptionConverter.instantiateByKey(props, prefix, Appender.class, (Object)null);
            if (appender == null) {
                LogLog.error("Could not instantiate appender named \"" + appenderName + "\".");
                return null;
            } else {
                appender.setName(appenderName);
                if (appender instanceof OptionHandler) {
                    if (appender.requiresLayout()) {
                        Layout layout = (Layout)OptionConverter.instantiateByKey(props, layoutPrefix, Layout.class, (Object)null);
                        if (layout != null) {
                            appender.setLayout(layout);
                            LogLog.debug("Parsing layout options for \"" + appenderName + "\".");
                            PropertySetter.setProperties(layout, props, layoutPrefix + ".");
                            LogLog.debug("End of parsing for \"" + appenderName + "\".");
                        }
                    }

                    String errorHandlerPrefix = prefix + ".errorhandler";
                    String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props);
                    if (errorHandlerClass != null) {
                        ErrorHandler eh = (ErrorHandler)OptionConverter.instantiateByKey(props, errorHandlerPrefix, ErrorHandler.class, (Object)null);
                        if (eh != null) {
                            appender.setErrorHandler(eh);
                            LogLog.debug("Parsing errorhandler options for \"" + appenderName + "\".");
                            this.parseErrorHandler(eh, errorHandlerPrefix, props, this.repository);
                            Properties edited = new Properties();
                            String[] keys = new String[]{errorHandlerPrefix + "." + "root-ref", errorHandlerPrefix + "." + "logger-ref", errorHandlerPrefix + "." + "appender-ref"};
                            Iterator iter = props.entrySet().iterator();

                            while(true) {
                                if (!iter.hasNext()) {
                                    PropertySetter.setProperties(eh, edited, errorHandlerPrefix + ".");
                                    LogLog.debug("End of errorhandler parsing for \"" + appenderName + "\".");
                                    break;
                                }

                                Entry entry = (Entry)iter.next();

                                int i;
                                for(i = 0; i < keys.length && !keys[i].equals(entry.getKey()); ++i) {
                                }

                                if (i == keys.length) {
                                    edited.put(entry.getKey(), entry.getValue());
                                }
                            }
                        }
                    }

                    PropertySetter.setProperties(appender, props, prefix + ".");
                    LogLog.debug("Parsed \"" + appenderName + "\" options.");
                }

                this.parseAppenderFilters(props, appenderName, appender);
                this.registryPut(appender);
                return appender;
            }
        }
    }

  • 通過上述的原始碼,發現配置log4j.appender.的方式:log4j.appender.+appenderName

    • 其中:appenderName就是輸出控制器名字
  • 繼而:推匯出log4j.properties配置檔案中的一個配置項appender輸出方式為:log4j.appender.+appenderName=某一種輸出控制器名字

    • 其中:輸出控制器名字在前面一開始就接觸過了

image

  • 因此Log4j.properties的appender輸出方式配置方式舉例就是log4j.appender.console=org.apache.log4j.ConsoleAppender
  • 同樣道理,通過第二句程式碼 String layoutPrefix = prefix + ".layout";,也就知道了layout輸出格式的配置方式

image

  • layout日誌輸出格式配置舉例:log4j.appender.console.layout=org.log4j.SimpleLayout


小小總結一波:log4j.properties配置檔案中的appender輸出控制器 和 layout日誌輸出格式的配置方式

    log4j.appender,console=org.apache.log4j.ConsoleAppender
    log4j.appender.console.layout=org.log4j.SimpleLayout



繼續找第二個配置static final String ROOT_LOGGER_PREFIX = "log4j.rootLogger"中的rootLogger配置方式
通過log4j.rootLogge進行搜尋

  • 發現如下的方法
    void configureRootCategory(Properties props, LoggerRepository hierarchy) {
        String effectiveFrefix = "log4j.rootLogger";
        String value = OptionConverter.findAndSubst("log4j.rootLogger", props);
        if (value == null) {
            value = OptionConverter.findAndSubst("log4j.rootCategory", props);
            effectiveFrefix = "log4j.rootCategory";
        }

        if (value == null) {
            LogLog.debug("Could not find root logger information. Is this OK?");
        } else {
            Logger root = hierarchy.getRootLogger();
            synchronized(root) {
                // 這裡面執行了這個方式
                this.parseCategory(props, root, effectiveFrefix, "root", value);
            }
        }

    }



檢視parseCategory()

  • 在這裡找到了想要的配置方式
    void parseCategory(Properties props, Logger logger, String optionKey, String loggerName, String value) {
        LogLog.debug("Parsing for [" + loggerName + "] with value=[" + value + "].");
        // 配置方式就在這裡,這個操作的意思就是:表示要以逗號的方式來切割字串,證明了log4j.rootLogger的取值,可以有多個值,使用逗號進行分隔
        StringTokenizer st = new StringTokenizer(value, ",");
        if (!value.startsWith(",") && !value.equals("")) {
            if (!st.hasMoreTokens()) {
                return;
            }

            // 把字串通過逗號切割之後,第一個值的用途就在這裡 - levelStr、level,即:切割後的第一個值是日誌的級別
            String levelStr = st.nextToken();
            LogLog.debug("Level token is [" + levelStr + "].");
            if (!"inherited".equalsIgnoreCase(levelStr) && !"null".equalsIgnoreCase(levelStr)) {
                logger.setLevel(OptionConverter.toLevel(levelStr, Level.DEBUG));
            } else if (loggerName.equals("root")) {
                LogLog.warn("The root logger cannot be set to null.");
            } else {
                logger.setLevel((Level)null);
            }

            LogLog.debug("Category " + loggerName + " set to " + logger.getLevel());
        }

        logger.removeAllAppenders();

        // 字串切割之後的第一個值是level日誌級別,而剩下的值的用途就在這裡
        while(st.hasMoreTokens()) {
            // 通過這句程式碼得知:第2 - 第n個值,就是我們配置的其他資訊,這個資訊就是appenderName
            String appenderName = st.nextToken().trim();
            if (appenderName != null && !appenderName.equals(",")) {
                LogLog.debug("Parsing appender named \"" + appenderName + "\".");
                Appender appender = this.parseAppender(props, appenderName);
                if (appender != null) {
                    logger.addAppender(appender);
                }
            }
        }

    }

  • 通過上述的程式碼分析,得知log4j.rootLogger的配置方式為:log4j.rootLogger=日誌級別,appenderName1,appenderName2,appenderName3....
    • 表示可以同時在根節點上配置多個日誌輸出的途徑


2,2,3、最基本的log4j.properties配置

  • 通過前面的原始碼分析之後,得出BasicConfigurator.configure();替代品的properties配置如下:
# rootLogger所有logger的根配置 - log4j.rootLogger=日誌級別,appenderName1,appenderName2,appenderName3....
# 這裡的例子沒用日誌輸出路徑,這個日誌輸出路徑後續再加
log4j.rootLogger=debug,console
# appender輸出控制器配置 log4j.appender.+appenderName=某一種輸出型別 - 採用Console控制檯的方式舉例
log4j.appender.console=org.apache.log4j.ConsoleAppender
# 輸出的格式配置log4j.appender.+appenderName+layout=某種layout格式型別
log4j.appender.console.layout=org.apache.log4j.SimpleLayout


測試

        // 注掉這一句就可以了,這句程式碼的配置由上面的自定義配置進行代替了
        //BasicConfigurator.configure();

        Logger logger = Logger.getLogger(Log4jTest01.class);

        logger.fatal("fatal資訊");
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");



2.2.3、開啟日誌輸出的詳細資訊

    @Test
    public void test03(){

        /*
            通過Logger中的開關
                開啟日誌輸出的詳細資訊
                檢視LogManager類中的方法getLoggerRepository()
                找到程式碼LogLog.debug(msg, ex);
                LogLog會使用debug級別的輸出為我們展現日誌輸出詳細資訊
                Logger是記錄系統的日誌,那麼LogLog就是用來記錄Logger的日誌

                進入到LogLog.debug(msg, ex);方法中
                通過程式碼:if (debugEnabled && !quietMode) 
                觀察到if判斷中的這兩個開關都必須開啟才行
                !quietMode是已經啟動的狀態,不需要我們去管
                debugEnabled預設是關閉的
                所以我們只需要設定debugEnabled為true就可以了
         */
        // 開啟 debugEnabled
        LogLog.setInternalDebugging(true);

        Logger logger = Logger.getLogger(Log4jTest01.class);

        logger.fatal("fatal資訊");
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }

  • 若開啟debugEnabled,那麼輸出資訊就是如下的樣子( 如下是舉的一個例子而已)
0 [main] FATAL cn.zixieqing.HotelJavaApplicationTests  - fatal資訊
1 [main] ERROR cn.zixieqing.HotelJavaApplicationTests  - error資訊
1 [main] WARN cn.zixieqing.HotelJavaApplicationTests  - warn資訊
1 [main] INFO cn.zixieqing.HotelJavaApplicationTests  - info資訊
1 [main] DEBUG cn.zixieqing.HotelJavaApplicationTests  - debug資訊

  • 開啟之後,資訊就會更全
log4j: Trying to find [log4j.xml] using context classloader sun.misc.Launcher$AppClassLoader@18b4aac2.
log4j: Trying to find [log4j.xml] using sun.misc.Launcher$AppClassLoader@18b4aac2 class loader.
log4j: Trying to find [log4j.xml] using ClassLoader.getSystemResource().
log4j: Trying to find [log4j.properties] using context classloader sun.misc.Launcher$AppClassLoader@18b4aac2.
log4j: Trying to find [log4j.properties] using sun.misc.Launcher$AppClassLoader@18b4aac2 class loader.
log4j: Trying to find [log4j.properties] using ClassLoader.getSystemResource().
log4j: Could not find resource: [null].
0 [main] FATAL cn.zixieqing.HotelJavaApplicationTests  - fatal資訊
0 [main] ERROR cn.zixieqing.HotelJavaApplicationTests  - error資訊
1 [main] WARN cn.zixieqing.HotelJavaApplicationTests  - warn資訊
1 [main] INFO cn.zixieqing.HotelJavaApplicationTests  - info資訊
1 [main] DEBUG cn.zixieqing.HotelJavaApplicationTests  - debug資訊



2.2.4、自定義輸出格式 patternLayout

  • 自定義配置的也就是Layout而已,而自定義就是玩的PatternLayout,這個類有一個setConversionPattern()方法,檢視這個方法的原始碼
    public void setConversionPattern(String conversionPattern) {
        this.pattern = conversionPattern;
        this.head = this.createPatternParser(conversionPattern).parse();
    }

  • 從中發現,需要配置的就是String conversionPattern,因此:在log4j.properties配置檔案中新增上conversionPattern屬性配置即可,當然:這個屬性配置遵循一定的寫法,寫法如下:
    %m 輸出程式碼中指定的日誌資訊
    %p 輸出優先順序,及 DEBUG、INFO 等
    %n 換行符(Windows平臺的換行符為 "\n",Unix 平臺為 "\n")
    %r 輸出自應用啟動到輸出該 log 資訊耗費的毫秒數
    %c 輸出列印語句所屬的類的全名
    %t 輸出產生該日誌的執行緒全名
    %d 輸出伺服器當前時間,預設為 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss}
    %l 輸出日誌時間發生的位置,包括類名、執行緒、及在程式碼中的行數。如:Test.main(Test.java:10)
    %F 輸出日誌訊息產生時所在的檔名稱
    %L 輸出程式碼中的行號
    %% 輸出一個 "%" 字元
    可以在 % 與字元之間加上修飾符來控制最小寬度、最大寬度和文字的對其方式
        [%10p]:[]中必須有10個字元,由空格來進行補齊,資訊右對齊
        [%-10p]:[]中必須有10個字元,由空格來進行補齊,資訊左對齊,應用較廣泛


	上述舉例:[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

  • 修改了log4j.properties的配置如下( 做的修改就是最後兩個配置項 )
# rootLogger所有logger的根配置 - log4j.rootLogger=日誌級別,appenderName1,appenderName2,appenderName3....
log4j.rootLogger=debug,console
# appender輸出控制器配置 log4j.appender.+appenderName=某一種輸出型別 - 採用Console控制檯的方式舉例
log4j.appender.console=org.apache.log4j.ConsoleAppender
# 輸出的格式配置log4j.appender.+appenderName+layout=某種layout格式型別 - 注意:這裡型別改了,是PatternLayout,即自定義
log4j.appender.console.layout=org.apache.log4j.PatternLayout
# 編寫自定義輸出格式( 直接把上面舉例的拿過來 ) - 注意:加上了剛剛說的conversionPattern屬性
log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

  • 測試程式碼
    public void test04(){

        LogLog.setInternalDebugging(true);
        Logger logger = Logger.getLogger(Log4jTest01.class);

        logger.fatal("fatal資訊");
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }



2.2.5、將日誌輸出到檔案中

2.2.5.1、改造log4j.properties檔案

# rootLogger所有logger的根配置 - 這裡再加一個file
log4j.rootLogger=debug,console,file
# 控制檯的appender輸出控制器配置
log4j.appender.console=org.apache.log4j.ConsoleAppender
# 採用自定義控制檯輸出的格式
log4j.appender.console.layout=org.apache.log4j.PatternLayout
# 編寫控制檯自定義輸出格式
log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

# 再來一份,變為file的配置 - 把console改為file即可
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n



2.2.5.2、指定檔案輸出位置 及 字元編碼設定

  • 檢視FileAppender原始碼,首先看到的是四個屬性
	// 表示日誌檔案是否採用內容追加的方式 - 原始碼中有一個構造方法,這個的預設值是true
    protected boolean fileAppend;
    protected String fileName;
    protected boolean bufferedIO;
	// 日誌檔案的大小 - 原始碼的構造方法中預設值是8192
    protected int bufferSize;

	// 構造方法原始碼

    public FileAppender() {
        this.fileAppend = true;
        this.fileName = null;
        this.bufferedIO = false;
        this.bufferSize = 8192;
    }

  • FlieAppender中還有一個setFile()的方法,得知這個就是設定檔案的方法 ,也就是檔案日誌檔案存放路徑位置
    public void setFile(String file) {
        String val = file.trim();
        this.fileName = val;
    }

  • 這裡面需要傳一個file引數進去,因此:通過MyBatis的知識就知道log4j.properties配置中的這個對應屬性就是file了( 截掉set,得到屬性名 )
# rootLogger所有logger的根配置 - 這裡再加一個file
log4j.rootLogger=debug,console,file
# 控制檯的appender輸出控制器配置
log4j.appender.console=org.apache.log4j.ConsoleAppender
# 採用自定義控制檯輸出的格式
log4j.appender.console.layout=org.apache.log4j.PatternLayout
# 編寫控制檯自定義輸出格式
log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

# 再來一份,變為file的配置 - 把console改為file即可
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
# 日誌檔案儲存路徑 - 注:前一個file為自定義的appenderName;後一個file是日誌檔案輸出路徑的屬性,要是怕看錯眼,可以把後者換為File大寫也沒錯
log4j.appender.file.file=D:\log4j\zixieqing\log4j.log
  • 現在看FileAppender的父類WriterAppender,去看字元編碼設定
    protected boolean immediateFlush;
	// 這個屬性就是編碼設定
    protected String encoding;
    protected QuietWriter qw;

  • 因此:加上日誌檔案輸出路徑和字元編碼之後的log4.properties配置為:
# 控制檯日誌輸出設定
log4j.rootLogger=debug,console,file
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

# 日誌檔案輸出設定
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
log4j.appender.file.file=D:\log4j\zixieqing\log4j.log
log4j.appender.file.encoding=UTF-8



2.2.5.3、拆分日誌檔案

  • 一份日誌不可能用於一直記錄下去,那樣的話,日誌檔案體積就太大了

  • 繼續檢視FileAppender原始碼,看實現類

image


2.2.5.3.1、RollingFileAppender實現類
  • 這個玩意兒就是利用檔案大小來進行日誌拆分
  • 原始碼中有兩個屬性需要關注
	// 達到多大檔案時進行日誌拆分
    protected long maxFileSize = 10485760L;
	// 一共能夠拆分出多少份日誌檔案
    protected int maxBackupIndex = 1;

  • 因此:現在將log4j.properties配置檔案修改一下即可實現日誌根據檔案大小進行拆分
# 注意:記得在log4j.rootLoger=debug,console,file這一句中加上下面的這個檔案大小進行拆分的配置
# 如:log4j.rootLoger=debug,console,rollingFile
log4j.appender.rollingFile=org.apache.log4j.RollingFileAppender
log4j.appender.rollingFile.layout=org.apache.log4j.PatternLayout
log4j.appender.rollingFile.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
log4j.appender.rollingFile.file=D:\log4j\zixieqing\log4j.log
# 檔案多大時進行日誌拆分
log4j.appender.rollingFile.maxFileSize=10MB
# 最多可以拆分多少份
log4j.appender.rollingFile.maxBackupIndex=50


2.2.5.3.2、DailyRollingFileAppender實現類 - 建議用
  • 這個東西就是根據時間來進行日誌拆分,看原始碼,有這麼一個屬性
	// 時間格式,預設值就是如下的天
    private String datePattern = "'.'yyyy-MM-dd";

  • log4j.properties配置檔案中修改成如下的配置即可使用
# 一樣的,要使用這種方式:記得在log4j.rootLogger=debug,console,file這一句中加上下面的這個檔案大小進行拆分的配置
# 如:log4j.rootLogger=debug,console,dateRollingFile
log4j.appender.dateRollingFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.dateRollingFile.layout=org.apache.log4j.PatternLayout
log4j.appender.dateRollingFile.layout.conversionPattern=[%-10p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
log4j.appender.dateRollingFile.file=D:\log4j\zixieqing\log4j.log
# 加上時間格式 - 下面的值根據實際情況即可
log4j.appender.dateRollingFile.datePattern='.'yyyy-MM-dd



2.2.6、日誌持久化到資料庫

表基礎欄位

CREATE TABLE tbl_log(
    id int(11) NOT NULL AUTO_INCREMENT,
    name varchar(100) DEFAULT NULL COMMENT '專案名稱',
    createTime varchar(100) DEFAULT NULL COMMENT '建立時間',
    level varchar(10) DEFAULT NULL COMMENT '日誌級別',
    category varchar(100) DEFAULT NULL COMMENT '所在類的全路徑',
    fileName varchar(100) DEFAULT NULL COMMENT '檔名稱',
    message varchar(255) DEFAULT NULL COMMENT '日誌訊息',
    PRIMARY KEY(id)
)


專案MySQL驅動

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
            <scope>runtime</scope>
        </dependency>


log4.properties配置

# 配置appender輸出方式 輸出到資料庫表 - 注意:在rootManager中加上logDB,如:log4j.rootLogger=debug,console,logDB
log4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.logDB.layout=org.apache.log4j.PatternLayout
log4j.appender.logDB.Driver=com.mysql.jdbc.Driver
log4j.appender.logDB.URL=jdbc:mysql://localhost:3306/test
log4j.appender.logDB.User=root
log4j.appender.logDB.Password=072413
log4j.appender.logDB.Sql=INSERT INTO tbl_log(name,createTime,level,category,fileName,message) values('project_log','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%m')


測試程式碼

    @Test
    public void test07(){
        Logger logger = Logger.getLogger(Log4jTest01.class);
        logger.fatal("fatal資訊");
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }



2.2.7、自定義logger

  • 前面配置檔案中使用的都是rootManager的logger,接下來就自定義一個logger,看PropertyConfigurator類的原始碼,它裡面有一個屬性
	// 自定義logger配置的寫法 - 這後面拼接的就是自定義的logger名字
    static final String LOGGER_PREFIX = "log4j.logger.";

  • 其中:上述說的自定義logger名字遵循父子關係,也就是包關係
如:cn.zixieqing.log4j.test.Log4jTest01
    它的父logger就是上層的路徑或者是更上層的路徑
    例如:
        cn.zixieqing.log4j.test
        cn.zixieqing.log4j
        ...
        cn


修改log4j.properties

# 根logger,輸出級別是trace,在console控制檯進行輸出
log4j.rootLogger=trace,console
# 自定義logger,級別為info,在file檔案中輸出
log4j.logger.cn.zixieqing.log4j.test=info,file
# 自定義logger,是apache的,級別為error
log4j.logger.org.apache=error


自定義logger的注意點

  • 如果根節點的logge( 即:rootManager ) 和 自定義logger配置的輸出位置是不同的,則取二者的並集,配置的位置都會進行輸出操作
  • 如果二者配置的日誌級別不同,以我們自定義logger的輸出級別為主


2.2.8、一份簡單的log4j.properties配置

	log4j.rootLogger=DEBUG,console,file
	#控制檯輸出的相關設定
	log4j.appender.console = org.apache.log4j.ConsoleAppender
	log4j.appender.console.Target = System.out
	log4j.appender.console.Threshold=DEBUG
	log4j.appender.console.layout = org.apache.log4j.PatternLayout
	log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
	#檔案輸出的相關設定
	log4j.appender.file = org.apache.log4j.RollingFileAppender
	log4j.appender.file.File=./1og/zixieqing.log
	log4j.appender.file.MaxFileSize=10mb
	log4j.appender.file.Threshold=DEBUG
	log4j.appender.file.layout=org.apache.log4j.PatternLayout
	log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]‰m%n
	#日誌輸出級別
	log4j.logger.org.mybatis=DEBUG
	log4j.logger.java.sq1=DEBUG
	log4j.logger.java.sql.Statement=DEBUG
	log4j.logger.java.sql.ResultSet=DEBUG
	log4j.logger.java.sql.PreparedStatement=DEBUG
	


3、JCL

  • 全稱為Jakarta Commons Logging,是Apache提供的一個通用日誌AP

  • 注意:這個玩意兒本身沒有記錄日誌的實現功能,而是相當於一個門面。我們可以自由選擇第三方的日誌元件( log4j、JUL )作為具體實現,common-logging會通過動態查詢的機制,在程式執行時自動找出真正使用的日誌庫

image


JCL的組成

  • JCL 有兩個基本的抽象類
    • Log:日誌記錄器
    • LogFactory:日誌工廠(負責建立Log例項)


3.1、玩JCL

依賴

        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

測試

    @Test
    void jclQuickStartTest() {

        // LogFactory是org.apache.commons.logging.LogFactory
        Log log = LogFactory.getLog(JCLTest01.class);
        log.info("info資訊");
    }

  • 執行之後看效果會發現:輸出格式是JUL格式
  • 即:如果沒有任何第三方日誌框架的時候,預設使用的就是JUL


3.2、引入log4j

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

  • 然後使用log4j.properties配置檔案
  • 當再次進行測試時就會變成log4j的輸出配置


JCL原始碼分析

Log介面的4個實現類
    JDk13
    JDK14 正常java.util.logging
    Log4j 我們整合的log4j
    Simple JCL自帶實現類

    (1)檢視Jdk14Logger證明裡面使用的是JUL日誌框架 - 看import引入的就可以得知
    (2)檢視Log4JLogger證明裡面使用的是Log4j日誌框架 - 看import引入的就可以得知

    (3)觀察LogFactory,看看如何載入的Logger物件
        這是一個抽象類,無法例項化
        需要觀察其實現類LogFactoryImpl

    (4)觀察LogFactoryImpl
    	真正載入日誌實現使用的就是這個實現類LogFactoryImpl

    (5)進入getLog - 採用打斷點debug能夠更清晰地看清楚

        進入getInstance

        找到instance = this.newInstance(name);,繼續進入

        找到instance = this.discoverLogImplementation(name); 表示發現一個日誌的實現

        for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
            result = this.createLogFromClass(classesToDiscover[i], logCategory, true);
        }
    遍歷我們擁有的日誌實現框架
        遍歷的是一個陣列,這個陣列是按照
        log4j
        jdk14
        jdk13
        SimpleLogger
        的順序依次遍歷
        表示的是,第一個要遍歷的就是log4j,如果有log4j則執行該日誌框架
        如果沒有,則遍歷出來第二個,使用jdk14的JUL日誌框架
        以此類推

        result = this.createLogFromClass(classesToDiscover[i], logCategory, true);
    表示幫我們建立Logger物件
        在這個方法中,我們看到了
        c = Class.forName(logAdapterClassName, true, currentCL);
    是取得該型別的反射型別物件

        使用反射的形式幫我們建立logger物件
        constructor = c.getConstructor(this.logConstructorSignature);



4、SLF4J

  • 這也是一個日誌門面( 門面模式 / 外觀模式 ),其核心為:外部與一個子系統的通訊必須通過一個統一的外觀物件進行,使得子系統更易於使用

常見的日誌門面和日誌實現

  • 常見的日誌實現:JUL、log4j、logback、log4j2

  • 常見的日誌門面 :JCL、slf4j

  • 出現順序 :log4j -->JUL-->JCL--> slf4j --> logback --> log4j2


瞭解SLF4J

  • 全稱:Simple Logging Facade For Java,簡單日誌門面
  • 主要是為了給Java日誌訪問提供一套標準、規範的API框架,其主要意義在於提供介面,具體的實現可以交由其他日誌框架,例如log4j和logback等
  • SLF4J最重要的兩個功能就是對於日誌框架的繫結以及日誌框架的橋接


4.1、玩SLF4J

4.1.1、快速上手

準備知識

  • SLF4J對日誌的級別劃分
trace、debug、info、warn、error五個級別

    trace:日誌追蹤資訊
    debug:日誌詳細資訊
    info:日誌的關鍵資訊 預設列印級別
    warn:日誌警告資訊
    error:日誌錯誤資訊


  • 在沒有任何其他日誌實現框架整合的基礎之上,slf4j使用的就是自帶的框架slf4j-simple
    • 注意點:slf4j-simple也必須以單獨依賴的形式匯入進來
	<!--slf4j 自帶的簡單日誌實現 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.25</version>
    </dependency>


入門

  • 依賴
    <!--slf4j 核心依賴-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.25</version>
    </dependency>

    <!--slf4j 自帶的簡單日誌實現 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.25</version>
    </dependency>


  • 測試程式碼
	@Test
    public void test01(){

        // 是slf4j包下的
        Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
        logger.trace("trace資訊");
        logger.debug("debug資訊");
        logger.info("info資訊");
        logger.warn("warn資訊");
        logger.error("error資訊");

    }



4.1.2、動態資訊輸出

  • 本質:使用佔位符
  • 有些時候輸出的日誌資訊,需要我們搭配動態的資料,這些資料有可能是資訊,有可能是資料庫表中的資料
    @Test
    public void test02(){

        Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
        String name = "zs";
        int age = 23;
        /* 
            字串拼接的形式:logger.info("學生資訊-姓名:"+name+";年齡:"+age);
            使用JCL的形式:logger.info("學生資訊-姓名:{},年齡:{}",new Object[]{name,age});

            這上面兩者雖然都可以做到相應的輸出,但是:麻煩
        */
        // 使用SLF4J的形式
        logger.info("學生資訊-姓名:{},年齡:{}",name,age);

    }

  • 注意點:如果後面拼接的字串是一個物件,那麼{}並不能充當佔位,進行字串拼接


4.2.3、輸出異常資訊

    @Test
    public void test03(){

        /*

            日誌對於異常資訊的處理

                一般情況下,我們在開發中的異常資訊,都是記錄在控制檯上(我們開發環境的一種日誌列印方式)
                我們會根據異常資訊提取出有用的線索,來除錯bug

                但是在真實生產環境中(專案上線),對於伺服器或者是系統相關的問題
                在控制檯上其實也會提供相應的異常或者錯誤資訊的輸出
                但是這種錯誤輸出方式(輸出的時間,位置,格式...)都是伺服器系統預設的

                我們可以通過日誌技術,選擇將異常以日誌列印的方式,進行輸出檢視
                輸出的時間,位置(控制檯,檔案),格式,完全由我們自己去進行定義

         */

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

        try {
            Class.forName("aaa");
        } catch (ClassNotFoundException e) {
            // e.printStackTrace();
            logger.info("XXX類中的XXX方法出現了異常,請及時關注資訊");
            // e是引用型別物件,不能根前面的{}做有效的字串拼接
            // logger.info("具體錯誤是:{}",e);
            // 我們不用加{},直接後面加上異常物件e即可 - 這是利用了過載方法info(String message, Throwable throwable )
            logger.info("具體錯誤是:",e);
        }
    }



4.2.4、SLF4J與日誌繫結

image

  • 圖中分為了三部分

    • 1、在沒有繫結任何日誌實現的基礎之上,日誌是不能夠繫結實現任何功能的
      • slf4j-simple是slf4j官方提供的
      • 使用的時候,也是需要匯入依賴,自動繫結到slf4j門面上
      • 如果不匯入,slf4j 核心依賴是不提供任何實現的
    • 2、logback和simple(包括nop)
    • 都是slf4j門面時間線後面提供的日誌實現,所以API完全遵循slf4j進行的設計
    • 只需要匯入想要使用的日誌實現依賴,即可與slf4j無縫銜接
    • 注意:nop雖然也劃分到實現中了,但是它是指不實現日誌記錄
    • 3、log4j和JUL
    • 都是slf4j門面時間線前面的日誌實現,所以API不遵循slf4j進行設計
    • 通過適配橋接的技術,完成的與日誌門面的銜接


4.2.5、繫結logback

依賴

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>


測試

public class SLF4JTest {

    @Test
    public void bingingLogTest() {

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

        try {
            Class.forName("aaa");
        } catch (ClassNotFoundException e) {
            logger.info("具體錯誤是:",e);
        }
    }
}


結果

10:50:15.391 [main] INFO com.zixieqing.SLF4JTest - 具體錯誤是:
java.lang.ClassNotFoundException: aaa
	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)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:264)
	.......
  • 這種就看起來很清爽,如果加上slf4j-simple那輸出結果又是另一回事,測試跳過
  • 上面這種就是不用去管底層到底採用的是哪一種日誌實現,可是上面的原始碼完全沒有改變,照常寫,這就是日誌門面的好處


4.2.6、slf4j-nop禁止日誌列印

依賴

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <!--
            注意:有坑兒,和依賴匯入順序有關
                如·:要是將這個依賴放到logback的下面,那麼屁用都沒有
                但是:把這個依賴放在logback的前面就可以實現日誌禁止列印了

                原因:在slf4j環境下,要是同時出現了多個日誌實現,預設使用先匯入的日誌實現
        -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>


測試

    @Test
    public void slf4jNopTest() {

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

        try {
            Class.forName("aaa");
        } catch (ClassNotFoundException e) {
            logger.info("具體錯誤是:",e);
        }
    }


結果

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/D:/install/maven/apache-maven-3.6.1/maven-repo/org/slf4j/slf4j-nop/1.7.30/slf4j-nop-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/install/maven/apache-maven-3.6.1/maven-repo/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.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 [org.slf4j.helpers.NOPLoggerFactory]

  • 日誌內容的相關列印就沒了
  • 注意:如果想要讓nop發揮效果,禁止所有日誌的列印,那麼就必須要將slf4j-nop的依賴放在所有日誌實現依賴的上方


4.2.7、繫結log4j

  • 玩這個就需要注意日誌框架時間線的問題了
    • 出現順序 :log4j -->JUL-->JCL--> slf4j --> logback --> log4j2

  • 也就是在slf4j之後出現的( 如:logback、log4j2 ),這些都准許slf4j的規範,所以直接匯入對應的依賴之後就可以使用,但是在slf4j之前的,並沒有預料到會出現後續這些規範嘛,而slf4j想要繫結log4j就需要一個slf4j-log4j12的介面卡

玩玩繫結log4j

依賴

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <!--slf4j和log4j的介面卡 - 想要slf4j能夠繫結log4j就需要這個-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.30</version>
        </dependency>


測試

    @Test
    public void slf4jBindingLog4jTest() {
        Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
        logger.info("info資訊");
    }


結果

# 雖然沒有輸出訊息,但是有如下這個話就足夠了No appenders could be found for logger
# 這表示沒有appender嘛,加上log4j.properties配置檔案就可以使用了
log4j:WARN No appenders could be found for logger (com.zixieqing.SLF4JTest).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.


其他日誌框架的繫結就看官網的那張圖,在那張圖中對繫結誰時匯入哪一個依賴都有備註

image



4.2.8、slf4j原始碼分析流程

進入到getLogger
            看到Logger logger = getLogger(clazz.getName());

            進入過載的getLogger
            ILoggerFactory iLoggerFactory = getILoggerFactory(); 用來取得Logger工廠實現的方法

            進入getILoggerFactory()
            看到以雙重檢查鎖的方式去做判斷
            執行performInitialization(); 工廠的初始化方法

            進入performInitialization()
            bind()就是用來繫結具體日誌實現的方法

            進入bind()
            看到Set集合Set<URL> staticLoggerBinderPathSet = null;
            因為當前有可能會有N多個日誌框架的實現
            看到staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();

            進入findPossibleStaticLoggerBinderPathSet()
            看到建立了一個有序不可重複的集合物件
            LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet();
            宣告瞭列舉類的路徑,經過if else判斷,以獲取系統中都有哪些日誌實現
            看到Enumeration paths;
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }

            我們主要觀察常量STATIC_LOGGER_BINDER_PATH
            通過常量我們會找到類StaticLoggerBinder
            這個類是以靜態的方式繫結Logger實現的類
            來自slf4j-JDK14的介面卡

            進入StaticLoggerBinder
            看到new JDK14LoggerFactory();
            進入JDK14LoggerFactory類的無參構造方法
            看到java.util.logging.Logger.getLogger("");
            使用的就是jul的Logger

            接著觀察findPossibleStaticLoggerBinderPathSet
            看到以下程式碼,表示如果還有其他的日誌實現
            while(paths.hasMoreElements()) {
                URL path = (URL)paths.nextElement();
                將路徑新增進入
                staticLoggerBinderPathSet.add(path);
            }

            回到bind方法
            表示對於繫結多實現的處理
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            如果出現多日誌實現的情況
            則會列印
            Util.report("Class path contains multiple SLF4J bindings.");


通過原始碼總結:
在真實生產環境中,slf4j只繫結一個日誌實現框架就可以了
繫結多個,預設使用匯入依賴的第一個,而且會產生沒有必要的警告資訊



4.2.9、slf4j日誌重構

有這麼一個情況:專案原本使用的是log4j日誌,但是隨著技術的迭代,需要使用另外的日誌框架,如:slf4j+logback,因此:此時不可能說去該原始碼,把所有使用log4j的地方都改成slf4j

  • 這種情況,在slf4j官網中就提供了對應的解決方式,就是加個依賴而已,加的東西就叫做橋接器

image



4.2.9.1、玩一下橋接器

演示bug:先保證專案沒有其他任何的干擾,如:另外的依賴、另外日誌的程式碼

依賴

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

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


log4j.properties配置檔案

log4j.rootLogger=debug,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.lo4j.SimpleLayout


測試程式碼

package com.zixieqing;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.junit.Test;



    @Test
    public void bridgingTest() {

        // 假設專案用的是log4j
        Logger logger = LogManager.getLogger(SLF4JTest.class);

        logger.info("inf資訊");
    }


此時,專案升級,改用slf4j+logback來當日志

  • 1、去掉log4j的依賴

  • 2、新增slf4j的橋接元件,所以現在的依賴就變成如下的樣子

    • 		<dependency>
                  <groupId>junit</groupId>
                  <artifactId>junit</artifactId>
                  <version>4.13.2</version>
                  <scope>test</scope>
              </dependency>
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-api</artifactId>
                  <version>1.7.25</version>
              </dependency>
      		<!--這個就是log4j和slf4j的橋接元件-->
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>log4j-over-slf4j</artifactId>
                  <version>1.7.25</version>
              </dependency>
              <dependency>
                  <groupId>ch.qos.logback</groupId>
                  <artifactId>logback-classic</artifactId>
                  <version>1.2.11</version>
              </dependency>
      
      
    • 要引入什麼橋接元件,看要怎麼重構,然後根據官網的說明來弄就行

image


測試

  • 原始碼直接不用動
package com.zixieqing;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.junit.Test;


    @Test
    public void bridgingTest() {

        // 假設專案用的是log4j
        Logger logger = LogManager.getLogger(SLF4JTest.class);

        logger.info("inf資訊");
    }


結果

14:45:09.901 [main] INFO com.zixieqing.SLF4JTest - inf資訊



5、LOGBACK

  • 這是log4j的作者跳槽之後開發的另一款日誌框架,比log4j出色很多
  • Logback當前分成三個模組:logback-core,logback- classic和logback-access
    • logback-core是其它兩個模組的基礎模組
    • logback-classic是log4j的一個改良版本。此外logback-classic完整實現SLF4J API。可以很方便地更換成其它日誌系統如log4j或JDK14 Logging
    • logback-access訪問模組與Servlet容器整合提供通過Http來訪問日誌的功能

logback的組成

  • Logger: 日誌的記錄器,主要用於存放日誌物件,也可以定義日誌型別、級別
  • Appender:用於指定日誌輸出的目的地,目的地可以是控制檯、檔案、資料庫等等
  • Layout: 負責把事件轉換成字串,格式化的日誌資訊的輸出
    • 注意點:在Logback中Layout物件被封裝在encoder中,即 未來使用的encoder其實就是Layout

logback配置檔案的型別

  • Logback提供了3種配置檔案
    • logback.groovy
    • logback-test.xml
    • logback.xml
  • 我們用的一般是xml的格式

logback的日誌輸出格式

日誌輸出格式:
    %-10level  級別 案例為設定10個字元,左對齊
    %d{yyyy-MM-dd HH:mm:ss.SSS} 日期
    %c  當前類全限定名
    %M  當前執行日誌的方法
    %L  行號
    %thread 執行緒名稱
    %m或者%msg    資訊
    %n  換行



5.1、玩logback

5.1、logback的日誌級別

	error > warn > info > debug > trace

	其中:logback的預設日誌級別就是debug


5.2、快速上手

依賴

        <!-- ogback-classic 中包含了基礎模板 ogback-core-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
        <!--logback是日誌實現,也需要通過日誌門面來整合-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>


測試

public class LogBackTest {

    @Test
    public void quickStartTest() {

        // 這個log4j中的
        Logger logger = LoggerFactory.getLogger(LogBackTest.class);
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }
}


結果

15:44:27.032 [main] ERROR com.zixieqing.LogBackTest - error資訊
15:44:27.033 [main] WARN com.zixieqing.LogBackTest - warn資訊
15:44:27.033 [main] INFO com.zixieqing.LogBackTest - info資訊
15:44:27.033 [main] DEBUG com.zixieqing.LogBackTest - debug資訊

  • 得出資訊,logback的預設日誌級別就是debug


5.3、簡單認識logback的配置檔案

<?xml version="1.0" encoding="utf-8" ?>
<!--logback的所有配置都需要在configuration標籤中機芯-->
<configuration>

    <!--通用屬性配置 - 目的:在下面配置需要的地方使用${name}的形式,方便的取得value值
        name 起的名字 見名知意即可
        value 值
    -->
    <!--配置日誌輸出格式-->
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L %thread %m%n"/>

    <!--配置日誌輸出型別
        name 亂取,見名知意即可
        class 型別全類路徑
    -->
    <appender name = "consoleAppender" class = "ch.qos.logback.core.ConsoleAppender">
        <!--配置日誌的顏色-->
        <target>
            <!--還可以用system.out就是常見的黑白色-->
            system.err
        </target>

        <!--配置日誌輸出格式,直接引用前面配置的property通用屬性-->
        <encoder class = "ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>

    <!--配置root Logger-->
    <root level="DEBUG">
        <appender-ref ref="consoleAppender"/>
    </root>

</configuration>
  • 其中:<property name="" value = "" vlaue的引數配置參考
日誌輸出格式:
    %-10level  級別 案例為設定10個字元,左對齊
    %d{yyyy-MM-dd HH:mm:ss.SSS} 日期
    %c  當前類全限定名
    %M  當前執行日誌的方法
    %L  行號
    %thread 執行緒名稱
    %m或者%msg    資訊
    %n  換行


測試

public class LogBackTest {

    @Test
    public void quickStartTest() {

        // 這個log4j中的
        Logger logger = LoggerFactory.getLogger(LogBackTest.class);
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }
}



5.4、將日誌輸出到檔案中

logback.xml中加入如下的配置即可

    <!--配置全域性的檔案輸出路徑-->
    <property name="filePath" value="D:\test"/>

    <!--定義檔案的appender輸出型別-->
    <appender name="fileAppender" class="ch.qos.logback.core.FileAppender">
        <!--檔案路徑 和 檔名 -->
        <file>${filePath}/logback.log</file>

        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--檔案輸出的格式-->
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>

    <!--記得在root logger中引用一下fileAppender的配置-->
    <!--配置root Logger-->
    <root level="DEBUG">
        <appender-ref ref="consoleAppender"/>
        <!--引用配置的檔案appender輸出配置-->
        <appender-ref ref="fileAppender"/>
    </root>


測試程式碼

    @Test
    public void fileAppenderTest() {
        // 這個log4j中的
        Logger logger = LoggerFactory.getLogger(LogBackTest.class);
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }

通過結果觀察會發現:logback的日誌檔案輸出預設是以追加的形式新增新日誌內容



5.5、以HTML格式記錄日誌檔案

  • 當日志檔案不是很多的時候,可以採用這種方式,因為這種HTML裡面的樣式和格式都可以讓logback進行生成,而裡面的內容使我們加進去的而已,因此:這樣一看就知道,HTML檔案佔用的記憶體容量就蠻大了

logback.xml配置檔案編寫

    <!--定義HTML檔案輸出格式,本質還是檔案,所以class是FileAppender-->
    <appender name="HTMLFileAppender" class="ch.qos.logback.core.FileAppender">
        <file>${filePath}\logback.log</file>

        <!--注意:這裡採用的是LayoutWrappingEncoder-->
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <!--注意:採用layout標籤包裹,格式型別為HTMLLayout-->
            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <pattern>${pattern}</pattern>
            </layout>
        </encoder>
    </appender>

    <!--配置root Logger-->
    <root level="DEBUG">
        <!--<appender-ref ref="consoleAppender"/>-->
        <!--<appender-ref ref="fileAppender"/>-->
        <appender-ref ref="HTMLFileAppender"/>
    </root>


測試

    @Test
    public void htmlFileAppenderTest() {
        // 這個log4j中的
        Logger logger = LoggerFactory.getLogger(LogBackTest.class);
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }

  • 執行程式即可生成HTML檔案


5.6、拆分日誌 和 歸檔壓縮

    <!-- 配置檔案的appender 可拆分歸檔的檔案 -->
    <appender name="roll" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <!-- 輸入格式 -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!-- 引入檔案位置 -->
        <file>${logDir}/roll_logback.log</file>

        <!-- 指定拆分規則 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">

            <!-- 按照時間和壓縮格式宣告檔名 壓縮格式gz -->
            <fileNamePattern>${logDir}/roll.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern>

            <!-- 按照檔案大小來進行拆分 -->
            <maxFileSize>1KB</maxFileSize>
        </rollingPolicy>
    </appender>

    <!--配置root Logger-->
    <root level="DEBUG">
        <!--<appender-ref ref="consoleAppender"/>-->
        <!--<appender-ref ref="fileAppender"/>-->
        <!--<appender-ref ref="HTMLFileAppender"/>-->
        <appender-ref ref="roll"/>
    </root>

  • 注意點:只要使用到拆分日誌的事情,那麼<fileNamePattern></fileNamePattern>標籤必須有,原始碼中有說明
在TimeBasedRollingPolicy類中有一個常量
 static final String FNP_NOT_SET ="The FileNamePattern option must be set before using TimeBasedRollingPolicy. ";

這個常量值告知的結果就是上面的注意點

  • 另外:其他有哪些屬性,而每一個class對應對應下有哪些標籤可以用,那就是class指定值中的屬性名,也就是標籤名,看原始碼即可

    @Test
    public void rollFileAppenderTest() {
        for (int i = 0; i < 1000; i++) {
            // 這個log4j中的
            Logger logger = LoggerFactory.getLogger(LogBackTest.class);
            logger.error("error資訊");
            logger.warn("warn資訊");
            logger.info("info資訊");
            logger.debug("debug資訊");
            logger.trace("trace資訊");
        }
    }



5.7、過濾器

    <!-- 配置控制檯的appender 使用過濾器 -->
    <appender name="consoleFilterAppender" class="ch.qos.logback.core.ConsoleAppender">

        <target>
            System.out
        </target>

        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>

        <!-- 配置過濾器 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">

            <!-- 設定日誌的輸出級別 -->
            <level>ERROR</level>

            <!-- 高於level中設定的級別,則列印日誌 -->
            <onMatch>ACCEPT</onMatch>

            <!-- 低於level中設定的級別,則遮蔽日誌 -->
            <onMismatch>DENY</onMismatch>

        </filter>

    </appender>

	
    <!--配置root Logger-->
    <root level="DEBUG">
        <!--<appender-ref ref="consoleAppender"/>-->
        <!--<appender-ref ref="fileAppender"/>-->
        <!--<appender-ref ref="HTMLFileAppender"/>-->
        <!--<appender-ref ref="roll"/>-->
        <!--解開console日誌列印logger-->
		<appender-ref ref="consoleAppender"/>
    </root>
  • 測試程式碼照常用,然後在console控制檯輸出的內容會根據上述配置的filter過濾器罷對應的內容過濾掉,從而不列印出來


5.8、非同步日誌

    <!-- 1、配置非同步日誌 -->
    <appender name="asyncAppender" class="ch.qos.logback.classic.AsyncAppender">
         <!-- 引用appender配置,即:讓什麼輸出型別的日誌進行非同步操作,可以選擇consoleAppender、fileAppender.... -->
        <appender-ref ref="fileAppender"/>
    </appender>

    <!--配置root Logger-->
    <root level="DEBUG">
        <!--<appender-ref ref="consoleAppender"/>-->
        <!--<appender-ref ref="fileAppender"/>-->
        <!--<appender-ref ref="HTMLFileAppender"/>-->
        <!--<appender-ref ref="roll"/>-->
        <!--解開console日誌列印logger-->
		<!--<appender-ref ref="consoleAppender"/>-->
        <!--2、在root logger中引用一下配置的非同步日誌-->
        <appender-ref ref="asyncAppender"/>
    </root>


另外:在上面的1、配置非同步日誌裡面還有兩個配置屬性

    下面這個配置的是一個閾值,當佇列的剩餘容量小於這個閾值的時候,當前日誌的級別 trace、debug、info這3個級別的日誌將被丟棄
    設定為0,說明永遠都不會丟棄trace、debug、info這3個級別的日誌
    <discardingThreshold>0</discardingThreshold>

    配置佇列的深度,這個值會影響記錄日誌的效能,預設值就是256
    <queueSize>256</queueSize>

    關於這兩個屬性,一般情況下,使用預設值即可
    這兩個屬性不要亂配置,會影響系統效能,瞭解其功能即可


    @Test
    public void asyncAppenderTest(){

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

        // 日誌列印操作
        for (int i = 0; i < 100; i++) {

            logger.error("error資訊");
            logger.warn("warn資訊");
            logger.info("info資訊");
            logger.debug("debug資訊");
            logger.trace("trace資訊");

        }

        // 系統本身業務相關的其他操作
        System.out.println("系統本身業務相關的操作1");
        System.out.println("系統本身業務相關的操作2");
        System.out.println("系統本身業務相關的操作3");
        System.out.println("系統本身業務相關的操作4");
    }



5.9、自定義logger

  • 還是老樣子,自定義logger就是為了替換下面這一堆
    <!--配置root Logger-->
    <root level="DEBUG">
        <!--<appender-ref ref="consoleAppender"/>-->
        <!--<appender-ref ref="fileAppender"/>-->
        <!--<appender-ref ref="HTMLFileAppender"/>-->
        <!--<appender-ref ref="roll"/>-->
        <!--解開console日誌列印logger-->
		<!--<appender-ref ref="consoleAppender"/>-->
        <!--2、在root logger中引用一下配置的非同步日誌-->
        <appender-ref ref="asyncAppender"/>
    </root>


自定義logger配置

    <!--
        additivity="false" 表示不繼承rootlogger
    -->
    <logger name="com.bjpowernode" level="info" additivity="false">
        <!-- 在自定義logger中配置appender -->
        <appender-ref ref="consoleAppender"/>
    </logger>

  • 測試程式碼和前面一樣,跳過


6、LOG4J2

  • 這玩意兒雖然叫log4j2,也就是對log4j做了增強,但是更多的其實是對logback不足做了優化

log4j2的特徵

效能提升

  • Log4j2包含基於LMAX Disruptor庫的下一代非同步記錄器。在多執行緒場景中,非同步記錄器的吞吐量比Log4j 1.x和Logback高18倍,延遲低

自動重新載入配置

  • 與Logback一樣,Log4j2可以在修改時自動重新載入其配置。與Logback不同,它會在重新配置發生時不會丟失日誌事件

高階過濾

  • 與Logback一樣,Log4j2支援基於Log事件中的上下文資料,標記,正規表示式和其他元件進行過濾
  • 此外,過濾器還可以與記錄器關聯。與Logback不同,Log4j2可以在任何這些情況下使用通用的Filter類

外掛架構

  • Log4j使用外掛模式配置元件。因此,無需編寫程式碼來建立和配置Appender,Layout,Pattern Converter等。在配置了的情況下,Log4j自動識別外掛並使用它們

無垃圾機制

  • 在穩態日誌記錄期間,Log4j2 在獨立應用程式中是無垃圾的,在Web應用程式中是低垃圾。這減少了垃圾收集器的壓力,並且可以提供更好的響應效能

log4j2的最佳搭配

  • 採用log4j2 + slf4j的方式


6.1、玩log4j2

6.1.1、快速上手

依賴

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>


測試

public class Log4j2Test {

    @Test
    public void quickStartTest() {

        // 是org.apache.logging.log4j包下的
        Logger logger = LogManager.getLogger(Log4j2Test.class);
        logger.fatal("fatal資訊");
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
    }
}


結果

21:18:43.909 [main] FATAL com.zixieqing.Log4j2Test - fatal資訊
21:18:43.911 [main] ERROR com.zixieqing.Log4j2Test - error資訊

  • 得出結論:log4j2的預設級別是error級別


6.1.2、簡單瞭解log4j2的配置檔案

  • log4j2是參考logback創作出來的,所以配置檔案也是使用xml
  • log4j2同樣是預設載入類路徑(resources)下的log4j2.xml檔案中的配置
  • 但是:log4j2.xml和logback.xml有區別,第一點就是log4j2.xml中的標籤名字是首字母大寫;第二點就是多個單詞采用的是駝峰命名法,還有其他的區別

簡單的log4j2.xml配置檔案

<?xml version="1.0" encoding="UTF-8" ?>
<!--一樣的,所有的日誌配置都需要在Configuration標籤中進行

    還可以跟兩個屬性配置
        status="級別" 日誌框架本身的日誌輸出級別 一般都不需要配置,因為加了之後,輸出資訊會多一些其實沒多大用的內容
            如:<Configuration status="DEBUG"/>
        monitorInterval="數值" 自動載入配置檔案的間隔時間 如:monitorInterval="5" 就是5秒

	注意:這個標籤不要加xmlns屬性,加上之後就有表空間約束,指不定會搞出問題
-->
<Configuration>
    <!--配置Appender輸出型別-->
    <Appenders>
        <Console name="consoleAppender" target="SYSTEM_OUT"/>
    </Appenders>

    <!--配置logger-->
    <Loggers>
        <!--配置root logger-->
        <Root level="DEBUG">
            <AppenderRef ref="consoleAppender"/>
        </Root>
    </Loggers>
</Configuration>


測試程式碼

    @Test
    public void log4j2XmlTest() {

        // 是org.apache.logging.log4j包下的
        Logger logger = LogManager.getLogger(Log4j2Test.class);
        logger.fatal("fatal資訊");
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
    }


結果

直接輸出如下內容:

    fatal資訊
    error資訊
    warn資訊
    info資訊



6.1.3、SLF4J + LOG4J2 - 從此開始都是重點

  • slf4j門面呼叫的是log4j2的門面,再由log4j2的門面呼叫log4j2的實現

  • 步驟
    • 1、匯入slf4j的日誌門面
    • 2、匯入log4j2的介面卡
    • 3、匯入log4j2的日誌門面
    • 4、匯入log4j2的日誌實現
    • 5、編寫log4j2.xml

依賴

        <!--slf4j門面-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <!--log4j-slf4j介面卡-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.17.1</version>
        </dependency>
        <!--log4j門面-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.1</version>
        </dependency>
        <!--logj日誌實現-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
        </dependency>
        <dependency>


log4j2.xml配置檔案

<?xml version="1.0" encoding="UTF-8" ?>
<!--一樣的,所有的日誌配置都需要在Configuration標籤中進行

    還可以跟兩個屬性配置
        status="級別" 日誌框架本身的日誌輸出級別 一般都不需要配置,因為加了之後,輸出資訊會多一些其實沒多大用的內容
            如:<Configuration status="DEBUG"/>
        monitorInterval="數值" 自動載入配置檔案的間隔時間 如:monitorInterval="5" 就是5秒

	注意:這個標籤不要加xmlns屬性,加上之後就有表空間約束,指不定會搞出問題
-->
<Configuration>
    <!--配置Appender輸出型別-->
    <Appenders>
        <Console name="consoleAppender" target="SYSTEM_OUT"/>
    </Appenders>

    <!--配置logger-->
    <Loggers>
        <!--配置root logger-->
        <Root level="DEBUG">
            <AppenderRef ref="consoleAppender"/>
        </Root>
    </Loggers>
</Configuration>


測試

  • 保證沒有其他的測試程式碼 或者說 保證import匯入是org.slf4j門面的
    @Test
    public void log4j2AndSlf4jTest() {
        Logger logger = LoggerFactory.getLogger(Log4j2Test.class);
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }


結果

error資訊
warn資訊
info資訊
debug資訊



6.1.4、將日誌輸出到檔案

  • 目前的程式碼都是基於前面配置好的log4j2 + slf4j的形式

log4j2.xml的配置檔案

<?xml version="1.0" encoding="UTF-8" ?>
<!--一樣的,所有的日誌配置都需要在Configuration標籤中進行

    這個標籤中除了xmlns,還可以跟兩個屬性配置
        status="級別" 日誌框架本身的日誌輸出級別 一般都不需要配置,因為加了之後,輸出資訊會多一些其實沒多大用的內容
            如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/>
        monitorInterval="數值" 自動載入配置檔案的間隔時間 如:monitorInterval="5" 就是5秒
-->
<Configuration>

    <!--配置全域性通用屬性-->
    <properties>
        <property name="logPath">D:\test</property>
    </properties>

    <!--配置Appender輸出型別-->
    <Appenders>
        <Console name="consoleAppender" target="SYSTEM_OUT"/>

        <!--配置檔案輸出-->
        <File name="fileAppender" fileName="${logPath}/log4j2.log">
            <!-- 配置檔案輸出格式 一樣遵循logback中的格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>
        </File>
    </Appenders>

    <!--配置logger-->
    <Loggers>
        <!--配置root logger-->
        <Root level="DEBUG">
            <AppenderRef ref="consoleAppender"/>
            <AppenderRef ref="fileAppender"/>
        </Root>
    </Loggers>
</Configuration>


    @Test
    public void fileAppenderTest() {
        Logger logger = LoggerFactory.getLogger(Log4j2Test.class);
        logger.error("error資訊");
        logger.warn("warn資訊");
        logger.info("info資訊");
        logger.debug("debug資訊");
        logger.trace("trace資訊");
    }



6.1.5、拆分日誌

log4j2.xml配置檔案

<?xml version="1.0" encoding="UTF-8" ?>
<!--一樣的,所有的日誌配置都需要在Configuration標籤中進行

    這個標籤中除了xmlns,還可以跟兩個屬性配置
        status="級別" 日誌框架本身的日誌輸出級別 一般都不需要配置,因為加了之後,輸出資訊會多一些其實沒多大用的內容
            如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/>
        monitorInterval="數值" 自動載入配置檔案的間隔時間 如:monitorInterval="5" 就是5秒
-->
<Configuration>

    <!--配置全域性通用屬性-->
    <properties>
        <property name="logPath">D:\test</property>
    </properties>

    <!--配置Appender輸出型別-->
    <Appenders>
        <Console name="consoleAppender" target="SYSTEM_OUT"/>

        <!--配置檔案輸出-->
        <File name="fileAppender" fileName="${logPath}/log4j2.log">
            <!-- 配置檔案輸出格式 一樣遵循logback中的格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>
        </File>

        <!--
           按照指定規則來拆分日誌檔案

           fileName:日誌檔案的名字
           filePattern:日誌檔案拆分後檔案的命名規則
                       $${date:yyyy-MM-dd}:根據日期當天,建立一個資料夾
                             例如:2021-01-01這個資料夾中,記錄當天的所有日誌資訊(拆分出來的日誌放在這個資料夾中)
                                  2021-01-02這個資料夾中,記錄當天的所有日誌資訊(拆分出來的日誌放在這個資料夾中)
                        rollog-%d{yyyy-MM-dd-HH-mm}-%i.log
                             為檔案命名的規則:%i表示序號,從0開始,目的是為了讓每一份檔名字不會重複
       -->
        <RollingFile name="rollingFile" 
                     fileName="${logPath}/rollingLog.log"
                     filePattern="${logPath}/$${date:yyyy-MM-dd}/rollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log">

            <!-- 日誌訊息格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>

            <Policies>
                <!-- 在系統啟動時,觸發拆分規則,產生一個日誌檔案 -->
                <OnStartupTriggeringPolicy/>

                <!-- 按照檔案的大小進行拆分 注:這裡雖然限制是10KB,但是真實生成出來的檔案一般都是比這數值大1KB-->
                <SizeBasedTriggeringPolicy size="10KB"/>

                <!-- 按照時間節點進行拆分 拆分規則就是filePattern-->
                <TimeBasedTriggeringPolicy/>
            </Policies>

            <!-- 在同一目錄下,檔案的個數限制,如果超出了設定的數值,則根據時間以新的覆蓋舊-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>
    </Appenders>

    <!--配置logger-->
    <Loggers>
        <!--配置root logger-->
        <Root level="DEBUG">
            <AppenderRef ref="consoleAppender"/>
            <AppenderRef ref="fileAppender"/>
            <AppenderRef ref="rollingFile"/>
        </Root>
    </Loggers>
</Configuration>


測試

    @Test
    public void rollingLogTest() {
        Logger logger = LoggerFactory.getLogger(Log4j2Test.class);

        for (int i = 0; i < 1000; i++) {
            logger.error("error資訊");
            logger.warn("warn資訊");
            logger.info("info資訊");
            logger.debug("debug資訊");
            logger.trace("trace資訊");
        }
    }



6.1.6、非同步日誌

  • 這個技術就是log4j2最騷的一個點
  • Log4j2提供了兩種實現日誌的方式,一個是通過AsyncAppender,一個是通過AsyncLogger,分別對應前面我們說的Appender元件和Logger元件
    • 注意這是兩種不同的實現方式,在設計和原始碼上都是不同的體現


6.1.6.1、AsyncAppender方式 - 瞭解

  • 是通過引用別的Appender來實現的
  • 這種方式使用的就是<Async></Async>標籤
當有日誌事件到達時,會開啟另外一個執行緒來處理它們。

需要注意的是,如果在Appender的時候出現異常,對應用來說是無法感知的。

AsyncAppender應該在它引用的Appender之後配置,預設使用 java.util.concurrent.ArrayBlockingQueue實現而不需要其它外部的類庫。 當使用此Appender的時候,在多執行緒的環境下需要注意,阻塞佇列容易受到鎖爭用的影響,這可能會對效能產生影響。這時候,我們應該考慮使用無鎖的非同步記錄器(AsyncLogger)


步驟

  • 1、新增非同步日誌依賴

  •         <!--asyncAppender非同步日誌依賴-->
            <dependency>
                <groupId>com.lmax</groupId>
                <artifactId>disruptor</artifactId>
                <version>3.4.2</version>
            </dependency>
    
    
  • 2、在Appenders標籤中,對於非同步進行配置

    • 使用Async標籤

      •         <!-- 配置非同步日誌 -->
                <Async name="asyncAppender">
                    <!-- 引用在前面配置的appender -->
                    <AppenderRef ref="fileAppender"/>
                </Async>
        
        
  • 3、rootlogger引用Async

    •     <!--配置logger-->
          <Loggers>
              <!--配置root logger-->
              <Root level="DEBUG">
                  <AppenderRef ref="asyncAppender"/>
              </Root>
          </Loggers>
      

完整的log4j2.xml配置

<?xml version="1.0" encoding="UTF-8" ?>
<!--一樣的,所有的日誌配置都需要在Configuration標籤中進行

    這個標籤中除了xmlns,還可以跟兩個屬性配置
        status="級別" 日誌框架本身的日誌輸出級別 一般都不需要配置,因為加了之後,輸出資訊會多一些其實沒多大用的內容
            如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/>
        monitorInterval="數值" 自動載入配置檔案的間隔時間 如:monitorInterval="5" 就是5秒
-->
<Configuration>

    <!--配置全域性通用屬性-->
    <properties>
        <property name="logPath">D:\test</property>
    </properties>

    <!--配置Appender輸出型別-->
    <Appenders>
        <Console name="consoleAppender" target="SYSTEM_OUT"/>

        <!--配置檔案輸出-->
        <File name="fileAppender" fileName="${logPath}/log4j2.log">
            <!-- 配置檔案輸出格式 一樣遵循logback中的格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>
        </File>

        <!--
           按照指定規則來拆分日誌檔案

           fileName:日誌檔案的名字
           filePattern:日誌檔案拆分後檔案的命名規則
                       $${date:yyyy-MM-dd}:根據日期當天,建立一個資料夾
                             例如:2021-01-01這個資料夾中,記錄當天的所有日誌資訊(拆分出來的日誌放在這個資料夾中)
                                  2021-01-02這個資料夾中,記錄當天的所有日誌資訊(拆分出來的日誌放在這個資料夾中)
                        rollog-%d{yyyy-MM-dd-HH-mm}-%i.log
                             為檔案命名的規則:%i表示序號,從0開始,目的是為了讓每一份檔名字不會重複
       -->
        <RollingFile name="rollingFile"
                     fileName="${logPath}/rollingLog.log"
                     filePattern="${logPath}/$${date:yyyy-MM-dd}/rollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log">

            <!-- 日誌訊息格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>

            <Policies>
                <!-- 在系統啟動時,觸發拆分規則,產生一個日誌檔案 -->
                <OnStartupTriggeringPolicy/>

                <!-- 按照檔案的大小進行拆分 注:這裡雖然限制是10KB,但是真實生成出來的檔案一般都是比這數值大1KB-->
                <SizeBasedTriggeringPolicy size="10KB"/>

                <!-- 按照時間節點進行拆分 拆分規則就是filePattern-->
                <TimeBasedTriggeringPolicy/>
            </Policies>

            <!-- 在同一目錄下,檔案的個數限制,如果超出了設定的數值,則根據時間以新的覆蓋舊-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!-- 配置非同步日誌 -->
        <Async name="asyncAppender">
            <!-- 引用在前面配置的appender -->
            <AppenderRef ref="fileAppender"/>
        </Async>
    </Appenders>

    <!--配置logger-->
    <Loggers>
        <!--配置root logger-->
        <Root level="DEBUG">
            <AppenderRef ref="consoleAppender"/>
            <AppenderRef ref="fileAppender"/>
            <AppenderRef ref="rollingFile"/>
            <AppenderRef ref="asyncAppender"/>
        </Root>
    </Loggers>
</Configuration>


測試

    @Test
    public void asyncAppenderTest() {
        Logger logger = LoggerFactory.getLogger(Log4j2Test.class);

        for (int i = 0; i < 1000; i++) {
            logger.error("error資訊");
            logger.warn("warn資訊");
            logger.info("info資訊");
            logger.debug("debug資訊");
            logger.trace("trace資訊");
        }

        System.out.println("其他要執行的非同步任務1");
        System.out.println("其他要執行的非同步任務2");
        System.out.println("其他要執行的非同步任務3");
        System.out.println("其他要執行的非同步任務4");
        System.out.println("其他要執行的非同步任務5");
    }



6.1.6.2、AsyncLogger方式 - 必須會

  • AsyncLogger才是log4j2實現非同步最重要的功能體現,也是官方推薦的非同步方式
  • 它可以使得呼叫Logger.log返回的更快。你可以有兩種選擇:全域性非同步和混合非同步


6.1.6.2.1、全域性非同步 - 瞭解
  • 所有的日誌都非同步的記錄,在配置檔案上不用做任何改動,只需要在jvm啟動的時候增加一個引數即可實現

  • 設定方式

    • 只需要在類路徑resources下新增一個properties屬性檔案,做一步配置即可,**檔名要求必須是:log4j2.component.properties**,這個properties配置檔案內容如下:

      • Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
        
        

操作一波

resources目錄中新建log4j2.component.properties檔案,並配置如下內容:

Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector


log4j2.xml檔案配置( 不配置任何的async非同步設定 )

<?xml version="1.0" encoding="UTF-8" ?>
<!--一樣的,所有的日誌配置都需要在Configuration標籤中進行

    這個標籤中除了xmlns,還可以跟兩個屬性配置
        status="級別" 日誌框架本身的日誌輸出級別 一般都不需要配置,因為加了之後,輸出資訊會多一些其實沒多大用的內容
            如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/>
        monitorInterval="數值" 自動載入配置檔案的間隔時間 如:monitorInterval="5" 就是5秒
-->
<Configuration>

    <!--配置全域性通用屬性-->
    <properties>
        <property name="logPath">D:\test</property>
    </properties>

    <!--配置Appender輸出型別-->
    <Appenders>
        <Console name="consoleAppender" target="SYSTEM_OUT"/>

        <!--配置檔案輸出-->
        <File name="fileAppender" fileName="${logPath}/log4j2.log">
            <!-- 配置檔案輸出格式 一樣遵循logback中的格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>
        </File>

        <!--
           按照指定規則來拆分日誌檔案

           fileName:日誌檔案的名字
           filePattern:日誌檔案拆分後檔案的命名規則
                       $${date:yyyy-MM-dd}:根據日期當天,建立一個資料夾
                             例如:2021-01-01這個資料夾中,記錄當天的所有日誌資訊(拆分出來的日誌放在這個資料夾中)
                                  2021-01-02這個資料夾中,記錄當天的所有日誌資訊(拆分出來的日誌放在這個資料夾中)
                        rollog-%d{yyyy-MM-dd-HH-mm}-%i.log
                             為檔案命名的規則:%i表示序號,從0開始,目的是為了讓每一份檔名字不會重複
       -->
        <RollingFile name="rollingFile"
                     fileName="${logPath}/rollingLog.log"
                     filePattern="${logPath}/$${date:yyyy-MM-dd}/rollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log">

            <!-- 日誌訊息格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>

            <Policies>
                <!-- 在系統啟動時,觸發拆分規則,產生一個日誌檔案 -->
                <OnStartupTriggeringPolicy/>

                <!-- 按照檔案的大小進行拆分 注:這裡雖然限制是10KB,但是真實生成出來的檔案一般都是比這數值大1KB-->
                <SizeBasedTriggeringPolicy size="10KB"/>

                <!-- 按照時間節點進行拆分 拆分規則就是filePattern-->
                <TimeBasedTriggeringPolicy/>
            </Policies>

            <!-- 在同一目錄下,檔案的個數限制,如果超出了設定的數值,則根據時間以新的覆蓋舊-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!-- 配置非同步日誌 -->
        <!--<Async name="asyncAppender">-->
        <!--    &lt;!&ndash; 引用在前面配置的appender &ndash;&gt;-->
        <!--    <AppenderRef ref="fileAppender"/>-->
        <!--</Async>-->
    </Appenders>

    <!--配置logger-->
    <Loggers>
        <!--配置root logger-->
        <Root level="DEBUG">
            <AppenderRef ref="consoleAppender"/>
            <AppenderRef ref="fileAppender"/>
            <AppenderRef ref="rollingFile"/>
            <!--<AppenderRef ref="asyncAppender"/>-->
        </Root>
    </Loggers>
</Configuration>


測試

    @Test
    public void globalAsync() {
        Logger logger = LoggerFactory.getLogger(Log4j2Test.class);

        for (int i = 0; i < 1000; i++) {
            logger.error("error資訊");
            logger.warn("warn資訊");
            logger.info("info資訊");
            logger.debug("debug資訊");
            logger.trace("trace資訊");
        }

        System.out.println("其他要執行的非同步任務1");
        System.out.println("其他要執行的非同步任務2");
        System.out.println("其他要執行的非同步任務3");
        System.out.println("其他要執行的非同步任務4");
        System.out.println("其他要執行的非同步任務5");
    }



6.1.6.2.2、混合非同步 - 必須會
  • 可以在應用中同時使用同步日誌和非同步日誌,這使得日誌的配置方式更加靈活
  • 混合非同步的方式需要通過修改配置檔案來實現,使用AsyncLogger標籤來配置

log4j2.xml配置

  • 注意點:記得把全域性非同步的properties內容註釋掉,全域性非同步和混合非同步不能同時出現
  • 編寫方式:在appender標籤中石油``AsyncLogger`標籤,然後在這裡面引用要非同步輸出的appender即可
  • 下面配置的需求是:cn.zixieqing包下的日誌輸出到檔案進行非同步操作,而root logger日誌是同步操作
<?xml version="1.0" encoding="UTF-8" ?>
<!--一樣的,所有的日誌配置都需要在Configuration標籤中進行

    這個標籤中除了xmlns,還可以跟兩個屬性配置
        status="級別" 日誌框架本身的日誌輸出級別 一般都不需要配置,因為加了之後,輸出資訊會多一些其實沒多大用的內容
            如:<Configuration xmlns="http://logging.apache.org/log4j/2.0/config" status="DEBUG"/>
        monitorInterval="數值" 自動載入配置檔案的間隔時間 如:monitorInterval="5" 就是5秒
-->
<Configuration>

    <!--配置全域性通用屬性-->
    <properties>
        <property name="logPath">D:\test</property>
    </properties>

    <!--配置Appender輸出型別-->
    <Appenders>
        <Console name="consoleAppender" target="SYSTEM_OUT"/>

        <!--配置檔案輸出-->
        <File name="fileAppender" fileName="${logPath}/log4j2.log">
            <!-- 配置檔案輸出格式 一樣遵循logback中的格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>
        </File>

        <!--
           按照指定規則來拆分日誌檔案

           fileName:日誌檔案的名字
           filePattern:日誌檔案拆分後檔案的命名規則
                       $${date:yyyy-MM-dd}:根據日期當天,建立一個資料夾
                             例如:2021-01-01這個資料夾中,記錄當天的所有日誌資訊(拆分出來的日誌放在這個資料夾中)
                                  2021-01-02這個資料夾中,記錄當天的所有日誌資訊(拆分出來的日誌放在這個資料夾中)
                        rollog-%d{yyyy-MM-dd-HH-mm}-%i.log
                             為檔案命名的規則:%i表示序號,從0開始,目的是為了讓每一份檔名字不會重複
       -->
        <RollingFile name="rollingFile"
                     fileName="${logPath}/rollingLog.log"
                     filePattern="${logPath}/$${date:yyyy-MM-dd}/rollingLog-%d{yyyy-MM-dd-HH-mm}-%i.log">

            <!-- 日誌訊息格式 -->
            <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n"/>

            <Policies>
                <!-- 在系統啟動時,觸發拆分規則,產生一個日誌檔案 -->
                <OnStartupTriggeringPolicy/>

                <!-- 按照檔案的大小進行拆分 注:這裡雖然限制是10KB,但是真實生成出來的檔案一般都是比這數值大1KB-->
                <SizeBasedTriggeringPolicy size="10KB"/>

                <!-- 按照時間節點進行拆分 拆分規則就是filePattern-->
                <TimeBasedTriggeringPolicy/>
            </Policies>

            <!-- 在同一目錄下,檔案的個數限制,如果超出了設定的數值,則根據時間以新的覆蓋舊-->
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!-- 配置非同步日誌 -->
        <!--<Async name="asyncAppender">-->
        <!--    &lt;!&ndash; 引用在前面配置的appender &ndash;&gt;-->
        <!--    <AppenderRef ref="fileAppender"/>-->
        <!--</Async>-->

        <!-- 自定義logger,讓自定義的logger為非同步logger -->
        <!--
            name 就是包路徑,注意點:這個非同步配置只限於這個name指定的包下的日誌有非同步操作,要是另有一個包cn.xiegongzi
                    那麼:cn.xiegongzi包下的日誌還是同步的,並不會非同步

            includeLocation="false"
                表示去除日誌記錄中的行號資訊,這個行號資訊非常的影響日誌記錄的效率(生產中都不加這個行號)
                嚴重的時候可能記錄的比同步的日誌效率還有低

            additivity="false" 表示不繼承rootlogger

        -->
        <AsyncLogger name="cn.zixieqing"
                     level="debug"
                     includeLocation="false"
                     additivity="false">

            <!-- 檔案輸出fileAppender,設定為非同步列印 -->
            <AppenderRef ref="fileAppender"/>

        </AsyncLogger>
    </Appenders>

    <!--配置logger-->
    <!--現在這個root logger中的日誌是同步的-->
    <Loggers>
        <!--配置root logger-->
        <Root level="DEBUG">
            <AppenderRef ref="consoleAppender"/>
            <AppenderRef ref="fileAppender"/>
            <AppenderRef ref="rollingFile"/>
            <!--<AppenderRef ref="asyncAppender"/>-->
        </Root>
    </Loggers>
</Configuration>


測試

    @Test
    public void blendAsync() {
        Logger logger = LoggerFactory.getLogger(Log4j2Test.class);

        for (int i = 0; i < 1000; i++) {
            logger.error("error資訊");
            logger.warn("warn資訊");
            logger.info("info資訊");
            logger.debug("debug資訊");
            logger.trace("trace資訊");
        }

        System.out.println("其他要執行的非同步任務1");
        System.out.println("其他要執行的非同步任務2");
        System.out.println("其他要執行的非同步任務3");
        System.out.println("其他要執行的非同步任務4");
        System.out.println("其他要執行的非同步任務5");
    }



6.1.6.2.3、AsyncAppender、AsyncLogge兩種方式的建議
  • 如果使用非同步日誌,AsyncAppender、AsyncLogger不要同時出現,不會有這種奇葩需求,同時效果也不會疊加,如果同時出現,那麼效率會以AsyncAppender為主
  • 同樣的,AsyncLogger的全域性非同步和混合非同步也不要同時出現,也不會有這種奇葩,效果也不會疊加


7、下一篇博文地址


  • 另起一篇博文的原因:這篇部落格內容太多,載入量太大,這個垃圾部落格,老衲真心懷疑:TMD,它是每次更新時線上更新博文全部內容,TNND,不會在點選確認時再收集全部資料啊,到了3000多行以上的博文內容之後,老衲只要一寫內容,就發現在重新載入,然後我筆記本的風扇就開始快速轉動,記憶體快速升到60%以上
    image

  • 然後就發現瀏覽器彈出艹了它先人墳的如下頁面,害老衲又要重新把markdown中的內容重新複製過來,搞得老衲很想只弄我的知乎去了
    image

相關文章