溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java日志的核心組件是什么

發(fā)布時間:2022-01-06 21:01:15 來源:億速云 閱讀:150 作者:iii 欄目:編程語言

本篇內(nèi)容介紹了“Java日志的核心組件是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

Java日志基礎(chǔ)

Java使用了一種自定義的、可擴展的方法來輸出日志。雖然Java通過java.util.logging包提供了一套基本的日志處理API,但你可以很輕松的使用一種或者多種其它日志解決方案。這些解決方案盡管使用不同的方法來創(chuàng)建日志數(shù)據(jù),但它們的最終目標(biāo)是一樣的,即將日志從你的應(yīng)用程序輸出到目標(biāo)地址。

在這一節(jié)中,我們會探索Java日志背后的原理,并說明如何通過日志來讓你成為一個更好的Java開發(fā)人員。

Java日志組件

Java日志API由以下三個核心組件組成:

  • Loggers:Logger負(fù)責(zé)捕捉事件并將其發(fā)送給合適的Appender。

  • Appenders:也被稱為Handlers,負(fù)責(zé)將日志事件記錄到目標(biāo)位置。在將日志事件輸出之前,Appenders使用Layouts來對事件進(jìn)行格式化處理。

  • Layouts:也被稱為Formatters,它負(fù)責(zé)對日志事件中的數(shù)據(jù)進(jìn)行轉(zhuǎn)換和格式化。Layouts決定了數(shù)據(jù)在一條日志記錄中的最終形式。

當(dāng)Logger記錄一個事件時,它將事件轉(zhuǎn)發(fā)給適當(dāng)?shù)腁ppender。然后Appender使用Layout來對日志記錄進(jìn)行格式化,并將其發(fā)送給控制臺、文件或者其它目標(biāo)位置。另外,F(xiàn)ilters可以讓你進(jìn)一步指定一個Appender是否可以應(yīng)用在一條特定的日志記錄上。在日志配置中,F(xiàn)ilters并不是必需的,但可以讓你更靈活地控制日志消息的流動。

Java日志的核心組件是什么

日志框架

在Java中,輸出日志需要使用一個或者多個日志框架,這些框架提供了必要的對象、方法和配置來傳輸消息。Java在java.util.logging包中提供了一個默認(rèn)的框架。除此之外,還有很多其它第三方框架,包括Log4j、Logback以及tinylog。還有其它一些開發(fā)包,例如SLF4J和Apache Commons Logging,它們提供了一些抽象層,對你的代碼和日志框架進(jìn)行解耦,從而允許你在不同的日志框架中進(jìn)行切換。

如何選擇一個日志解決方案,這取決于你的日志需求的復(fù)雜度、和其它日志解決方案的兼容性、易用性以及個人喜好。Logback基于Log4j之前的版本開發(fā)(版本1),因此它們的功能集合都非常類似。然而,Log4j在***版本(版本2)中引用了一些改進(jìn),例如支持多API,并提升了在用Disruptor庫的性能。而tinylog,由于缺少了一些功能,運行特別快,非常適合小項目。

另外一個考慮因素是框架在基于Java的各種不同項目上的支持程度。例如Android程序只能使用Log4j、Logback或者第三方包來記錄日志, Apache Tomcat可以使用Log4j來記錄內(nèi)部消息,但只能使用版本1的Log4j。

抽象層

諸如SLF4J這樣的抽象層,會將你的應(yīng)用程序從日志框架中解耦。應(yīng) 用程序可以在運行時選擇綁定到一個特定的日志框架(例如java.util.logging、Log4j或者Logback),這通過在應(yīng)用程序的類路徑 中添加對應(yīng)的日志框架來實現(xiàn)。如果在類路徑中配置的日志框架不可用,抽象層就會立刻取消調(diào)用日志的相應(yīng)邏輯。抽象層可以讓我們更加容易地改變項目現(xiàn)有的日志框架,或者集成那些使用了不同日志框架的項目。

配置

盡管所有的Java日志框架都可以通過代碼進(jìn)行配置,但是大部分配置還是通過外部配置文件完成的。這些文件決定了日志消息在何時通過什么方式進(jìn)行處理,日志框架可以在運行時加載這些文件。在這一節(jié)中提供的大部分配置示例都使用了配置文件。

java.util.logging

默認(rèn)的Java日志框架將其配置存儲到一個名為 logging.properties 的文件中。在這個文件中,每行是一個配置項,配置項使用點標(biāo)記(dot notation)的形 式。Java在其安裝目錄的lib文件夾下面安裝了一個全局配置文件,但在啟動一個Java程序時,你可以通過指定  java.util.logging.config.file 屬性的方式來使用一個單獨的日志配置文件,同樣也可以在個人項目中創(chuàng)建和存儲  logging.properties 文件。

下面的示例描述了如何在全局的logging.properties文件中定義一個Appender:

# 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

Log4j

Log4j版本1使 用的語法和 java.util.logging 的語法很類似。使用了Log4j的程序會在項目目錄中尋找一個名為 log4j.properties  的文件。默認(rèn)情況下,Log4j配置會將所有日志消息輸出到控制臺上。Log4j同樣也支持XML格式的配置文件,對應(yīng)的配置信息會存儲到  log4j.xml 文件中。

Log4j版本2支持XML、JSON和YAML格式的配置,這些配置會分別存儲到 log4j2.xml、log4j2.json 和 log4j2.yaml 文件中。和版本1類似,版本2也會在工程目錄中尋找這些文件。你可以在每個版本的文檔中找到相應(yīng)的配置文件示例。

Logback

對于Logback來說,大部分配置都是在 logback.xml 文件中完成的,這個文件使用了和Log4j類似的XML語法。Logback同時也支持通過Groovy語言的方式來進(jìn)行配置,配置信息會存儲到 logback.groovy 文件中。你可以通過每種類型配置文件的鏈接找到對應(yīng)的配置文件示例。

Loggers

Loggers是用來觸發(fā)日志事件的對象,在我們的Java應(yīng)用程序中被創(chuàng)建和調(diào)用,然后Loggers才會將事件傳遞給Appender。一個類 中可以包含針對不同事件的多個獨立的Loggers,你也可以在一個Loggers里面內(nèi)嵌一個Loggers,從而創(chuàng)建一種Loggers層次結(jié)構(gòu)。

創(chuàng)建新Logger

在不同的日志框架下面創(chuàng)建新Logger過程大同小異,盡管調(diào)用的具體方法名稱可能不同。在使用 java.util.logging  時,你可以通過 Logger.getLogger().getLogger()  方法創(chuàng)建新Logger,這個方法接收一個string參數(shù),用于指定Logger的名字。如果指定名字的Logger已經(jīng)存在,那么只需要返回已經(jīng)存在 的Logger;否則,程序會創(chuàng)建一個新Logger。通常情況下,一種好的做法是,我們在當(dāng)前類下使用 class.getName() 作為新Logger的名字。

Logger logger = Logger.getLogger(MyClass.class.getName());

記錄日志事件

Logger提供了幾種方法來觸發(fā)日志事件。然而,在你記錄一個事件之前,你還需要設(shè)置級別。日志級別用來確定日志的嚴(yán)重程度,它可以用來過濾日志事件或者將其發(fā)送給不同的Appender(想了解更多信息,請參考“日志級別”一節(jié)),Logger.log() 方法除了日志消息以外,還需要一個日志級別作為參數(shù):

logger.log(Level.WARNING, “This is a warning!”);

大部分日志框架都針對輸出特定級別日志提供了快捷方式。例如,下面語句的作用和上面語句的作用是一樣的:

logger.warning(“This is a warning!”);

你還可以阻止Logger輸出低于指定日志級別的消息。在下面的示例中,Logger只能輸出高于WARNING級別的日志消息,并丟棄日志級別低于WARNING的消息:

logger.setLevel(Level.WARNING);

我們還有另外一些方法可以用來記錄額外的信息。logp()(精確日志)可以讓你指定每條日志記錄的源類(source class)和方法,而 logrb()(使用資源綁定的日志)可以讓你指定用于提取日志消息的資源。entering() 和 exiting() 方法可以讓你記錄方法調(diào)用信息,從而追蹤程序的執(zhí)行過程。

Appenders

Appenders將日志消息轉(zhuǎn)發(fā)給期望的輸出。它負(fù)責(zé)接收日志事件,使用Layout格式化事件,然后將其發(fā)送給對應(yīng)的目標(biāo)。對于一個日志事件,我們可以使用多個Appenders來將事件發(fā)送到不同的目標(biāo)位置。例如,我們可以在控制臺上顯示一個簡單的日志事件的同時,將其通過郵件的方式發(fā)送給指定的接收者。

請注意,在java.util.logging中,Appenders被稱作Handlers。

增加Appender

大部分日志框架的Appender都會執(zhí)行類似的功能,但在實現(xiàn)方面大相徑庭。如果使用 java.util.logging,你可以使用  Logger.addHandler()  方法將Appender添加到Logger中。例如,下面的代碼添加了一個新的ConsoleHandler,它會將日志輸出到控制臺:

logger.addHandler(new ConsoleHandler());

一種更常用的添加Appender的方式是使用配置文件。如果使用 java.util.logging,Appenders會定義一個以逗號隔開的列表,下面的示例將日志事件輸出到控制臺和文件:

handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler

如果使用基于XML的配置文件,Appenders會被添加到<Appenders>元素下面,如果使用Log4j,我們可以很容易地添加一個新ConsoleAppender來將日志消息發(fā)送到System.out:

<Console name="console" target="SYSTEM_OUT">
  <PatternLayout pattern="[%p] %t: %m%n" />
</Console>

Appenders類型

這一節(jié)描述了一些更通用的Appenders,以及它們在各種日志框架中是如何實現(xiàn)的。

ConsoleAppender

ConsoleAppender是最常用的Appenders之一,它只是將日志消息顯示到控制臺上。許多日志框架都將其作為默認(rèn)的Appender,并且在基本的配置中進(jìn)行預(yù)配置。例如,在Log4j中ConsoleAppender的配置參數(shù)如下所示。

參數(shù)

描述

filter

用于決定是否需要使用該Appender來處理日志事件

layout

用于決定如何對日志記錄進(jìn)行格式化,默認(rèn)情況下使用“%m%n”,它會在每一行顯示一條日志記錄

follow

用于決定Appender是否需要了解輸出(system.out或者system.err)的變化,默認(rèn)情況是不需要跟蹤這種變化

name

用于設(shè)置Appender的名字

ignoreExceptions

用于決定是否需要記錄在日志事件處理過程中出現(xiàn)的異常

target

用于指定輸出目標(biāo)位置,默認(rèn)情況下使用SYSTEM_OUT,但也可以修改成SYSTEM_ERR

一個完整的Log4j2的配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp">
   <Appenders>
     <Console name="MyAppender" target="SYSTEM_OUT">
       <PatternLayout pattern="%m%n"/>
     </Console>
   </Appenders>
   <Loggers>
     <Root level="error">
       <AppenderRef ref="MyAppender"/>
     </Root>
   </Loggers>
</Configuration>

這個配置文件創(chuàng)建了一個名為MyAppender的ConsoleAppender,它使用PatternLayout來對日志事件進(jìn)行格式化,然 后再將其輸出到System.out。<Loggers>元素對定義在程序代碼中的Loggers進(jìn)行了配置。在這里,我們只配置了一個 LoggerConfig,即名為Root的Logger,它會接收哪些日志級別在ERROR以上的日志消息。如果我們使用logger.error() 來記錄一個消息,那么它就會出現(xiàn)在控制臺上,就像這樣:

An unexpected error occurred.

你也可以使用Logback實現(xiàn)完全一樣的效果:

<configuration>
  <appender name="MyAppender" class="ch.qos.Logback.core.ConsoleAppender">
    <encoder>
      <pattern>%m%n</pattern>
    </encoder>
  </appender>
  <root level="error">
    <appender-ref ref="MyAppender" />
  </root>
</configuration>

FileAppenders

FileAppenders將日志記錄寫入到文件中,它負(fù)責(zé)打開、關(guān)閉文件,向文件中追加日志記錄,并對文件進(jìn)行加鎖,以免數(shù)據(jù)被破壞或者覆蓋。

在Log4j中,如果想創(chuàng)建一個FileAppender,需要指定目標(biāo)文件的名字,寫入方式是追加還是覆蓋,以及是否需要在寫入日志時對文件進(jìn)行加鎖:

...
<Appenders>
  <File name="MyFileAppender" fileName="myLog.log" append="true" locking="true">
    <PatternLayout pattern="%m%n"/>
  </File>
</Appenders>
...

這樣我們創(chuàng)建了一個名為MyFileAppender的FileAppender,并且在向文件中追加日志時會對文件進(jìn)行加鎖操作。

如果使用Logback,你可以同時啟用prudent模式來保證文件的完整性。雖然Prudent模式增加了寫入文件所花費的時間,但它可以保證在多個FileAppender甚至多個Java程序向同一個文件寫入日志時,文件的完整性。

...
<appender name="FileAppender" class="ch.qos.Logback.core.FileAppender">
  <file>myLog.log</file>
  <append>true</append>
  <prudent>true</prudent>
  <encoder>
    <pattern>%m%n</pattern>
  </encoder>
</appender>
...

SyslogAppender

SyslogAppenders將日志記錄發(fā)送給本地或者遠(yuǎn)程系統(tǒng)的日志服務(wù)。syslog是一個接收日志事件服務(wù),這些日志事件來自操作系統(tǒng)、進(jìn)程、其它服務(wù)或者其它設(shè)備。事件的范圍可以從診斷信息到用戶登錄硬件失敗等。syslog的事件按照設(shè)備進(jìn)行分類,它指定了正在記錄的事件的類型。例如,auth facility表明這個事件是和安全以及認(rèn)證有關(guān)。

Log4j和Logback都內(nèi)置支持SyslogAppenders。在Log4j中,我們創(chuàng)建SyslogAppender時,需要指定syslog服務(wù)監(jiān)聽的主機號、端口號以及協(xié)議。下面的示例演示了如何設(shè)定裝置:

...
<Appenders>
  <Syslog name="SyslogAppender" host="localhost" port="514" protocol="UDP" facility="Auth" />
</Appenders>
...

在Logback中,我們可以實現(xiàn)同樣的效果:

...
<appender name="SyslogAppender" class="ch.qos.Logback.classic.net.SyslogAppender">
  <syslogHost>localhost</syslogHost>
  <port>514</port>
  <facility>Auth</facility>
</appender>
...

其它Appender

我們已經(jīng)介紹了一些經(jīng)常用到的Appenders,還有很多其它Appender。它們添加了新功能或者在其它的 一些Appender基礎(chǔ)上實現(xiàn)了新功能。例如,Log4j中的RollingFileAppender擴展了FileAppender,它可以在滿足特 定條件時自動創(chuàng)建新的日志文件;SMTPAppender會將日志內(nèi)容以郵件的形式發(fā)送出去;FailoverAppender會在處理日志的過程中,如 果一個或者多個Appender失敗,自動切換到其他Appender上。

如果想了解更多關(guān)于其他Appender的信息,可以查看Log4j Appender參考以及Logback Appender參考。

Layouts

Layouts將日志記錄的內(nèi)容從一種數(shù)據(jù)形式轉(zhuǎn)換成另外一種。日志框架為純文本、HTML、syslog、XML、JSON、序列化以及其它日志提供了Layouts。

請注意:在java.util.logging中Layouts也被稱為Formatters。

例如,java.util.logging提供了兩種Layouts:SimpleFormatter和XMLFormatter。默認(rèn)情況下,ConsoleHandlers使用SimpleFormatter,它輸出的純文本日志記錄就像這樣:

Mar 31, 2015 10:47:51 AM MyClass main
SEVERE: An exception occurred.

而默認(rèn)情況下,F(xiàn)ileHandlers使用XMLFormatter,它的輸出就像這樣:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2015-03-31T10:47:51</date>
  <millis>1427903275893</millis>
  <sequence>0</sequence>
  <logger>MyClass</logger>
  <level>SEVERE</level>
  <class>MyClass</class>
  <method>main</method>
  <thread>1</thread>
  <message>An exception occurred.</message>
</record>
</log>

配置Layout

我們通常使用配置文件對Layouts進(jìn)行配置。從Java 7開始,我們也可以使用system property來配置SimpleFormatter。

例如,在Log4j和Logback中最常用的Layouts是PatternLayout。它可以讓你決定日志事件中的哪些部分需要輸出,這是通過轉(zhuǎn)換模式(Conversion Pattern)完成的,轉(zhuǎn)換模式在每一條日志事件的數(shù)據(jù)中扮演了“占位符”的角色。例如,Log4j默認(rèn)的PatternLayout使用了如下轉(zhuǎn)換模式:

<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>

%d{HH:mm:ss.SSS} 將日期轉(zhuǎn)換成時、分、秒和毫秒的形式,%level顯示日志事件的嚴(yán)重程度,%C顯示生成日志事件的類的名字,%t顯示Logger的當(dāng)前線程,%m顯示時間的消息,***,%n為下一個日志事件進(jìn)行了換行。

改變Layouts

如果在java.util.logging中使用一個不同的Layout,需要將Appender的formatter屬性設(shè)置成你想要的Layout。在 代碼中,你可以創(chuàng)建一個新的Handler,調(diào)用setFormatter方法,然后通過logger.AddHandler()方法將Handler放 到Logger上面。下面的示例創(chuàng)建了一個ConsoleAppender,它使用XMLFormatter來對日志進(jìn)行格式化,而不是使用默認(rèn)的 SimpleFormatter:

Handler ch = new ConsoleHandler();
ch.setFormatter(new XMLFormatter());
logger.addHandler(ch);

這樣Logger會將下面的信息輸出到控制臺上:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2015-03-31T10:47:51</date>
  <millis>1427813271000</millis>
  <sequence>0</sequence>
  <logger>MyClass</logger>
  <level>SEVERE</level>
  <class>MyClass</class>
  <method>main</method>
  <thread>1</thread>
  <message>An exception occurred.</message>
</record>

如果想了解更多信息,你可以查看Log4j Layouts參考以及Logback Layouts參考。

使用自定義Layouts

自定義Layouts可以讓你指定Appender應(yīng)該如何輸出日志記錄。從Java SE 7開始,盡管你可以調(diào)整SimpleLogger的輸出,但有一個限制,即只能夠調(diào)整簡單的純文本消息。對于更高級的格式,例如HTML或者JSON,你需要一個自定義Layout或者一個單獨的框架。

如果想了解更多使用java.util.logging創(chuàng)建自定義Layouts的信息,你可以查看Jakob Jenkov的Java日志指南中的Java Logging: Formatters章節(jié)。

日志級別

日志級別提供了一種方式,我們可以用它來根據(jù)嚴(yán)重程度對日志進(jìn)行分類和識別。java.util.logging 按照嚴(yán)重程度從重到輕,提供了以下級別:

  • SEVERE(***級別)

  • WARNING

  • INFO

  • CONFIG

  • FINE

  • FINER

  • FINEST(***級別)

另外, 還有兩個日志級別:ALL和OFF。ALL會讓Logger輸出所有消息,而OFF則會關(guān)閉日志功能。

設(shè)置日志級別

在設(shè)定日志級別后,Logger會自動忽略那些低于設(shè)定級別的日志消息。例如,下面的語句會讓Logger忽略那些低于WARNING級別的日志消息:

logger.setLevel(Level.WARNING);

然后,Logger會記錄任何WARNING或者更高級別的日志消息。我們也可以在配置文件中設(shè)置Logger的日志級別:

...
<Loggers>
  <Logger name="MyLogger" level="warning">
  ...

轉(zhuǎn)換模式

Log4j和Logback中的PatternLayout類都支持轉(zhuǎn)換模式,它決定了我們?nèi)绾螐拿恳粭l日志事件中提取信息以及如何對信息進(jìn)行格式 化。下面顯示了這些模式的一個子集,對于Log4j和Logback來說,雖然這些特定的字段都是一樣的,但是并不是所有的字段都會使用相同的模式。想要 了解更多信息,可以查看Log4j和Logback的PatternLayout文檔。

字段名稱

Log4j/Logback 模式

消息

%m

級別/嚴(yán)重程度

%p

異常

%ex

線程

%t

Logger

%c

方法

%M

例如,下面的PatternLayout會在中括號內(nèi)x顯示日志級別,后面是線程名字和日志事件的消息:

[%p] %t: %m

下面是使用了上述轉(zhuǎn)換模式后的日志輸出示例:

[INFO] main: initializing worker threads
[DEBUG] worker: listening on port 12222[INFO] worker: received request from 192.168.1.200[ERROR] worker: unknown request ID from 192.168.1.200

記錄棧跟蹤信息

如果你在Java程序中使用過異常,那么很有可能已經(jīng)看到過棧跟蹤信息。它提供了一個程序中方法調(diào)用的快照,讓你準(zhǔn)確定位程序執(zhí)行的位置。例如,下面的棧跟蹤信息是程序試圖打開一個不存在的文件后生成的:

[ERROR] main: Unable to open file! java.io.FileNotFoundException: foo.file (No such file or directory)
  at java.io.FileInputStream.open(Native Method) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:146) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:101) ~[?:1.7.0_79]
  at java.io.FileReader.<init>(FileReader.java:58) ~[?:1.7.0_79]
  at FooClass.main(FooClass.java:47)

這個示例使用了一個名為FooClass的類,它包含一個main方法。在程序第47行,F(xiàn)ileReader獨享試圖打開一個名為foo.file的文件,由于在程序目錄下沒有名字是foo.file的文件,因此Java虛擬機拋出了一個FileNotFoundException。因為這個方法調(diào)用被放到了try-catch語塊中,所以我們能夠捕獲這個異常并記錄它,或者至少可以阻止程序崩潰。

使用PatternLayout記錄棧跟蹤信息

在寫本篇文章時***版本的Log4j和Logback中,如果在Layout中沒有和可拋異常相關(guān)的信息,那么都會自動將%xEx(這種棧跟蹤信息包含了每次方法調(diào)用的包信息)添加到PatternLayout中。如果對于普通的日志信息的模式如下:

[%p] %t: %m

它會變?yōu)椋?/p>

[%p] %t: %m%xEx

這樣不僅僅錯誤信息會被記錄下來,完整的棧跟蹤信息也會被記錄:

[ERROR] main: Unable to open file! java.io.FileNotFoundException: foo.file (No such file or directory)
  at java.io.FileInputStream.open(Native Method) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:146) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:101) ~[?:1.7.0_79]
  at java.io.FileReader.<init>(FileReader.java:58) ~[?:1.7.0_79]
  at FooClass.main(FooClass.java:47)

%xEx中的包查詢是一個代價昂貴的操作,如果你頻繁的記錄異常信息,那么可能會碰到性能問題,例如:

// ...
  } catch (FileNotFoundException ex) {
    logger.error(“Unable to open file!”, ex);
}

一種解決方法是在模式中顯式的包含%ex,這樣就只會請求異常的棧跟蹤信息:

[%p] %t: %m%ex

另外一種方法是通過追加%xEx(none)的方法排除(在Log4j)中所有的異常信息:

[%p] %t: %m%xEx{none}

或者在Logback中使用%nopex:

[%p] %t: %m%nopex

使用結(jié)構(gòu)化布局輸出棧跟蹤信息

如你在“解析多行棧跟蹤信息”一節(jié)中所見,對于站跟蹤信息來說,使用結(jié)構(gòu)化布局來記錄是最合適的方式,例如JSON和XML。 這些布局會自動將棧跟蹤信息按照核心組件進(jìn)行分解,這樣我們可以很容易將其導(dǎo)出到其他程序或者日志服務(wù)中。對于上述站跟蹤信息,如果使用JSON格式,部分信息顯示如下:

...
"loggerName" : "FooClass",
  "message" : "Foo, oh no! ",
  "thrown" : {
    "commonElementCount" : 0,
    "localizedMessage" : "foo.file (No such file or directory)",
    "message" : "foo.file (No such file or directory)",
    "name" : "java.io.FileNotFoundException",
    "extendedStackTrace" : [ {
    "class" : "java.io.FileInputStream",
    "method" : "open",
    "file" : "FileInputStream.java",
    ...

記錄未捕獲異常

通常情況下,我們通過捕獲的方式來處理異常。如果一個異常沒有被捕獲,那么它可能會導(dǎo)致程序終止。如果能夠留存任何日志,那么這是一個可以幫助我們調(diào)試為什么會發(fā)生異常的好辦法,這樣你就可以找到發(fā)生異常的根本原因并解決它。下面來說明我們?nèi)绾谓⒁粋€默認(rèn)的異常處理器來記錄這些錯誤。

Thread類中有兩個方法,我們可以用它來為未捕獲的異常指定一個ExceptionHandler:

setDefaultUncaughtExceptionHandler 可以讓你在任何線程上處理任何異常。setUncaughtExceptionHandler可以讓你針對一個指定的線程設(shè)定一個不同的處理方法。而ThreadGroup則允許你設(shè)定一個處理方法。大部分人會使用默認(rèn)的異常處理方法。

下面是一個示例,它設(shè)定了一個默認(rèn)的異常處理方法,來創(chuàng)建一個日志事件。它要求你傳入一個UncaughtExceptionHandler:

import java.util.logging.*;
public class ExceptionDemo {
  private static final Logger logger = Logger.getLogger(ExceptionDemo.class);
  public static void main(String[] args) {
    Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
      public void uncaughtException(Thread t, Throwable e) {
        logger.log(Level.SEVERE, t + " ExceptionDemo threw an exception: ", e);
      };
  });
  class adminThread implements Runnable {
    public void run() {
      throw new RuntimeException();
    }
  }
  Thread t = new Thread(new adminThread());
  t.start();
  }
}

下面是一個未處理異常的輸出示例:

May 29, 2015 2:21:15 PM ExceptionDemo$1 uncaughtException
SEVERE: Thread[Thread-1,5,main] ExceptionDemo threw an exception:
java.lang.RuntimeException
  at ExceptionDemo$1adminThread.run(ExceptionDemo.java:15)
  at java.lang.Thread.run(Thread.java:745)

JSON

JSON(JavaScript Object Notation)是一種用來存儲結(jié)構(gòu)化數(shù)據(jù)的格式,它將數(shù)據(jù)存儲成鍵值對的集合,類似于HashMap或者Hashtable。JSON具有的可移植性和通用性,大部分現(xiàn)代語言都內(nèi)置支持它或者通過已經(jīng)準(zhǔn)備好的第三方類庫來支持它。

JSON支持許多基本數(shù)據(jù)類型,包括字符串、數(shù)字、布爾、數(shù)組和null。例如,你可以使用下面的JSON格式來表示一個電腦:

{
  "manufacturer": "Dell",
  "model": "Inspiron",
  "hardware": {
    "cpu": "Intel Core i7",
    "ram": 16384,
    “cdrom”: null
  },
  "peripherals": [
    {
      "type": "monitor",
      "manufacturer": "Acer",
      "model": "S231HL"
    }
  ]
}

JSON的可移植性使得它非常適合存儲日志記錄,使用JSON后,Java日志可以被任何數(shù)目的JSON解釋器所讀取。因為數(shù)據(jù)已經(jīng)是結(jié)構(gòu)化的,所以解析JSON日志要遠(yuǎn)比解析純文本日志容易。

Java中的JSON

對于Java來說,有大量的JSON實現(xiàn),其中一個是JSON.simple。JSON.simple是輕量級的、易于使用,并且全部符合JSON標(biāo)準(zhǔn)。

如果想將上面的computer對象轉(zhuǎn)換成可用的Java對象,我們可以從文件中讀取JSON內(nèi)容,將其傳遞給JSON.simple,然后返回一個Object,接著我們可以將Object轉(zhuǎn)換成JSONObject:

Object computer = JSONValue.parse(new FileReader("computer.json"));
JSONObject computerJSON = (JSONObject)computer;

另外,為了取得鍵值對的信息,你可以使用任何日志框架來記錄一個JSONObject,JSONObject對象包含一個toString()方法, 它可以將JSON轉(zhuǎn)換成文本:

2015-05-06 14:54:32,878 INFO  JSONTest main {"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}

雖然這樣做可以很容易的打印JSONObject,但如果你使用結(jié)構(gòu)化的Layouts,例如JSONLayout或者XMLLayout,可能會導(dǎo)致意想不到的結(jié)果:

...
"message" : "{"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}",
...

Log4j中的JSONLayout并沒有內(nèi)置支持內(nèi)嵌JSON對象,但你可以通過創(chuàng)建自定義Layout的方式來添加一個JSONObject字段,這個Layout會繼承或者替換JSONLayout。然 而,如果你使用一個日志管理系統(tǒng),需要記住許多日志管理系統(tǒng)會針對某些字段使用預(yù)定義的數(shù)據(jù)類型。如果你創(chuàng)建一個Layout并將JSONObject存 儲到message字段中,那么它可能會和日志系統(tǒng)中使用的String數(shù)據(jù)類型相沖突。一種解決辦法是將JSON數(shù)據(jù)存儲到一個字段中,然后將字符串類 型的日志消息存儲到另外一個字段中。

其它JSON庫

除了JSON.simple,Java中還有很多其它JSON庫。JSON-java是由JSON創(chuàng)建者開發(fā)的一個參考實現(xiàn),它包含了額外的一些功能,可以轉(zhuǎn)換其它數(shù)據(jù)類型,包括web元素。但是目前JSON-java已經(jīng)沒有人來維護(hù)和提供支持了。

如果想將JSON對象轉(zhuǎn)換成Java對象或者逆向轉(zhuǎn)換,Google提供了一個Gson庫。使 用Gson時,可以很簡單使用 toJson() 和 fromJson()  方法來解析JSON,這兩個方法分別用來將Java對象轉(zhuǎn)換成JSON字符串以及將JSON字符串轉(zhuǎn)換成Java對象。Gson甚至可以應(yīng)用在內(nèi)存對象 中,允許你映射到那些沒有源代碼的對象上。

Jackson

Jackson是一個強大的、流行的、功能豐富的庫,它可以在Java中管理JSON對象。有一些框架甚至使用Jackson作為它們的JSONLayouts。盡管它很大并且復(fù)雜,但Jackson對于初學(xué)者和高級用戶來說,是很容易使用的。

Logback通過logback-jackson和logback-json-classic庫繼承了Jackson,這兩個庫也是logback-contrib項目的一部分。在集成了Jackson后,你可以將日志以JSON的格式導(dǎo)出到任何Appender中。

Logback Wiki詳細(xì)解釋了如何將JSON添加到logback中,在Wiki頁面中的示例使用了LogglyAppender,這里的配置也可以應(yīng)用到其他Appender上。下面的示例說明了如何將JSON格式化的日志記錄寫入到名為myLog.json的文件中:

...
<appender name="file" class="ch.qos.Logback.core.FileAppender">
  <file>myLog.json</file>
  <encoder class="ch.qos.Logback.core.encoder.LayoutWrappingEncoder">
  <layout class="ch.qos.Logback.contrib.json.classic.JsonLayout">
    <jsonFormatter class="ch.qos.Logback.contrib.jackson.JacksonJsonFormatter"/>
    </layout>
  </encoder>
</appender>
...

你也可以通過FasterXML Wiki找到更多關(guān)于Jackson的深度介紹。

了解更多JSON相關(guān)信息

你可以通過JSON主頁學(xué)習(xí)更多JSON相關(guān)信息,或者通過CodeAcademy來通過學(xué)習(xí)一個交互式的快速上手教程(請注意這個課程是基于JavaScript的,而不是Java)。有一些在線工具例如JSONLint和JSON在線編輯器可以幫助你解析、驗證以及格式化JSON代碼。

NDC、MDC以及ThreadContext

當(dāng)處理多線程應(yīng)用程序,特別是web服務(wù)時,跟蹤事件可能會變得困難。當(dāng)針對多個同時存在的多個用戶生成日志記錄時,你如何區(qū)分哪個行為和哪個日志 事件有關(guān)呢?如何兩個用戶沒有成功打開一個相同的文件,或者在同一時間沒有成功登陸,那么怎么處理日志記錄?你可能需要一種方式來將日志記錄和程序中的唯 一標(biāo)示符關(guān)聯(lián)起來,這些標(biāo)識符可能是用戶ID,會話ID或者設(shè)備ID。而這就是NDC、MDC以及ThreadContext的用武之地。

NDC、MDC和ThreadContext通過向單獨的日志記錄中添加***的數(shù)據(jù)戳,來創(chuàng)建日志足跡(log  trails)。這些數(shù)據(jù)戳也被稱為魚標(biāo)記(fish  tagging),我們可以通過一個或者多個***的值來區(qū)分日志。這些數(shù)據(jù)戳在每個線程級別上進(jìn)行管理,并且一直持續(xù)到線程結(jié)束,或者直到數(shù)據(jù)戳被刪 掉。例如,如果你的Web應(yīng)用程序為每個用戶生成一個新的線程,那么你可以使用這個用戶的ID來標(biāo)記日志記錄。當(dāng)你想在一個復(fù)雜的系統(tǒng)中跟蹤特定的請求、 事務(wù)或者用戶,這是一種非常有用的方法。

嵌套診斷上下文(NDC)

NDC或者嵌套診斷上下文(Nested Diagnostic Context)是基于棧的思想,信息可以被放到棧上或者從棧中移除。而棧中的值可以被Logger訪問,并且Logger無需顯示想日志方法中傳入任何值。

下面的代碼示例使用NDC和Log4j來將用戶姓名和一條日志記錄關(guān)聯(lián)起來。NDC是一個靜態(tài)類,因此我們可以直接訪問它的方法,而無需實例化一個 NDC對象。在這個示例中, NDC.oush(username) 和 NDC.push(sessionID)  方法在棧中存儲了當(dāng)前的用戶名(admin)和會話ID(1234),而NDC.pop()方法將一些項從棧中移除,NDC.remove()方法讓 Java回收內(nèi)存,以免造成內(nèi)存溢出。

Java日志的核心組件是什么

import java.io.FileReader;
import org.apache.Log4j.Logger;
import org.apache.Log4j.NDC;
...
String username = "admin";
String sessionID = "1234";
NDC.push(username);
NDC.push(sessionID);
try {
  // tmpFile doesn't exist, causing an exception.
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file.");
}
finally {
  NDC.pop();
  NDC.pop();
  NDC.remove();
}

Log4j的PatternLayout類通過%x轉(zhuǎn)換字符從NDC中提取值。如果一個日志事件被觸發(fā),那么完整的NDC棧就被傳到Log4j:

<PatternLayout pattern="%x %-5p - %m%n" />

運行程序后,我們可以得出下面的輸出:

"admin 1234 ERROR &ndash; Unable to open file."

映射診斷上下文(MDC)

MDC或者映射診斷上下文和NDC很相似,不同之處在于MDC將值存儲在鍵值對中,而不是棧中。這樣你可以很容易的在Layout中引用一個單獨的 鍵。MDC.put(key,value) 方法將一個新的鍵值對添加到上下文中,而 MDC.remove(key) 方法會移除指定的鍵值對。

如果想在日志中同樣顯示用戶名和會話ID,我們需要使用 MDC.put() 方法將這兩個變量存儲成鍵值對:

import java.io.FileReader;
import org.apache.Log4j.Logger;
import org.apache.Log4j.MDC;
...
MDC.put("username", "admin");
MDC.put("sessionID", "1234");
try {
  // tmpFile doesn't exist, causing an exception.
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file!");
}
finally {
  MDC.clear();
}

這里再一次強調(diào),在不需要使用Context后,我們需要使用 MDC.clear() 方法將所有的鍵值對從MDC中移除,這樣會降低內(nèi)存的使用量,并阻止MDC在后面試圖調(diào)用那些已經(jīng)過期的數(shù)據(jù)。

在日志框架中訪問MDC的值時,也稍微有些區(qū)別。對于存儲在上下文中的任何鍵,我們可以使用%X(鍵)的方式來訪問對應(yīng)的值。這樣,我們可以使用 %X(username) 和 %X(sessionID) 來獲取對應(yīng)的用戶名和會話ID:

<PatternLayout pattern="%X{username} %X{sessionID} %-5p - %m%n" />
"admin 1234 ERROR &ndash; Unable to open file!"

如果我們沒有指定任何鍵,那么MDC上下文就會被以 {(key, value),(key, value)} 的方式傳遞給Appender。

Logback中的NDC和MDC

和Log4j不同,Logback內(nèi)置沒有實現(xiàn)NDC。但是slf4j-ext包提供了一個NDC實現(xiàn),它使用MDC作為基礎(chǔ)。在Logback內(nèi)部,你可以使用 MDC.put()、MDC.remove() 和 MDC.clear() 方法來訪問和管理MDC:

import org.slf4j.MDC;
...
Logger logger = LoggerFactory.getLogger(MDCLogback.class);
...
MDC.put("username", "admin");
MDC.put("sessionID", "1234");
try {
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file.");
}
finally {
  MDC.clear();
}

在Logback中,你可以在Logback.xml中將如下模式應(yīng)用到Appender上,它可以輸出和上面Log4j相同的結(jié)果:

<Pattern>[%X{username}] %X{sessionID} %-5p - %m%n</Pattern>
"[admin] 1234 ERROR - Unable to open file."

針對MDC的訪問并不僅僅限制在PatternLayout上,例如,當(dāng)使用JSONFormatter時,MDC中的所有值都會被導(dǎo)出:

{
"timestamp":"1431970324945",
"level":"ERROR",
"thread":"main",
"mdc":{
"username":"admin",
"sessionID":"1234"
},
"logger":"MyClass",
"message":"Unable to open file.",
"context":"default"
}

ThreadContext

Version 2 of Log4j merged MDC and NDC into a single concept known as  the Thread Context. The Thread Context is an evolution of MDC and NDC,  presenting them respectively as the Thread Context Map and Thread  Context Stack. The Thread Context is managed through the static  ThreadContext class, which is implemented similar to Log4j 1&rsquo;s MDC and  NDC classes.

Log4j版本2中將MDC和NDC合并到一個單獨的組件中,這個組件被稱為線程上下文。線程上下文是針對MDC和NDC的進(jìn)化,它分別用線程上下文Map映射線程上下文棧來表示MDC和NDC。我們可以通過ThreadContext靜態(tài)類來管理線程上下文,這個類在實現(xiàn)上類似于Log4j版本1中的MDC和NDC。

When using the Thread Context Stack, data is pushed to and popped from a stack just like with NDC:

當(dāng)使用線程上下文棧時,我們可以向NDC那樣向棧中添加或者刪除數(shù)據(jù):

import org.apache.logging.Log4j.ThreadContext;
...
ThreadContext.push(username);
ThreadContext.push(sessionID);
// Logging methods go here
ThreadContext.pop();
...

當(dāng)使用線程上下文映射時,我們可以像MDC那樣將值和鍵結(jié)合在一起:

import org.apache.logging.Log4j.ThreadContext;
...
ThreadContext.put(“username”,"admin");
ThreadContext.put("sessionID", "1234");
// Logging methods go here
ThreadContext.clearMap();
...

ThreadContext類提供了一些方法,用于清除棧、清除MDC、清除存儲在上下文中的所有值,對應(yīng)的方法是ThreadContext.clearAll()、ThreadContext.clearMap()和ThreadContext.clearStack()。

和在MDC以及NDC中一樣,我們可以使用Layouts在線程上下文中訪問這些值。使用PatternLayout時,%x轉(zhuǎn)換模式會從棧中獲取值,%X和%X(鍵)會從圖中獲取值。

ThreadContext過濾

一些框架允許你基于某些屬性對日志進(jìn)行過濾。例如,Log4j的DynamicThresholdFilter 會在鍵滿足特定條件的情況下,自動調(diào)整日志級別。再比如,如果我們想要觸發(fā)TRACE級別的日志消息,我們可以創(chuàng)建一個名為trace-logging-enabled的鍵,并向log4j配置文件中添加一個過濾器:

<Configuration name="MyApp">
<DynamicThresholdFilter key="trace-logging-enabled" onMatch="ACCEPT" onMismatch="NEUTRAL">
<KeyValuePair key="true" value="TRACE" />
</DynamicThresholdFilter>
...

如果ThreadContext包含一個名為trace-logging-enabled的鍵,onMatch 和 onMismatch  會決定如何處理它。關(guān)于 onMatch 和  onMismatch,我們有三個可選項:ACCEPT,它會處理過濾器的規(guī)則;DENY,它會忽略過濾器的規(guī)則;NEUTRAL,它會推遲到下一個過濾 器。除了這些,我們還定義一個鍵值對,當(dāng)值為true時,我們啟用TRACE級別的日志。

現(xiàn)在,當(dāng)trace-logging-enabled被設(shè)置成true時,即使根Logger設(shè)置的日志級別高于TRACE,Appender也會記錄TRACE級別的消息。

你可能還想過濾一些特定的日志到特定的Appender中,Log4j中提供了ThreadContextMapFilter來實現(xiàn)這一點。如果我們想要限制某個特定的Appender,只記錄針對某個用戶的TRACE級別的消息,我們可以基于username鍵添加一個ThreadContextMapFilter:

<Console name="ConsoleAppender" target="SYSTEM_OUT">
<ThreadContextMapFilter onMatch="ACCEPT" onMismatch="DENY">
<KeyValuePair key="username" value="admin" />
</ThreadContextMapFilter>
...

如果想了解更多信息,你可以查看Log4j和Logback文檔中關(guān)于DynamicThresholdFilter部分。

Markers

Markers允許你對單獨的日志記錄添加一些***的數(shù)據(jù)。它可以用來對日志記錄進(jìn)行分組,觸發(fā)一些行為,或者對日志記錄進(jìn)行過濾,并將過濾結(jié)果輸出到指定的Appender中。你甚至可以將Markers和ThreadContext結(jié)合在一起使用,以提高搜索和過濾日志數(shù)據(jù)的能力。

例如,假設(shè)我們有一個可以連接到數(shù)據(jù)庫的類,如果在打開數(shù)據(jù)庫的時候發(fā)生了異常,我們需要把異常記錄成fatal錯誤。我們可以創(chuàng)建一個名為DB_ERROR的Marker,然后將其應(yīng)用到日志事件中:

import org.apache.logging.Log4j.Marker;
import org.apache.logging.Log4j.MarkerManager;
...
final static Marker DB_ERROR = MarkerManager.getMarker("DATABASE_ERROR");
...
logger.fatal(DB_ERROR, "An exception occurred.");

為了在日志輸出中顯示Marker信息,我們需要在PatternLayout中添加%marker轉(zhuǎn)換模式:

<PatternLayout pattern="%p %marker: %m%n" />
[FATAL] DATABASE_ERROR: An exception occurred.

或者對于JSON和XML格式的Layouts,會自動在輸出中包含Marker信息:

...
"thread" : "main",
"level" : "FATAL",
"loggerName" : "DBClass",
"marker" : {
  "name" : "DATABASE_ERROR"
},
"message" : "An exception occurred.",
...

通過對Marker數(shù)據(jù)進(jìn)行自動解析和排序,集中式的日志服務(wù)可以很容易對日志進(jìn)行搜索處理。

Markers過濾

Marker過濾器可以讓你決定哪些Marker由哪些Logger來處理。marker字段會比較在日志事件里面的Marker名字,如果名字匹配,那么Logger會執(zhí)行后續(xù)的行為。例如,在Log4j中,我們可以配置一個Appender來只顯示哪些使用了DB_ERROR Marker的消息,這可以通過log4j2.xml中的Appender添加如下信息來實現(xiàn):

<MarkerFilter marker="DATABASE_ERROR" onMatch="ACCEPT" onMismatch="DENY" />

如果日志記錄中某一條的Marker可以匹配這里的marker字段,那么onMatch會決定如何處理這條記錄。如果不能夠匹配,或者日志記錄中 沒有Marker信息,那么onMismatch就會決定如何處理這條記錄。對于onMatch和onMismatch來說,有3個可選 項:ACCEPT,它允許記錄事件;DENY,它會阻塞事件;NEUTRAL,它不會對事件進(jìn)行任何處理。

在Logback中,我們需要更多一些設(shè)置。首先,想Appender中添加一個新的EvaluatorFilter,并如上所述指定onMatch和onMismatch行為。然后,添加一個OnMarkerEvaluator并將Marker的名字傳遞給它:

<filter class="ch.qos.Logback.core.filter.EvaluatorFilter">
  <evaluator class="ch.qos.Logback.classic.boolex.OnMarkerEvaluator">
    <marker>DATABASE_ERROR</marker>
  </evaluator>
  <onMatch>ACCEPT</onMatch>
  <onMismatch>DENY</onMismatch>
</filter>

將Markers和NDC、MDC以及ThreadContext結(jié)合使用

Marker的功能和ThreadContext類似,它們都是向日志記錄中添加***的數(shù)據(jù),這些數(shù)據(jù)可以被Appender訪問。如果把這兩者結(jié)合使用,可以讓你更容易的對日志數(shù)據(jù)進(jìn)行索引和搜索。如果能夠知道何時使用哪一種技術(shù),會對我們有所幫助。

NDC、MDC和ThreadContext被用于將相關(guān)日志記錄結(jié) 合在一起。如果你的應(yīng)用程序會處理多個同時存在的用戶,ThreadContext可以讓你將針對某個特定用戶的一組日志記錄組合在一起。因為 ThreadContext針對每個線程都是不一樣的,所以你可以使用同樣的方法來對相關(guān)的日志記錄進(jìn)行自動分組。

另一方面,Marker通常用于標(biāo)記或者高亮顯示某些特殊事件。在上述示例中,我們使用DB_ERROR  Marker來標(biāo)明在方法中發(fā)生的SQL相關(guān)異常。我們可以使用DB_ERROR  Marker來將這些事件的處理過程和其他事件區(qū)分開來,例如我們可以使用SMTP Appender來將這些事件通過郵件發(fā)送給數(shù)據(jù)庫管理員。

“Java日志的核心組件是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI