前面的文章我們提到過吃溅,Handler是真正執(zhí)行日志輸出操作的地方侦镇,JUL中的Handler由java.util.logging.Handler抽象類來表示勋陪。有兩個(gè)實(shí)現(xiàn)類直接繼承自Handler牢裳,分別是StreamHandler和MemoryHandler着饥,而StreamHandler又有三個(gè)直接子類分別是ConsoleHandler,FileHandler以及SocketHandler参咙。
Handler中有一個(gè)最核心的抽象方法就是publish()龄广,該方法的聲明如下所示:
public abstract void publish(LogRecord record);
Handler的作用就是用來將日志輸出到外部的,不同的Handler能夠?qū)⑷罩据敵龅讲煌牡胤桨豪铡treamHandler能夠?qū)⑷罩就ㄟ^一個(gè)OutputStream進(jìn)行輸出蜀细,StreamHandler的三個(gè)不同的子類即使用了不同的OutputStream對(duì)象。
ConsoleHandler會(huì)將日志輸出到控制臺(tái)戈盈,對(duì)應(yīng)的OutputStream對(duì)象是System.err
FileHandler會(huì)將日志輸出到文件奠衔,對(duì)應(yīng)的OutputStream對(duì)象是FileOutputStream
SocketHandler會(huì)將日志輸出到網(wǎng)絡(luò)套接字,對(duì)應(yīng)的OutputStream對(duì)象是Socket.getOutputStream()塘娶。
MemoryHandler的實(shí)現(xiàn)跟StreamHandler的實(shí)現(xiàn)不同归斤,它不是將日志寫入輸出流,而是將日志輸出到一個(gè)內(nèi)存緩沖區(qū)中刁岸,本文后面會(huì)詳細(xì)介紹MemoryHandler脏里。
我們先來介紹一下Handler中公共的一些屬性。Handler抽象類中含有如下get/set方法:
Level getLevel()
Filter getFilter()
Formatter getFormatter()
String getEncoding()
ErrorManager getErrorManager()
getLevel()是用來獲取Handler的級(jí)別的虹曙,之前提到過迫横,不僅Logger對(duì)象有級(jí)別番舆,Handler中也有級(jí)別,如果需要進(jìn)行輸出的日志信息的級(jí)別(即LogRecord中的級(jí)別)低于Hander中的級(jí)別時(shí)矾踱,也不會(huì)有實(shí)際的輸出操作恨狈。
getFilter()是用來獲取Handler的過濾器的,跟Level類似呛讲,不僅Logger對(duì)象可以設(shè)置過濾器禾怠,Handler中也能設(shè)置過濾器。
getFormatter()用來獲取格式化輸出器贝搁,Handler是用來對(duì)日志進(jìn)行實(shí)際輸出的組件吗氏,但是用什么樣的格式進(jìn)行輸出需要借助于Formatter格式化器,不同的Formatter輸出的信息格式是不一樣的雷逆,JUL中提供了兩種內(nèi)置的Formatter弦讽,一種是SimpleFormater,另一種是XMLFormatter,我們也可以實(shí)現(xiàn)自己的格式化器膀哲,只需要繼承自java.util.logging.Formatter抽象類坦袍,并重寫它的String format(LogRecord logRecord)方法。關(guān)于Formatter的信息我們后文再進(jìn)行介紹等太。
getEncoding()獲取字符編碼信息捂齐,由于Handler是用來對(duì)日志信息進(jìn)行實(shí)際輸出操作的,因此在輸出的過程中需要指定字符編碼方式缩抡。
getErrorManager()返回錯(cuò)誤處理器奠宜,errorManager用來處理日志記錄過程中發(fā)生的異常信息。
ConsoleHander會(huì)將日志信息輸出到控制臺(tái)瞻想,在我們通過Logger.getLogger(String name)方法拿到日志記錄器實(shí)例之后压真,我們可以對(duì)該日志記錄器進(jìn)行顯示地設(shè)置,比如設(shè)置logger的級(jí)別為INFO蘑险,設(shè)置logger的Handler為ConsoleHander,設(shè)置ConsoleHandler的級(jí)別為INFO,設(shè)置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");
}
}
這樣一來就會(huì)將INFO及其以上級(jí)別的日志信息以SimpleFormatter的格式輸出到控制臺(tái)。最終控制臺(tái)輸出如下:
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
我們發(fā)現(xiàn)一條日志被輸出了兩遍佃迄。至于為什么會(huì)輸出兩遍我們下一篇文章中會(huì)進(jìn)行詳細(xì)的分析泼差,現(xiàn)在我們只關(guān)注一點(diǎn):日志確實(shí)按照我們?cè)O(shè)置的以SimpleFormatter的格式將INFO級(jí)別的日志信息輸出到了控制臺(tái)。
FileHandler會(huì)將日志輸出到文件中呵俏,我們可以修改上面的代碼堆缘,給日志記錄器Logger配置一個(gè)FileHandler,從而將日志信息輸出到文件中,我們也順便把Formatter設(shè)置為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");
}
}
運(yùn)行程序之后會(huì)在當(dāng)前目錄下生成一個(gè)mylog.txt文件,文件內(nèi)容如下所示:
<?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缀皱,因此日志信息輸出到了我們指定的文件中斗这,由于我們?cè)O(shè)置了Formatter為XMLFormatter,因此最終的日志信息是以XML的格式展示的啤斗。
FileHandler給我們提供了多種不同的配置方式涝影,如根據(jù)pattern配置文件名格式,配置文件數(shù)目争占,配置文件大小,配置是否將信息追加到已有的文件中等等序目。具體有哪些配置方式我們后面介紹JUL的配置文件時(shí)再詳細(xì)介紹臂痕。
SocketHandler會(huì)將日志信息發(fā)送到網(wǎng)絡(luò)服務(wù)器,首先我們寫一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)服務(wù)器猿涨,監(jiān)聽在本地握童,端口號(hào)為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();
}
}
}
之后再設(shè)置我們的日志記錄器的Hander為SocketHandler叛赚,并指定主機(jī)和端口號(hào)澡绩,代碼如下所示:
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");
}
}
之后先運(yùn)行LoggingServer,再運(yùn)行JavaLogging,我們?cè)贚oggingServer的控制臺(tái)能看到如下輸出:
<!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格式發(fā)送到了本地主機(jī)俺附,監(jiān)聽端口為8888的網(wǎng)絡(luò)服務(wù)器肥卡,我們的網(wǎng)絡(luò)服務(wù)器接收到信息之后簡(jiǎn)單地將其在控制臺(tái)打印了出來。
MemoryHandler直接繼承自Handler,該Handler在內(nèi)部維護(hù)了一個(gè)LogRecord數(shù)組事镣,即一個(gè)內(nèi)存緩沖區(qū)步鉴,通過MemoryHandler的publish()方法輸出的日志信息首先進(jìn)入內(nèi)存緩沖區(qū)中,緩沖區(qū)如果滿了的話璃哟,新進(jìn)入的日志信息從緩沖區(qū)的頭部開始覆蓋氛琢,形成一個(gè)循環(huán)。只有到了一定條件的時(shí)候才通過“target Handler”向外部進(jìn)行輸出随闪。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屬性之外還多了一個(gè)pushLevel屬性阳似,該屬性與是否將日志信息交給target Handler進(jìn)行輸出有關(guān)。
buffer[]是用來緩存通過MemoryHandler的publish方法寫入的LogRecord對(duì)象的铐伴,其他Handler的publish方法是直接將LogRecord輸出到系統(tǒng)外部撮奏,而MemoryHandler是先將LogRecord緩存在內(nèi)部。
size屬性用來設(shè)置buffer數(shù)組的大小的当宴,默認(rèn)大小是DEFAULT_SIZE(即1000),也就是說默認(rèn)情況下挽荡,如果緩存了1000個(gè)LogRecord之后還沒有將日志信息發(fā)送到外部,那么后面進(jìn)來的LogRecord將從緩沖區(qū)的頭部開始覆蓋即供,start和count就是用來控制循環(huán)操作buffer[]的定拟。
target屬性就是當(dāng)達(dá)到一定條件時(shí)需要最終將緩沖區(qū)的日志信息輸出到外部的Handler,我們?cè)趧?chuàng)建MemoryHandler的時(shí)候一般來說需要設(shè)置該屬性,否則那些日志信息只是駐留在內(nèi)存中,而不會(huì)進(jìn)入外部系統(tǒng)青自,比如文件株依,控制臺(tái)或者網(wǎng)絡(luò)套接字等等。
我們可以看一下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緩沖區(qū)中的恋腕,重點(diǎn)關(guān)注最后的一個(gè)if語句,如果日志信息的級(jí)別高于或者等于MemoryHandler的pushLevel逆瑞,那么就要執(zhí)行push方法荠藤。push方法就是將buffer中的LogRecord全部發(fā)送到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;
}
循環(huán)調(diào)用target Handler的publish方法將緩沖區(qū)的LogRecord全部發(fā)送出去,并清空緩沖區(qū)获高。
默認(rèn)情況下哈肖,MemoryHandler會(huì)將LogRecord緩存起來,直到遇到某個(gè)LogRecord的級(jí)別高于或者等于pushLevel念秧,這時(shí)會(huì)將緩沖區(qū)中的所有LogRecord全部發(fā)送到目標(biāo)Handler進(jìn)行輸出淤井,并清空緩沖區(qū)。默認(rèn)情況下pushLevel的值為L(zhǎng)evel.SEVERE摊趾。
我們可以通過配置pushLevel來改變觸發(fā)push操作的時(shí)機(jī)币狠,比如配置成LogRecord級(jí)別高于WARNING時(shí)就push。我們也可以實(shí)現(xiàn)自己的MemoryHandler砾层,這樣就可以更靈活地按照我們的業(yè)務(wù)需求設(shè)置觸發(fā)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設(shè)置了一個(gè)MemoryHandler,該MemoryHandler的目標(biāo)handler是ConsoleHander,緩沖區(qū)大小設(shè)置為100肛炮,pushLevel級(jí)別設(shè)置為L(zhǎng)evel.WARNING渐行。
如果我們注釋掉了logger.server那一行,那么logger.info那一行的信息是不會(huì)通過MemoryHandler輸出到控制臺(tái)的铸董,因?yàn)闆]有達(dá)到push的觸發(fā)條件祟印,即沒有收到級(jí)別高于或者等于WARNING級(jí)別的LogRecord,而一旦執(zhí)行到logger.severe那一行粟害,就會(huì)觸發(fā)push蕴忆,從而將緩沖區(qū)中的所有LogRecord輸出到控制臺(tái)。
我們實(shí)際執(zhí)行的過程中會(huì)發(fā)現(xiàn)悲幅,就算注釋掉了logger.severe那一行套鹅,控制臺(tái)還是輸出了一行日志信息:
Aug 12, 2018 6:10:37 PM cn.codecrazy.study.logging.JavaLogging main
INFO: Hello, Java Logging
需要注意的是,這個(gè)日志的輸出不是通過MemoryHandler的targe Handler輸出來的汰具,而是直接通過另一個(gè)ConsoleHandler輸出來的卓鹿。為什么還會(huì)有另一個(gè)ConsoleHandler對(duì)日志進(jìn)行輸出呢?這涉及到JUL中的日志記錄器層級(jí)關(guān)系——Logger Hierarchy留荔,我們下篇文章再詳細(xì)介紹吟孙。