天天看點

Java Logging之JUL系列——Handler

前面的文章我們提到過,Handler是真正執行日志輸出操作的地方,JUL中的Handler由java.util.logging.Handler抽象類來表示。有兩個實作類直接繼承自Handler,分别是StreamHandler和MemoryHandler,而StreamHandler又有三個直接子類分别是ConsoleHandler,FileHandler以及SocketHandler。

Handler中有一個最核心的抽象方法就是publish(),該方法的聲明如下所示:

public abstract void publish(LogRecord record);

Handler的作用就是用來将日志輸出到外部的,不同的Handler能夠将日志輸出到不同的地方。StreamHandler能夠将日志通過一個OutputStream進行輸出,StreamHandler的三個不同的子類即使用了不同的OutputStream對象。

ConsoleHandler會将日志輸出到控制台,對應的OutputStream對象是System.err

FileHandler會将日志輸出到檔案,對應的OutputStream對象是FileOutputStream

SocketHandler會将日志輸出到網絡套接字,對應的OutputStream對象是Socket.getOutputStream()。

MemoryHandler的實作跟StreamHandler的實作不同,它不是将日志寫入輸出流,而是将日志輸出到一個記憶體緩沖區中,本文後面會詳細介紹MemoryHandler。

我們先來介紹一下Handler中公共的一些屬性。Handler抽象類中含有如下get/set方法:

Level getLevel()

Filter getFilter()

Formatter getFormatter()

String getEncoding()

ErrorManager getErrorManager()

getLevel()是用來擷取Handler的級别的,之前提到過,不僅Logger對象有級别,Handler中也有級别,如果需要進行輸出的日志資訊的級别(即LogRecord中的級别)低于Hander中的級别時,也不會有實際的輸出操作。

getFilter()是用來擷取Handler的過濾器的,跟Level類似,不僅Logger對象可以設定過濾器,Handler中也能設定過濾器。

getFormatter()用來擷取格式化輸出器,Handler是用來對日志進行實際輸出的元件,但是用什麼樣的格式進行輸出需要借助于Formatter格式化器,不同的Formatter輸出的資訊格式是不一樣的,JUL中提供了兩種内置的Formatter,一種是SimpleFormater,另一種是XMLFormatter,我們也可以實作自己的格式化器,隻需要繼承自java.util.logging.Formatter抽象類,并重寫它的String format(LogRecord logRecord)方法。關于Formatter的資訊我們後文再進行介紹。

getEncoding()擷取字元編碼資訊,由于Handler是用來對日志資訊進行實際輸出操作的,是以在輸出的過程中需要指定字元編碼方式。

getErrorManager()傳回錯誤處理器,errorManager用來處理日志記錄過程中發生的異常資訊。

ConsoleHander會将日志資訊輸出到控制台,在我們通過Logger.getLogger(String name)方法拿到日志記錄器執行個體之後,我們可以對該日志記錄器進行顯示地設定,比如設定logger的級别為INFO,設定logger的Handler為ConsoleHander,設定ConsoleHandler的級别為INFO,設定ConsoleHandler的Formatter為SimpleFormatter。代碼如下所示:

public class JavaLogging {
    private static final Logger logger = Logger.getLogger(JavaLogging.class.getName());
    static {
        logger.setLevel(Level.INFO);
        Handler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.INFO);
        consoleHandler.setFormatter(new SimpleFormatter());
        logger.addHandler(consoleHandler);
    }
    public static void main(String[] args) {
        logger.info("Hello, Java Logging");
    }
}
           

這樣一來就會将INFO及其以上級别的日志資訊以SimpleFormatter的格式輸出到控制台。最終控制台輸出如下:

Aug 12, 2018 4:20:44 PM cn.codecrazy.study.logging.JavaLogging main
INFO: Hello, Java Logging
Aug 12, 2018 4:20:44 PM cn.codecrazy.study.logging.JavaLogging main
INFO: Hello, Java Logging
           

我們發現一條日志被輸出了兩遍。至于為什麼會輸出兩遍我們下一篇文章中會進行詳細的分析,現在我們隻關注一點:日志确實按照我們設定的以SimpleFormatter的格式将INFO級别的日志資訊輸出到了控制台。

FileHandler會将日志輸出到檔案中,我們可以修改上面的代碼,給日志記錄器Logger配置一個FileHandler,進而将日志資訊輸出到檔案中,我們也順便把Formatter設定為XMLFormatter,看看XMLFormatter輸出來的格式是什麼樣的。代碼如下:

public class JavaLogging {
    private static final Logger logger = Logger.getLogger(JavaLogging.class.getName());
    static {
        logger.setLevel(Level.INFO);
        Handler fileHandler = null;
        try {
            fileHandler = new FileHandler("mylog.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }
        fileHandler.setLevel(Level.INFO);
        fileHandler.setFormatter(new XMLFormatter());
        logger.addHandler(fileHandler);
    }
    public static void main(String[] args) {
        logger.info("Hello, Java Logging");
    }
}
           

運作程式之後會在目前目錄下生成一個mylog.txt檔案,檔案内容如下所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2018-08-12T16:31:06</date>
  <millis>1534062666182</millis>
  <sequence>0</sequence>
  <logger>cn.codecrazy.study.logging.JavaLogging</logger>
  <level>INFO</level>
  <class>cn.codecrazy.study.logging.JavaLogging</class>
  <method>main</method>
  <thread>1</thread>
  <message>Hello, Java Logging</message>
</record>
</log>
           

可以看到,由于我們給Logger指定的Handler是FileHandler,是以日志資訊輸出到了我們指定的檔案中,由于我們設定了Formatter為XMLFormatter,是以最終的日志資訊是以XML的格式展示的。

FileHandler給我們提供了多種不同的配置方式,如根據pattern配置檔案名格式,配置檔案數目,配置檔案大小,配置是否将資訊追加到已有的檔案中等等。具體有哪些配置方式我們後面介紹JUL的配置檔案時再詳細介紹。

SocketHandler會将日志資訊發送到網絡伺服器,首先我們寫一個簡單的網絡伺服器,監聽在本地,端口号為8888,代碼如下所示:

public class LoggingServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);
        while(true) {
            Socket socket = serverSocket.accept();
            Runnable task = () -> handleSocket(socket);
            Executors.newFixedThreadPool(3).submit(task);
        }
    }
    private static void handleSocket(Socket socket) {
        try {
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
           

之後再設定我們的日志記錄器的Hander為SocketHandler,并指定主機和端口号,代碼如下所示:

public class JavaLogging {
    private static final Logger logger = Logger.getLogger(JavaLogging.class.getName());
    static {
        logger.setLevel(Level.INFO);
        Handler socketHandler = null;
        try {
            socketHandler = new SocketHandler("localhost", 8888);
        } catch (IOException e) {
            e.printStackTrace();
        }
        socketHandler.setLevel(Level.INFO);
        socketHandler.setFormatter(new XMLFormatter());
        logger.addHandler(socketHandler);
    }
    public static void main(String[] args) {
        logger.info("Hello, Java Logging");
    }
}
           

之後先運作LoggingServer,再運作JavaLogging,我們在LoggingServer的控制台能看到如下輸出:

<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2018-08-12T17:13:30</date>
  <millis>1534065210654</millis>
  <sequence>0</sequence>
  <logger>cn.codecrazy.study.logging.JavaLogging</logger>
  <level>INFO</level>
  <class>cn.codecrazy.study.logging.JavaLogging</class>
  <method>main</method>
  <thread>1</thread>
  <message>Hello, Java Logging</message>
</record>
</log>
           

說明我們的SocketHandler将我們的日志資訊以XML格式發送到了本地主機,監聽端口為8888的網絡伺服器,我們的網絡伺服器接收到資訊之後簡單地将其在控制台列印了出來。

MemoryHandler直接繼承自Handler,該Handler在内部維護了一個LogRecord數組,即一個記憶體緩沖區,通過MemoryHandler的publish()方法輸出的日志資訊首先進入記憶體緩沖區中,緩沖區如果滿了的話,新進入的日志資訊從緩沖區的頭部開始覆寫,形成一個循環。隻有到了一定條件的時候才通過“target Handler”向外部進行輸出。MemoryHandler的部分屬性如下所示:

private final static int DEFAULT_SIZE = 1000;
private volatile Level pushLevel;
private int size;
private Handler target;
private LogRecord buffer[];
int start, count;
           

MemoryHandler除了有其他Handler都有的level屬性之外還多了一個pushLevel屬性,該屬性與是否将日志資訊交給target Handler進行輸出有關。

buffer[]是用來緩存通過MemoryHandler的publish方法寫入的LogRecord對象的,其他Handler的publish方法是直接将LogRecord輸出到系統外部,而MemoryHandler是先将LogRecord緩存在内部。

size屬性用來設定buffer數組的大小的,預設大小是DEFAULT_SIZE(即1000),也就是說預設情況下,如果緩存了1000個LogRecord之後還沒有将日志資訊發送到外部,那麼後面進來的LogRecord将從緩沖區的頭部開始覆寫,start和count就是用來控制循環操作buffer[]的。

target屬性就是當達到一定條件時需要最終将緩沖區的日志資訊輸出到外部的Handler,我們在建立MemoryHandler的時候一般來說需要設定該屬性,否則那些日志資訊隻是駐留在記憶體中,而不會進入外部系統,比如檔案,控制台或者網絡套接字等等。

我們可以看一下MemoryHandler中的publish方法:

public synchronized void publish(LogRecord record) {
    if (!isLoggable(record)) {
        return;
    }
    int ix = (start+count)%buffer.length;
    buffer[ix] = record;
    if (count < buffer.length) {
        count++;
    } else {
        start++;
        start %= buffer.length;
    }
    if (record.getLevel().intValue() >= pushLevel.intValue()) {
        push();
    }
}
           

可以看到,方法中的前面部分是用來将LogRecord存入buffer緩沖區中的,重點關注最後的一個if語句,如果日志資訊的級别高于或者等于MemoryHandler的pushLevel,那麼就要執行push方法。push方法就是将buffer中的LogRecord全部發送到target Handler,push方法代碼如下:

public synchronized void push() {
    for (int i = 0; i < count; i++) {
        int ix = (start+i)%buffer.length;
        LogRecord record = buffer[ix];
        target.publish(record);
    }
    // Empty the buffer.
    start = 0;
    count = 0;
}
           

循環調用target Handler的publish方法将緩沖區的LogRecord全部發送出去,并清空緩沖區。

預設情況下,MemoryHandler會将LogRecord緩存起來,直到遇到某個LogRecord的級别高于或者等于pushLevel,這時會将緩沖區中的所有LogRecord全部發送到目标Handler進行輸出,并清空緩沖區。預設情況下pushLevel的值為Level.SEVERE。

我們可以通過配置pushLevel來改變觸發push操作的時機,比如配置成LogRecord級别高于WARNING時就push。我們也可以實作自己的MemoryHandler,這樣就可以更靈活地按照我們的業務需求設定觸發push的操作。使用MemoryHandler的代碼如下所示:

public class JavaLogging {
    private static final Logger logger = Logger.getLogger(JavaLogging.class.getName());
    static {
        logger.setLevel(Level.INFO);
        Handler memoryHandler = new MemoryHandler(new ConsoleHandler(), 100, Level.WARNING);
        memoryHandler.setLevel(Level.INFO);
        memoryHandler.setFormatter(new SimpleFormatter());
        logger.addHandler(memoryHandler);
    }
    public static void main(String[] args) {
        logger.info("Hello, Java Logging");
        logger.severe("severe");
    }
}
           

我們給logger設定了一個MemoryHandler,該MemoryHandler的目标handler是ConsoleHander,緩沖區大小設定為100,pushLevel級别設定為Level.WARNING。

如果我們注釋掉了logger.server那一行,那麼logger.info那一行的資訊是不會通過MemoryHandler輸出到控制台的,因為沒有達到push的觸發條件,即沒有收到級别高于或者等于WARNING級别的LogRecord,而一旦執行到logger.severe那一行,就會觸發push,進而将緩沖區中的所有LogRecord輸出到控制台。

我們實際執行的過程中會發現,就算注釋掉了logger.severe那一行,控制台還是輸出了一行日志資訊:

Aug 12, 2018 6:10:37 PM cn.codecrazy.study.logging.JavaLogging main
INFO: Hello, Java Logging
           

需要注意的是,這個日志的輸出不是通過MemoryHandler的targe Handler輸出來的,而是直接通過另一個ConsoleHandler輸出來的。為什麼還會有另一個ConsoleHandler對日志進行輸出呢?這涉及到JUL中的日志記錄器層級關系——Logger Hierarchy,我們下篇文章再詳細介紹。