JDK Logging源碼學習筆記

(1)Apache log4j-1.2.17源碼學習筆記 http://blog.csdn.net/zilong_zilong/article/details/78715500
(2)Apache log4j-1.2.17問答式學習筆記 http://blog.csdn.net/zilong_zilong/article/details/78916626 
(3)JDK Logging源碼學習筆記  http://aperise.iteye.com/blog/2411850

1.JDK Logging介紹

        斷點調試和記錄日誌,是程序員排查問題的2個有效手段,斷點調試需要對全盤代碼熟門熟路,費時費力,如果代碼不開源那麼此種方法就不能使用,相對於斷點調試,記錄日誌提供了另外一種更有效的排錯方法,預先植入了有效的日誌信息,後期只需通過配置文件即可管理日誌,藉助工具掃描日誌文件內容可以有效的監測當前運行的系統是否運行正常。記錄日誌作爲一個通用的重要的模塊,所以開源組織分別推出了自己的日誌框架,比如Apache Log4j,Apache Log4j 2、Apache Commons Logging、Slf4j、Logback和JDK Logging,今天我要分享的是JDK Logging日誌框架

        JDK Logging的組成及其關係如下圖所示:

        在上圖中,Application代表我們的Java程序,Logger代表用戶日誌輸出logger,每個用戶logger上關聯着handler,最終通過用戶logger的handler將日誌輸出到外界Outside World(內存、操作系統文件、socket),同時每個handler上也可以配置filter進行過濾filter,特殊的對於MemoryHandler可以將應用程序的日誌事件向下一個handler相傳,所以MemoryHandler的處理流程如下:
        其它詳細說明詳見JDK官方說明文檔,JDK Logging的官方介紹文檔鏈接如下:

JDK1.6 https://docs.oracle.com/javase/6/docs/technotes/guides/logging/overview.html
JDK1.7 https://docs.oracle.com/javase/7/docs/technotes/guides/logging/overview.html
JDK1.8 https://docs.oracle.com/javase/8/docs/technotes/guides/logging/overview.html

        在項目中使用JDK Logging舉例如下:

package com.wombat;
import java.util.logging.*;
public class Nose {
    private static Logger logger = Logger.getLogger("com.wombat.nose");
    private static FileHandler fh = new FileHandler("mylog.txt");
    public static void main(String argv[]) {
        // Send logger output to our FileHandler.
        logger.addHandler(fh);
        // Request that every detail gets logged.
        logger.setLevel(Level.ALL);
        // Log a simple INFO message.
        logger.info("doing stuff");
        try {
            Wombat.sneeze();
        } catch (Exception ex) {
            logger.log(Level.WARNING, "trouble sneezing", ex);
        }
        logger.fine("done");
    }
}

        接下來的環節就從這個代碼入手,從源碼的角度來逐步分析和學習JDK Logging。

 

2.LogManager的static代碼塊源碼分析(RootLogger創建過程)

        之前在Apache log4j-1.2.17的源碼分析中我們看到log4j-1.2.17也是通過類LogManager的靜態代碼塊來進行的,這裏對於JDK Logging也是一樣,下面將LogManager的static代碼塊註釋如下:


        上面代碼中有2個JAVA的基礎知識這裏也順便提一下:

  • AccessController.doPrivileged屬於特權操作,意思是不管此方法由哪個用戶發起,都無需對此操作涉及的資源(文件讀寫特權等等)進行檢查
  • System.getProperty用戶獲取JVM系統變量的值或者由JDK的參數-Dproterty=value設置的屬性的值

        這兩個知識點在衆多的開源框架中都被廣泛使用,好了回到正題,上面代碼中默認獲取的java.util.logging.manager爲空,所以cname=null,所以默認會通過構造方法new LogManager()創建LogManager對象,之後就是一系列上下文設置操作,但直至代碼結束,我們注意到始終未看到在哪裏讀取過日誌配置文件logging.properties(爲什麼是logging.properties這個接下來會講到),實際上對於logging.properties的加載是採用延遲加載策略,只會在第一次調用LogManager.getLogManager()纔開始解析logging.properties日誌配置文件,這裏注意和Apache log4j-1.2.17加載log4j.properties類比來增強學習,Apache log4j-1.2.17是直接在靜態代碼塊中就加載解析了log4j.properties文件的,並未採用延遲加載。

 

3.JDK Logging讀取日誌配置文件logging.properties代碼分析

        在此文的開頭對於JDK Logging的舉例使用中,我們看到對於JDK Logging的使用的第一行代碼爲:

        那麼我們就從此作爲入口,先看下JDK Logging中Logger.getLogger代碼:

        上面代碼中,當Logger.getLogger被調用時,接着會調用代碼Logger.demandLogger,而Logger.demandLogger中正如章節2中提到的,調用了全局靜態類LogManager的方法LogManager.getLogManager(),那麼接下來就詳細看下LogManager.getLogManager()相關代碼:

        從上面的代碼中,我們終於找到了JDK Logging讀取日誌配置文件的蛛絲馬跡,繼續跟蹤代碼Logger.readConfiguration:

        上面的代碼中我們看到,JDK Logging加載日誌配置文件是有先後順序的:

  • 首先通過java.util.logging.config.class配置的類來初始化JDK Logging;
  • 如果java.util.logging.config.class找不到,接着通過java.util.logging.config.file配置的文件來初始化JDK Logging;
  • 最後如果java.util.logging.config.file找不到,就通過操作系統安裝的JDK的jre/lib下的logging.properties來初始化JDK Logging;
  • 最後如果連JDK都爲安裝,直接拋出錯誤Error("Can't find java.home ??")

  

4.JDK Logging的用戶logger創建過程

        在上面我們已經看到了RootLogger的創建過程,RootLogger是所有用戶logger的父親logger,這裏繼續查看我們的用戶logger如何創建的,一般創建用戶logger的代碼如下:

Logger logger = Logger.getLogger("com.wombat.nose");

        那麼我們就以此爲入口查看logger是如何創建的,類Logger裏getLogger方法代碼如下:

        在上面的代碼中,我們看到,最終都會執行如下這行代碼:

        繼續分析這段代碼,如下:

        我們看到如果用戶logger不存在就創建,創建logger的代碼如下:
        如果用戶logger存在就直接從UserContext上下文獲取,那麼我們繼續關注下用戶logger創建完畢後,代碼addLogger(newLogger)幹了點啥,代碼如下:


        上面的代碼告訴我們創建用戶logger後,添加logger到usercontext,配置logger的handler以及其level屬性,很關鍵的信息是用戶logger只會創建一次

 

5.日誌輸出代碼logger.info(String msg)代碼分析

        上面我們已經看到創建了logger對象,接着就是在需要輸出日誌的地方調用logger.info(String msg)輸出日誌,那麼我們看下這塊的代碼邏輯,代碼分析如下:

 

6.JDK Logging的日誌級別

        我們習慣了Apache log4j-1.2.17,那麼這裏就講JDK Logging的日誌級別與Apache log4j-1.2.17進行對別如下:


7.JDK Logging的Handlers及其屬性配置

        JDK Logging的Handlers的類繼承關係圖如下:

        這裏在之前的代碼中,我們看到對於handler,每次創建,只設置了handler的level屬性,那麼對於其他屬性,是哪裏設置的呢?每個handler的具體實現類裏面都有個privatevoid configure() 方法,這個方法值得我們特別關注。

    7.1 MemoryHandler

        MemoryHandler會將LogRecords存儲到內存當中,存儲到內存的同時清理掉之前的LogRecords,MemoryHandler的屬性設置方法private void configure()在構造方法public MemoryHandler()中被調用,代碼如下:

        從上面的代碼我們看到MemoryHandler的配置屬性列表如下:

  • java.util.logging.MemoryHandler.level 設置handler的level(defaults to Level.ALL).
  • java.util.logging.MemoryHandler.filter 設置handler的filter (defaults to no Filter).
  • java.util.logging.MemoryHandler.size 設置handler的內存緩衝區數組的大小 (defaults to 1000).
  • java.util.logging.MemoryHandler.push 設置push level (defaults to level.SEVERE).
  • java.util.logging.MemoryHandler.target 設置目標handler給這個MemoryHandler (no default).

 

    7.2 StreamHandler

        流處理handler的基礎類,其實現類如下:

        StreamHandler的屬性設置方法private void configure()在構造方法public StreamHandler()中被調用,代碼如下:

        從上面可以看到StreamHandler的配置屬性如下:

  • java.util.logging.StreamHandler.level 設置handler的level (defaults to Level.INFO).
  • java.util.logging.StreamHandler.filter 設置handler的filter的類 (defaults to no Filter).
  • java.util.logging.StreamHandler.formatter 設置handler的formatter類 (defaults to java.util.logging.SimpleFormatter).
  • java.util.logging.StreamHandler.encoding 設置編碼格式 (defaults to the default platform encoding).

 

    7.3 ConsoleHandler

        ConsoleHandler默認將LogRecords輸出到System.err,ConsoleHandler的屬性設置方法private void configure()在構造方法public ConsoleHandler()中被調用,代碼如下:

        從上面可知ConsoleHandler的配置屬性如下:

  • java.util.logging.ConsoleHandler.level 設置handler的level (defaults to Level.INFO).
  • java.util.logging.ConsoleHandler.filter 設置handler使用的filter類 (defaults to no Filter).
  • java.util.logging.ConsoleHandler.formatter 設置formatter類 (defaults to java.util.logging.SimpleFormatter).
  • java.util.logging.ConsoleHandler.encoding 設置編碼格式(defaults to the default platform encoding).

 

    7.4 FileHandler

        FileHandler負責將LogRecords輸出到操作系統的文件,FileHandler的屬性設置方法private void configure()在構造方法public FileHandler()中被調用,代碼如下:

        從上面可以看出FileHandler的配置屬性如下:

  • java.util.logging.FileHandler.level 設置handler的level (defaults to Level.ALL).
  • java.util.logging.FileHandler.filter 設置handler使用的filter類 (defaults to no Filter).
  • java.util.logging.FileHandler.formatter 設置formatter類 (defaults to java.util.logging.XMLFormatter)
  • java.util.logging.FileHandler.encoding 設置編碼格式 (defaults to the default platform encoding).
  • java.util.logging.FileHandler.limit 設置一個日誌文件最大文件大小,單位bytes 。設置0表示沒有任何限制 (Defaults to no limit).
  • java.util.logging.FileHandler.count 設置最多保留幾個日誌文件 (defaults to 1).
  • java.util.logging.FileHandler.pattern 設置產生日誌文件的名字的規則(Defaults to "%h/java%u.log").
  • java.util.logging.FileHandler.append 設置是否在已經存在的日誌文件裏最後追加日誌,默認false (defaults to false).

 

    7.5 SocketHandler

        SocketHandler負責將LogRecords輸出到socket接口,SocketHandler的屬性設置方法private void configure()在構造方法public SocketHandler()中被調用,代碼如下:

        從上面可以看到SocketHandler的配置屬性如下:

  • java.util.logging.SocketHandler.level 設置handler的level(defaults to Level.ALL).
  • java.util.logging.SocketHandler.filter 設置handler使用的filter類 (defaults to no Filter).
  • java.util.logging.SocketHandler.formatter 設置編formatter類 (defaults to java.util.logging.XMLFormatter).
  • java.util.logging.SocketHandler.encoding 設置編碼格式 (defaults to the default platform encoding).
  • java.util.logging.SocketHandler.host 設置socket接口的IP地址 (no default).
  • java.util.logging.SocketHandler.port 設置socket接口的port (no default).

 

8.JDK Logging的Formatters

        Formatters的繼承關係圖如下:


    

    8.1 SimpleFormatter

        SimpleFormatter按照Java使用Java的String.format方法按照格式"%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%6$s%n"來格式化日誌進行輸出,代碼如下:


    8.2 XMLFormatter

        XMLFormatter負責將LogRecord格式化爲如下的XML格式:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2000-08-23 19:21:05</date>
  <millis>967083665789</millis>
  <sequence>1256</sequence>
  <logger>kgh.test.fred</logger>
  <level>INFO</level>
  <class>kgh.test.XMLTest</class>
  <method>writeLog</method>
  <thread>10</thread>
  <message>Hello world!</message>
</record>
</log>

        XMLFormatter的代碼部分註釋如下:

public class XMLFormatter extends Formatter {
    private LogManager manager = LogManager.getLogManager();

    // 將小於10的數字前補充0
    private void a2(StringBuffer sb, int x) {
        if (x < 10) {
            sb.append('0');
        }
        sb.append(x);
    }

    // 時間按照ISO 8601標準輸出
    private void appendISO8601(StringBuffer sb, long millis) {
        Date date = new Date(millis);
        sb.append(date.getYear() + 1900);
        sb.append('-');
        a2(sb, date.getMonth() + 1);
        sb.append('-');
        a2(sb, date.getDate());
        sb.append('T');
        a2(sb, date.getHours());
        sb.append(':');
        a2(sb, date.getMinutes());
        sb.append(':');
        a2(sb, date.getSeconds());
    }

    // 將於XML字符<>衝突的字符進行轉換,如果字符串爲null,則輸出<null>
    private void escape(StringBuffer sb, String text) {
        if (text == null) {
            text = "<null>";
        }
        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);
            if (ch == '<') {
                sb.append("&lt;");
            } else if (ch == '>') {
                sb.append("&gt;");
            } else if (ch == '&') {
                sb.append("&amp;");
            } else {
                sb.append(ch);
            }
        }
    }

    /**
     * 轉換爲如下XML格式的日誌輸出
     *<?xml version="1.0" encoding="UTF-8" standalone="no"?>
     *<!DOCTYPE log SYSTEM "logger.dtd">
     *<log>
     *<record>
     *  <date>2000-08-23 19:21:05</date>
     *  <millis>967083665789</millis>
     *  <sequence>1256</sequence>
     *  <logger>kgh.test.fred</logger>
     *  <level>INFO</level>
     *  <class>kgh.test.XMLTest</class>
     *  <method>writeLog</method>
     *  <thread>10</thread>
     *  <message>Hello world!</message>
     *</record>
     *</log>
     *
     * @param record 被格式化的日誌消息
     * @return 返回被格式化後的XML
     */
    public String format(LogRecord record) {
        StringBuffer sb = new StringBuffer(500);
        sb.append("<record>\n");

        sb.append("  <date>");
        appendISO8601(sb, record.getMillis());
        sb.append("</date>\n");

        sb.append("  <millis>");
        sb.append(record.getMillis());
        sb.append("</millis>\n");

        sb.append("  <sequence>");
        sb.append(record.getSequenceNumber());
        sb.append("</sequence>\n");

        String name = record.getLoggerName();
        if (name != null) {
            sb.append("  <logger>");
            escape(sb, name);
            sb.append("</logger>\n");
        }

        sb.append("  <level>");
        escape(sb, record.getLevel().toString());
        sb.append("</level>\n");

        if (record.getSourceClassName() != null) {
            sb.append("  <class>");
            escape(sb, record.getSourceClassName());
            sb.append("</class>\n");
        }

        if (record.getSourceMethodName() != null) {
            sb.append("  <method>");
            escape(sb, record.getSourceMethodName());
            sb.append("</method>\n");
        }

        sb.append("  <thread>");
        sb.append(record.getThreadID());
        sb.append("</thread>\n");

        if (record.getMessage() != null) {
            // Format the message string and its accompanying parameters.
            String message = formatMessage(record);
            sb.append("  <message>");
            escape(sb, message);
            sb.append("</message>");
            sb.append("\n");
        }

        // 如果消息需要國際化來實現本地化,這裏進行處理
        ResourceBundle bundle = record.getResourceBundle();
        try {
            if (bundle != null && bundle.getString(record.getMessage()) != null) {
                sb.append("  <key>");
                escape(sb, record.getMessage());
                sb.append("</key>\n");
                sb.append("  <catalog>");
                escape(sb, record.getResourceBundleName());
                sb.append("</catalog>\n");
            }
        } catch (Exception ex) {
            // The message is not in the catalog.  Drop through.
        }

        Object parameters[] = record.getParameters();
        //  Check to see if the parameter was not a messagetext format
        //  or was not null or empty
        if ( parameters != null && parameters.length != 0
                && record.getMessage().indexOf("{") == -1 ) {
            for (int i = 0; i < parameters.length; i++) {
                sb.append("  <param>");
                try {
                    escape(sb, parameters[i].toString());
                } catch (Exception ex) {
                    sb.append("???");
                }
                sb.append("</param>\n");
            }
        }

        if (record.getThrown() != null) {
            // Report on the state of the throwable.
            Throwable th = record.getThrown();
            sb.append("  <exception>\n");
            sb.append("    <message>");
            escape(sb, th.toString());
            sb.append("</message>\n");
            StackTraceElement trace[] = th.getStackTrace();
            for (int i = 0; i < trace.length; i++) {
                StackTraceElement frame = trace[i];
                sb.append("    <frame>\n");
                sb.append("      <class>");
                escape(sb, frame.getClassName());
                sb.append("</class>\n");
                sb.append("      <method>");
                escape(sb, frame.getMethodName());
                sb.append("</method>\n");
                // Check for a line number.
                if (frame.getLineNumber() >= 0) {
                    sb.append("      <line>");
                    sb.append(frame.getLineNumber());
                    sb.append("</line>\n");
                }
                sb.append("    </frame>\n");
            }
            sb.append("  </exception>\n");
        }

        sb.append("</record>\n");
        return sb.toString();
    }

    /**
     * 返回XML的消息頭,也即返回如下格式
     *<?xml version="1.0" encoding="UTF-8" standalone="no"?>
     *<!DOCTYPE log SYSTEM "logger.dtd">
     *<log>
     *
     * @param   h  The target handler (can be null)
     * @return  a valid XML string
     */
    public String getHead(Handler h) {
        StringBuffer sb = new StringBuffer();
        String encoding;
        sb.append("<?xml version=\"1.0\"");

        if (h != null) {
            encoding = h.getEncoding();
        } else {
            encoding = null;
        }

        if (encoding == null) {
            // Figure out the default encoding.
            encoding = java.nio.charset.Charset.defaultCharset().name();
        }
        // Try to map the encoding name to a canonical name.
        try {
            Charset cs = Charset.forName(encoding);
            encoding = cs.name();
        } catch (Exception ex) {
            // We hit problems finding a canonical name.
            // Just use the raw encoding name.
        }

        sb.append(" encoding=\"");
        sb.append(encoding);
        sb.append("\"");
        sb.append(" standalone=\"no\"?>\n");
        sb.append("<!DOCTYPE log SYSTEM \"logger.dtd\">\n");
        sb.append("<log>\n");
        return sb.toString();
    }

    /**
     * 返回XML的尾部,也即返回</log>
     *
     * @param   h  The target handler (can be null)
     * @return  a valid XML string
     */
    public String getTail(Handler h) {
        return "</log>\n";
    }
}

        其中logger.dtd的定義如下:

<!-- DTD used by the java.util.logging.XMLFormatter -->
<!-- This provides an XML formatted log message. -->

<!-- The document type is "log" which consists of a sequence
of record elements -->
<!ELEMENT log (record*)>

<!-- Each logging call is described by a record element. -->
<!ELEMENT record (date, millis, sequence, logger?, level,
class?, method?, thread?, message, key?, catalog?, param*, exception?)>

<!-- Date and time when LogRecord was created in ISO 8601 format -->
<!ELEMENT date (#PCDATA)>

<!-- Time when LogRecord was created in milliseconds since
midnight January 1st, 1970, UTC. -->
<!ELEMENT millis (#PCDATA)>

<!-- Unique sequence number within source VM. -->
<!ELEMENT sequence (#PCDATA)>

<!-- Name of source Logger object. -->
<!ELEMENT logger (#PCDATA)>

<!-- Logging level, may be either one of the constant
names from java.util.logging.Level (such as "SEVERE"
or "WARNING") or an integer value such as "20". -->
<!ELEMENT level (#PCDATA)>

<!-- Fully qualified name of class that issued
logging call, e.g. "javax.marsupial.Wombat". -->
<!ELEMENT class (#PCDATA)>

<!-- Name of method that issued logging call.
It may be either an unqualified method name such as
"fred" or it may include argument type information
in parenthesis, for example "fred(int,String)". -->
<!ELEMENT method (#PCDATA)>

<!-- Integer thread ID. -->
<!ELEMENT thread (#PCDATA)>

<!-- The message element contains the text string of a log message. -->
<!ELEMENT message (#PCDATA)>

<!-- If the message string was localized, the key element provides
the original localization message key. -->
<!ELEMENT key (#PCDATA)>

<!-- If the message string was localized, the catalog element provides
the logger's localization resource bundle name. -->
<!ELEMENT catalog (#PCDATA)>

<!-- If the message string was localized, each of the param elements
provides the String value (obtained using Object.toString())
of the corresponding LogRecord parameter. -->
<!ELEMENT param (#PCDATA)>

<!-- An exception consists of an optional message string followed
by a series of StackFrames. Exception elements are used
for Java exceptions and other java Throwables. -->
<!ELEMENT exception (message?, frame+)>

<!-- A frame describes one line in a Throwable backtrace. -->
<!ELEMENT frame (class, method, line?)>

<!-- an integer line number within a class's source file. -->
<!ELEMENT line (#PCDATA)>

 

9.logging.properties

        logging.properties是JDK Logging的默認配置文件,其默認位置在JDK的安裝根路徑下的jre/lib下,最近我換成MAC PRO了,所以這裏我截圖我的logging.properties的配置文件位置如下:

        默認的logging.properties配置文件的內容如下:

 
############################################################
#  	Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.  
# For example java -Djava.util.logging.config.file=myfile
############################################################

############################################################
#  	Global properties
############################################################

# "handlers" specifies a comma separated list of log Handler 
# classes.  These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.ConsoleHandler

# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers.  For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= INFO

############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################

# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# Example to customize the SimpleFormatter output format 
# to print one-line log message like this:
#     <level>: <log message> [<date/time>]
#
# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################

# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE

 

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