Java Logging之JUL系列——Handler

前面的文章我們提到過吃溅,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ì)介紹吟孙。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子杰妓,更是在濱河造成了極大的恐慌藻治,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巷挥,死亡現(xiàn)場(chǎng)離奇詭異桩卵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)倍宾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門雏节,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人高职,你說我怎么就攤上這事钩乍。” “怎么了初厚?”我有些...
    開封第一講書人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)孙技。 經(jīng)常有香客問我产禾,道長(zhǎng),這世上最難降的妖魔是什么牵啦? 我笑而不...
    開封第一講書人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任亚情,我火速辦了婚禮,結(jié)果婚禮上哈雏,老公的妹妹穿的比我還像新娘楞件。我一直安慰自己,他們只是感情好裳瘪,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開白布土浸。 她就那樣靜靜地躺著,像睡著了一般彭羹。 火紅的嫁衣襯著肌膚如雪黄伊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,842評(píng)論 1 290
  • 那天派殷,我揣著相機(jī)與錄音还最,去河邊找鬼。 笑死毡惜,一個(gè)胖子當(dāng)著我的面吹牛拓轻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播经伙,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼扶叉,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辜梳,我...
    開封第一講書人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤粱甫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后作瞄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茶宵,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年宗挥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乌庶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡契耿,死狀恐怖瞒大,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情搪桂,我是刑警寧澤透敌,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站踢械,受9級(jí)特大地震影響酗电,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜内列,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一撵术、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧话瞧,春花似錦嫩与、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至埃篓,卻和暖如春古毛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背都许。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工稻薇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胶征。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓塞椎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親睛低。 傳聞我的和親對(duì)象是個(gè)殘疾皇子案狠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容

  • 在現(xiàn)實(shí)生活中服傍,記錄日志非常重要。銀行轉(zhuǎn)賬時(shí)會(huì)有轉(zhuǎn)賬記錄骂铁;飛機(jī)飛行過程中吹零,會(huì)有黑盒子(飛行數(shù)據(jù)記錄器)記錄飛行過程中...
    chliar閱讀 759評(píng)論 1 0
  • 本文章是我大概三年前,在上家單位使用 Python 工作時(shí)結(jié)合官方文檔做的整理±郑現(xiàn)在 Python 官方文檔聽說已...
    好吃的野菜閱讀 216,558評(píng)論 14 232
  • 上文提到過灿椅,LogRecord可以理解為是一個(gè)DTO,那么LogRecord里面到底存儲(chǔ)了哪些數(shù)據(jù)呢钞支?我們可以看一...
    codecrazy閱讀 1,503評(píng)論 0 1
  • 雜英紛已積茫蛹,含芳獨(dú)暮春; 還如故園樹烁挟,忽憶故園人婴洼。
    蒼山暮雪閱讀 406評(píng)論 1 4
  • 入春以來,頻繁變換的天氣撼嗓,短短數(shù)日就將春夏秋冬一起經(jīng)歷柬采。幾次低溫過后,一場(chǎng)大雨且警,千樹萬樹梨花開遍粉捻,鮮活的世界充滿了...
    秋水伊人_44ad閱讀 231評(píng)論 0 0