1. 程式人生 > >日誌框架--(二)JDK Logging

日誌框架--(二)JDK Logging

前言

   從jdk1.4起,JDK開始自帶一套日誌系統。JDK Logger最大的優點就是不需要任何類庫的支援,只要有Java的執行環境就可以使用。相對於其他的日誌框架,JDK自帶的日誌可謂是雞肋,無論易用性,功能還是擴充套件性都要稍遜一籌,所以在商業系統中很少直接使用。

JDK Logging API提供了七個日誌級別用來控制輸出。這七個級別分別是:

級別

SEVERE

WARNING 

INFO

CONFIG 

FINE 

FINER

FINEST

呼叫方法

severe()

warning()

info()

config()

fine()

finer()

finest()

含意

嚴重

警告

資訊

配置

良好

較好

最好

如果將級別設為info,那麼info值錢的低級別資訊將不會輸出,只有info級別只有的資訊會輸出,通過控制級別達到控制輸出的目的。

1 Logger的使用

複製程式碼

package com.bes.logging;  
   
import java.util.logging.Level;  
import java.util.logging.Logger;  
   
public class LoggerTest {  
      private static Loggerlogger = Logger.getLogger("com.bes.logging");  
      public static void main(String argv[]) {  
               // Log a FINEtracing message  
               logger.info("Main running.");  
               logger.fine("doingstuff");  
               try {  
                         Thread.currentThread().sleep(1000);// do some work  
               } catch(Exception ex) {  
                         logger.log(Level.WARNING,"trouble sneezing", ex);  
               }  
               logger.fine("done");  
      }  
}  

複製程式碼

不做任何程式碼修改和JDK配置修改的話,執行上面的例子,你會發現,控制檯只會出現【Main running.】這一句日誌。如下問題應該呈現在你的大腦裡…

1,【Main running.】以外的日誌為什麼沒有輸出?怎麼讓它們也能夠出現?

2,日誌中出現的時間、類名、方法名等是從哪裡輸出的?

3,為什麼日誌就會出現在控制檯?

4,大型的系統可能有很多子模組(可簡單理解為有很多包名),如何對這些子模組進行單獨的日誌級別控制?

5,擴充:apache那個流行的log4j專案和JDK的logging有聯絡嗎,怎麼實現自己的LoggerManager?

帶著這些問題,可能你更有興趣瞭解一下JDK的logging機制,本章為你分析這個簡單模組的機制。

2. Logging 配置

JDK預設的logging配置檔案為:$JAVA_HOME/jre/lib/logging.properties,可以使用系統屬性java.util.logging.config.file指定相應的配置檔案對預設的配置檔案進行覆蓋,比如, java -Djava.util.logging.config.file=myfile

配置檔案中通常包含以下幾部分定義:

1,  handlers:用逗號分隔每個Handler,這些handler將會被加到root logger中。也就是說即使我們不給其他logger配置handler屬性,在輸出日誌的時候logger會一直找到root logger,從而找到handler進行日誌的輸入。

2,  .level是root logger的日誌級別

3,  <handler>.xxx是配置具體某個handler的屬性,比如java.util.logging.ConsoleHandler.formatter便是為ConsoleHandler配置相應的日誌Formatter.

4,  logger的配置,所有以[.level]結尾的屬性皆被認為是對某個logger的級別的定義,如com.bes.server.level=FINE是給名為[com.bes.server]的logger定義級別為FINE。順便說下,前邊提到過logger的繼承關係,如果還有com.bes.server.webcontainer這個logger,且在配置檔案中沒有定義該logger的任何屬性,那麼其將會從[com.bes.server]這個logger進行屬性繼承。除了級別之外,還可以為logger定義handler和useParentHandlers(預設是為true)屬性,如com.bes.server.handler=com.bes.test.ServerFileHandler(需要是一個extends java.util.logging.Handler的類),com.bes.server.useParentHandlers=false(意味著com.bes.server這個logger進行日誌輸出時,日誌僅僅被處理一次,用自己的handler輸出,不會傳遞到父logger的handler)。

以下是JDK配置檔案示例

複製程式碼

handlers= java.util.logging.FileHandler,java.util.logging.ConsoleHandler  
   
.level= INFO  
   
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  
   
java.util.logging.ConsoleHandler.level = INFO  
java.util.logging.ConsoleHandler.formatter =java.util.logging.SimpleFormatter  
   
com.xyz.foo.level = SEVERE 

複製程式碼

3. Logging執行原理

3.1.Logger的獲取

1.首先是呼叫Logger的如下方法獲得一個logger

public static synchronized Logger getLogger(String name) {  
       LogManager manager =LogManager.getLogManager();  
    returnmanager.demandLogger(name);  
} 

2.上面的呼叫會觸發java.util.logging.LoggerManager的類初始化工作,LoggerManager有一個靜態化初始化塊(這是會先於LoggerManager的建構函式呼叫的):

複製程式碼

 static {
        manager = AccessController.doPrivileged(new PrivilegedAction<LogManager>() {
            @Override
            public LogManager run() {
                LogManager mgr = null;
                String cname = null;
                try {
                    cname = System.getProperty("java.util.logging.manager");
                    if (cname != null) {
                        try {
                            Class<?> clz = ClassLoader.getSystemClassLoader()
                                    .loadClass(cname);
                            mgr = (LogManager) clz.newInstance();
                        } catch (ClassNotFoundException ex) {
                            Class<?> clz = Thread.currentThread()
                                    .getContextClassLoader().loadClass(cname);
                            mgr = (LogManager) clz.newInstance();
                        }
                    }
                } catch (Exception ex) {
                    System.err.println("Could not load Logmanager \"" + cname + "\"");
                    ex.printStackTrace();
                }
                if (mgr == null) {
                    mgr = new LogManager();
                }
                return mgr;

            }
        });
    }

複製程式碼

從靜態初始化塊中可以看出LoggerManager是可以使用系統屬性java.util.logging.manager指定一個繼承自java.util.logging.LoggerManager的類進行替換的,比如Tomcat啟動指令碼中就使用該機制以使用自己的LoggerManager。

不管是JDK預設的java.util.logging.LoggerManager還是自定義的LoggerManager,初始化工作中均會給LoggerManager新增兩個logger,一個是名稱為””的root logger,且logger級別設定為預設的INFO;另一個是名稱為global的全域性logger,級別仍然為INFO。

LogManager”類”初始化完成之後就會讀取配置檔案(預設為$JAVA_HOME/jre/lib/logging.properties),把配置檔案的屬性名<->屬性值這樣的鍵值對儲存在記憶體中,方便之後初始化logger的時候使用。

3.第1步驟中Logger類發起的getLogger操作將會呼叫java.util.logging.LoggerManager的如下方法:

複製程式碼

Logger demandLogger(String name) {  
  Logger result =getLogger(name);  
  if (result == null) {  
      result = newLogger(name, null);  
      addLogger(result);  
      result =getLogger(name);  
  }  
  return result;  
} 

複製程式碼

可以看出,LoggerManager首先從現有的logger列表中查詢,如果找不到的話,會新建一個looger並加入到列表中。當然很重要的是新建looger之後需要對logger進行初始化,這個初始化詳見java.util.logging.LoggerManager#addLogger()方法中,改方法會根據配置檔案設定logger的級別以及給logger新增handler等操作。

 到此為止logger已經獲取到了,你同時也需要知道此時你的logger中已經有級別、handler等重要資訊,下面將分析輸出日誌時的邏輯。 

3.2.日誌的輸出

首先我們通常會呼叫Logger類下面的方法,傳入日誌級別以及日誌內容。

複製程式碼

 public void log(Level level, String msg) {
        if (!isLoggable(level)) {
            return;
        }
        LogRecord lr = new LogRecord(level, msg);
        doLog(lr);
    }

複製程式碼

該方法可以看出,Logger類首先是進行級別的校驗,如果級別校驗通過,則會新建一個LogRecord物件,LogRecord中除了日誌級別,日誌內容之外還會包含呼叫執行緒資訊,日誌時刻等;之後呼叫doLog(LogRecord lr)方法

複製程式碼

 private void doLog(LogRecord lr) {
        lr.setLoggerName(name);
        final LoggerBundle lb = getEffectiveLoggerBundle();
        final ResourceBundle  bundle = lb.userBundle;
        final String ebname = lb.resourceBundleName;
        if (ebname != null && bundle != null) {
            lr.setResourceBundleName(ebname);
            lr.setResourceBundle(bundle);
        }
        log(lr);
    }

複製程式碼

doLog(LogRecord lr)方法中設定了ResourceBundle資訊(這個與國際化有關)之後便直接呼叫log(LogRecord record) 方法 

複製程式碼

    public void log(LogRecord record) {
        if (!isLoggable(record.getLevel())) {
            return;
        }
        Filter theFilter = filter;
        if (theFilter != null && !theFilter.isLoggable(record)) {
            return;
        }

        // Post the LogRecord to all our Handlers, and then to
        // our parents' handlers, all the way up the tree.

        Logger logger = this;
        while (logger != null) {
            final Handler[] loggerHandlers = isSystemLogger
                ? logger.accessCheckedHandlers()
                : logger.getHandlers();

            for (Handler handler : loggerHandlers) {
                handler.publish(record);
            }

            final boolean useParentHdls = isSystemLogger
                ? logger.useParentHandlers
                : logger.getUseParentHandlers();

            if (!useParentHdls) {
                break;
            }

            logger = isSystemLogger ? logger.parent : logger.getParent();
        }
    }

複製程式碼

很清晰,while迴圈是重中之重,首先從logger中獲取handler,然後分別呼叫handler的publish(LogRecordrecord)方法。while迴圈證明了前面提到的會一直把日誌委託給父logger處理的說法,當然也證明了可以使用logger的useParentHandlers屬性控制日誌不進行往上層logger傳遞的說法。到此為止logger對日誌的控制差不多算是完成,接下來的工作就是看handler的了,這裡我們以java.util.logging.ConsoleHandler為例說明日誌的輸出。

複製程式碼

public ConsoleHandler() {
        sealed = false;
        configure();
        setOutputStream(System.err);
        sealed = true;
    }

複製程式碼

ConsoleHandler建構函式中除了需要呼叫自身的configure()方法進行級別、filter、formatter等的設定之外,最重要的我們最關心的是setOutputStream(System.err)這一句,把系統錯誤流作為其輸出。而ConsoleHandler的publish(LogRecordrecord)是繼承自java.util.logging.StreamHandler的,如下所示:   

複製程式碼

  public synchronized void publish(LogRecord record) {
        if (!isLoggable(record)) {
            return;
        }
        String msg;
        try {
            msg = getFormatter().format(record);
        } catch (Exception ex) {
            // We don't want to throw an exception here, but we
            // report the exception to any registered ErrorManager.
            reportError(null, ex, ErrorManager.FORMAT_FAILURE);
            return;
        }

        try {
            if (!doneHeader) {
                writer.write(getFormatter().getHead(this));
                doneHeader = true;
            }
            writer.write(msg);
        } catch (Exception ex) {
            // We don't want to throw an exception here, but we
            // report the exception to any registered ErrorManager.
            reportError(null, ex, ErrorManager.WRITE_FAILURE);
        }
    }

複製程式碼

方法邏輯也很清晰,首先是呼叫Formatter對訊息進行格式化,說明一下:格式化其實是進行國際化處理的重要契機。然後直接把訊息輸出到對應的輸出流中。需要注意的是handler也會用自己的level和LogRecord中的level進行比較,看是否真正輸出日誌。

4.總結

至此,整個日誌輸出過程已經分析完成。我們來解答文章開頭的四個問題了。

1,【Main running.】以外的日誌為什麼沒有輸出?怎麼讓它們也能夠出現?

    這就是JDK預設的logging.properties檔案中配置的handler級別和跟級別均為info導致的,如果希望看到FINE級別日誌,需要修改logging.properties檔案,同時進行如下兩個修改

    java.util.logging.ConsoleHandler.level= FINE//修改

    com.bes.logging.level=FINE//新增

2,日誌中出現的時間、類名、方法名等是從哪裡輸出的?

    請參照[java.util.logging.ConsoleHandler.formatter= java.util.logging.SimpleFormatter]配置中指定的java.util.logging.SimpleFormatter類,其publicsynchronized String format(LogRecord record) 方法說明了一切。

複製程式碼

    public synchronized String format(LogRecord record) {
        dat.setTime(record.getMillis());
        String source;
        if (record.getSourceClassName() != null) {
            source = record.getSourceClassName();
            if (record.getSourceMethodName() != null) {
               source += " " + record.getSourceMethodName();
            }
        } else {
            source = record.getLoggerName();
        }
        String message = formatMessage(record);
        String throwable = "";
        if (record.getThrown() != null) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            pw.println();
            record.getThrown().printStackTrace(pw);
            pw.close();
            throwable = sw.toString();
        }
        return String.format(format,
                             dat,
                             source,
                             record.getLoggerName(),
                             record.getLevel().getLocalizedLevelName(),
                             message,
                             throwable);
    }

複製程式碼

3,為什麼日誌就會出現在控制檯?

    看到java.util.logging.ConsoleHandler 類構造方法中的[setOutputStream(System.err)]語句,相信你已經明白。

4,大型的系統可能有很多子模組(可簡單理解為有很多包名),如何對這些子模組進行單獨的日誌級別控制?

    在logging.properties檔案中分別對各個logger的級別進行定義,且最好使用java.util.logging.config.file屬性指定自己的配置檔案。

第5個問題暫時還解答不了,請繼續期待,在後面的博文將講述log4j和JDK logging的關係,以及怎麼實現自己的LoggerManager以使得我們完全定製化logger、handler、formatter,掌控日誌的國際化資訊等。