您好,登錄后才能下訂單哦!
本篇內容介紹了“Java日志的核心組件是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
Java日志基礎
Java使用了一種自定義的、可擴展的方法來輸出日志。雖然Java通過java.util.logging包提供了一套基本的日志處理API,但你可以很輕松的使用一種或者多種其它日志解決方案。這些解決方案盡管使用不同的方法來創建日志數據,但它們的最終目標是一樣的,即將日志從你的應用程序輸出到目標地址。
在這一節中,我們會探索Java日志背后的原理,并說明如何通過日志來讓你成為一個更好的Java開發人員。
Java日志組件
Java日志API由以下三個核心組件組成:
Loggers:Logger負責捕捉事件并將其發送給合適的Appender。
Appenders:也被稱為Handlers,負責將日志事件記錄到目標位置。在將日志事件輸出之前,Appenders使用Layouts來對事件進行格式化處理。
Layouts:也被稱為Formatters,它負責對日志事件中的數據進行轉換和格式化。Layouts決定了數據在一條日志記錄中的最終形式。
當Logger記錄一個事件時,它將事件轉發給適當的Appender。然后Appender使用Layout來對日志記錄進行格式化,并將其發送給控制臺、文件或者其它目標位置。另外,Filters可以讓你進一步指定一個Appender是否可以應用在一條特定的日志記錄上。在日志配置中,Filters并不是必需的,但可以讓你更靈活地控制日志消息的流動。
日志框架
在Java中,輸出日志需要使用一個或者多個日志框架,這些框架提供了必要的對象、方法和配置來傳輸消息。Java在java.util.logging包中提供了一個默認的框架。除此之外,還有很多其它第三方框架,包括Log4j、Logback以及tinylog。還有其它一些開發包,例如SLF4J和Apache Commons Logging,它們提供了一些抽象層,對你的代碼和日志框架進行解耦,從而允許你在不同的日志框架中進行切換。
如何選擇一個日志解決方案,這取決于你的日志需求的復雜度、和其它日志解決方案的兼容性、易用性以及個人喜好。Logback基于Log4j之前的版本開發(版本1),因此它們的功能集合都非常類似。然而,Log4j在***版本(版本2)中引用了一些改進,例如支持多API,并提升了在用Disruptor庫的性能。而tinylog,由于缺少了一些功能,運行特別快,非常適合小項目。
另外一個考慮因素是框架在基于Java的各種不同項目上的支持程度。例如Android程序只能使用Log4j、Logback或者第三方包來記錄日志, Apache Tomcat可以使用Log4j來記錄內部消息,但只能使用版本1的Log4j。
抽象層
諸如SLF4J這樣的抽象層,會將你的應用程序從日志框架中解耦。應 用程序可以在運行時選擇綁定到一個特定的日志框架(例如java.util.logging、Log4j或者Logback),這通過在應用程序的類路徑 中添加對應的日志框架來實現。如果在類路徑中配置的日志框架不可用,抽象層就會立刻取消調用日志的相應邏輯。抽象層可以讓我們更加容易地改變項目現有的日志框架,或者集成那些使用了不同日志框架的項目。
配置
盡管所有的Java日志框架都可以通過代碼進行配置,但是大部分配置還是通過外部配置文件完成的。這些文件決定了日志消息在何時通過什么方式進行處理,日志框架可以在運行時加載這些文件。在這一節中提供的大部分配置示例都使用了配置文件。
java.util.logging
默認的Java日志框架將其配置存儲到一個名為 logging.properties 的文件中。在這個文件中,每行是一個配置項,配置項使用點標記(dot notation)的形 式。Java在其安裝目錄的lib文件夾下面安裝了一個全局配置文件,但在啟動一個Java程序時,你可以通過指定 java.util.logging.config.file 屬性的方式來使用一個單獨的日志配置文件,同樣也可以在個人項目中創建和存儲 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 的文件。默認情況下,Log4j配置會將所有日志消息輸出到控制臺上。Log4j同樣也支持XML格式的配置文件,對應的配置信息會存儲到 log4j.xml 文件中。
Log4j版本2支持XML、JSON和YAML格式的配置,這些配置會分別存儲到 log4j2.xml、log4j2.json 和 log4j2.yaml 文件中。和版本1類似,版本2也會在工程目錄中尋找這些文件。你可以在每個版本的文檔中找到相應的配置文件示例。
Logback
對于Logback來說,大部分配置都是在 logback.xml 文件中完成的,這個文件使用了和Log4j類似的XML語法。Logback同時也支持通過Groovy語言的方式來進行配置,配置信息會存儲到 logback.groovy 文件中。你可以通過每種類型配置文件的鏈接找到對應的配置文件示例。
Loggers
Loggers是用來觸發日志事件的對象,在我們的Java應用程序中被創建和調用,然后Loggers才會將事件傳遞給Appender。一個類 中可以包含針對不同事件的多個獨立的Loggers,你也可以在一個Loggers里面內嵌一個Loggers,從而創建一種Loggers層次結構。
創建新Logger
在不同的日志框架下面創建新Logger過程大同小異,盡管調用的具體方法名稱可能不同。在使用 java.util.logging 時,你可以通過 Logger.getLogger().getLogger() 方法創建新Logger,這個方法接收一個string參數,用于指定Logger的名字。如果指定名字的Logger已經存在,那么只需要返回已經存在 的Logger;否則,程序會創建一個新Logger。通常情況下,一種好的做法是,我們在當前類下使用 class.getName() 作為新Logger的名字。
Logger logger = Logger.getLogger(MyClass.class.getName());
記錄日志事件
Logger提供了幾種方法來觸發日志事件。然而,在你記錄一個事件之前,你還需要設置級別。日志級別用來確定日志的嚴重程度,它可以用來過濾日志事件或者將其發送給不同的Appender(想了解更多信息,請參考“日志級別”一節),Logger.log() 方法除了日志消息以外,還需要一個日志級別作為參數:
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() 方法可以讓你記錄方法調用信息,從而追蹤程序的執行過程。
Appenders
Appenders將日志消息轉發給期望的輸出。它負責接收日志事件,使用Layout格式化事件,然后將其發送給對應的目標。對于一個日志事件,我們可以使用多個Appenders來將事件發送到不同的目標位置。例如,我們可以在控制臺上顯示一個簡單的日志事件的同時,將其通過郵件的方式發送給指定的接收者。
請注意,在java.util.logging中,Appenders被稱作Handlers。
增加Appender
大部分日志框架的Appender都會執行類似的功能,但在實現方面大相徑庭。如果使用 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來將日志消息發送到System.out:
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%p] %t: %m%n" />
</Console>
Appenders類型
這一節描述了一些更通用的Appenders,以及它們在各種日志框架中是如何實現的。
ConsoleAppender
ConsoleAppender是最常用的Appenders之一,它只是將日志消息顯示到控制臺上。許多日志框架都將其作為默認的Appender,并且在基本的配置中進行預配置。例如,在Log4j中ConsoleAppender的配置參數如下所示。
參數 | 描述 |
---|---|
filter | 用于決定是否需要使用該Appender來處理日志事件 |
layout | 用于決定如何對日志記錄進行格式化,默認情況下使用“%m%n”,它會在每一行顯示一條日志記錄 |
follow | 用于決定Appender是否需要了解輸出(system.out或者system.err)的變化,默認情況是不需要跟蹤這種變化 |
name | 用于設置Appender的名字 |
ignoreExceptions | 用于決定是否需要記錄在日志事件處理過程中出現的異常 |
target | 用于指定輸出目標位置,默認情況下使用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>
這個配置文件創建了一個名為MyAppender的ConsoleAppender,它使用PatternLayout來對日志事件進行格式化,然 后再將其輸出到System.out。<Loggers>元素對定義在程序代碼中的Loggers進行了配置。在這里,我們只配置了一個 LoggerConfig,即名為Root的Logger,它會接收哪些日志級別在ERROR以上的日志消息。如果我們使用logger.error() 來記錄一個消息,那么它就會出現在控制臺上,就像這樣:
An unexpected error occurred.
你也可以使用Logback實現完全一樣的效果:
<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將日志記錄寫入到文件中,它負責打開、關閉文件,向文件中追加日志記錄,并對文件進行加鎖,以免數據被破壞或者覆蓋。
在Log4j中,如果想創建一個FileAppender,需要指定目標文件的名字,寫入方式是追加還是覆蓋,以及是否需要在寫入日志時對文件進行加鎖:
...
<Appenders>
<File name="MyFileAppender" fileName="myLog.log" append="true" locking="true">
<PatternLayout pattern="%m%n"/>
</File>
</Appenders>
...
這樣我們創建了一個名為MyFileAppender的FileAppender,并且在向文件中追加日志時會對文件進行加鎖操作。
如果使用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將日志記錄發送給本地或者遠程系統的日志服務。syslog是一個接收日志事件服務,這些日志事件來自操作系統、進程、其它服務或者其它設備。事件的范圍可以從診斷信息到用戶登錄硬件失敗等。syslog的事件按照設備進行分類,它指定了正在記錄的事件的類型。例如,auth facility表明這個事件是和安全以及認證有關。
Log4j和Logback都內置支持SyslogAppenders。在Log4j中,我們創建SyslogAppender時,需要指定syslog服務監聽的主機號、端口號以及協議。下面的示例演示了如何設定裝置:
...
<Appenders>
<Syslog name="SyslogAppender" host="localhost" port="514" protocol="UDP" facility="Auth" />
</Appenders>
...
在Logback中,我們可以實現同樣的效果:
...
<appender name="SyslogAppender" class="ch.qos.Logback.classic.net.SyslogAppender">
<syslogHost>localhost</syslogHost>
<port>514</port>
<facility>Auth</facility>
</appender>
...
其它Appender
我們已經介紹了一些經常用到的Appenders,還有很多其它Appender。它們添加了新功能或者在其它的 一些Appender基礎上實現了新功能。例如,Log4j中的RollingFileAppender擴展了FileAppender,它可以在滿足特 定條件時自動創建新的日志文件;SMTPAppender會將日志內容以郵件的形式發送出去;FailoverAppender會在處理日志的過程中,如 果一個或者多個Appender失敗,自動切換到其他Appender上。
如果想了解更多關于其他Appender的信息,可以查看Log4j Appender參考以及Logback Appender參考。
Layouts
Layouts將日志記錄的內容從一種數據形式轉換成另外一種。日志框架為純文本、HTML、syslog、XML、JSON、序列化以及其它日志提供了Layouts。
請注意:在java.util.logging中Layouts也被稱為Formatters。
例如,java.util.logging提供了兩種Layouts:SimpleFormatter和XMLFormatter。默認情況下,ConsoleHandlers使用SimpleFormatter,它輸出的純文本日志記錄就像這樣:
Mar 31, 2015 10:47:51 AM MyClass main
SEVERE: An exception occurred.
而默認情況下,FileHandlers使用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進行配置。從Java 7開始,我們也可以使用system property來配置SimpleFormatter。
例如,在Log4j和Logback中最常用的Layouts是PatternLayout。它可以讓你決定日志事件中的哪些部分需要輸出,這是通過轉換模式(Conversion Pattern)完成的,轉換模式在每一條日志事件的數據中扮演了“占位符”的角色。例如,Log4j默認的PatternLayout使用了如下轉換模式:
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
%d{HH:mm:ss.SSS} 將日期轉換成時、分、秒和毫秒的形式,%level顯示日志事件的嚴重程度,%C顯示生成日志事件的類的名字,%t顯示Logger的當前線程,%m顯示時間的消息,***,%n為下一個日志事件進行了換行。
改變Layouts
如果在java.util.logging中使用一個不同的Layout,需要將Appender的formatter屬性設置成你想要的Layout。在 代碼中,你可以創建一個新的Handler,調用setFormatter方法,然后通過logger.AddHandler()方法將Handler放 到Logger上面。下面的示例創建了一個ConsoleAppender,它使用XMLFormatter來對日志進行格式化,而不是使用默認的 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應該如何輸出日志記錄。從Java SE 7開始,盡管你可以調整SimpleLogger的輸出,但有一個限制,即只能夠調整簡單的純文本消息。對于更高級的格式,例如HTML或者JSON,你需要一個自定義Layout或者一個單獨的框架。
如果想了解更多使用java.util.logging創建自定義Layouts的信息,你可以查看Jakob Jenkov的Java日志指南中的Java Logging: Formatters章節。
日志級別
日志級別提供了一種方式,我們可以用它來根據嚴重程度對日志進行分類和識別。java.util.logging 按照嚴重程度從重到輕,提供了以下級別:
SEVERE(***級別)
WARNING
INFO
CONFIG
FINE
FINER
FINEST(***級別)
另外, 還有兩個日志級別:ALL和OFF。ALL會讓Logger輸出所有消息,而OFF則會關閉日志功能。
設置日志級別
在設定日志級別后,Logger會自動忽略那些低于設定級別的日志消息。例如,下面的語句會讓Logger忽略那些低于WARNING級別的日志消息:
logger.setLevel(Level.WARNING);
然后,Logger會記錄任何WARNING或者更高級別的日志消息。我們也可以在配置文件中設置Logger的日志級別:
...
<Loggers>
<Logger name="MyLogger" level="warning">
...
轉換模式
Log4j和Logback中的PatternLayout類都支持轉換模式,它決定了我們如何從每一條日志事件中提取信息以及如何對信息進行格式 化。下面顯示了這些模式的一個子集,對于Log4j和Logback來說,雖然這些特定的字段都是一樣的,但是并不是所有的字段都會使用相同的模式。想要 了解更多信息,可以查看Log4j和Logback的PatternLayout文檔。
字段名稱 | Log4j/Logback 模式 |
---|---|
消息 | %m |
級別/嚴重程度 | %p |
異常 | %ex |
線程 | %t |
Logger | %c |
方法 | %M |
例如,下面的PatternLayout會在中括號內x顯示日志級別,后面是線程名字和日志事件的消息:
[%p] %t: %m
下面是使用了上述轉換模式后的日志輸出示例:
[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程序中使用過異常,那么很有可能已經看到過棧跟蹤信息。它提供了一個程序中方法調用的快照,讓你準確定位程序執行的位置。例如,下面的棧跟蹤信息是程序試圖打開一個不存在的文件后生成的:
[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行,FileReader獨享試圖打開一個名為foo.file的文件,由于在程序目錄下沒有名字是foo.file的文件,因此Java虛擬機拋出了一個FileNotFoundException。因為這個方法調用被放到了try-catch語塊中,所以我們能夠捕獲這個異常并記錄它,或者至少可以阻止程序崩潰。
使用PatternLayout記錄棧跟蹤信息
在寫本篇文章時***版本的Log4j和Logback中,如果在Layout中沒有和可拋異常相關的信息,那么都會自動將%xEx(這種棧跟蹤信息包含了每次方法調用的包信息)添加到PatternLayout中。如果對于普通的日志信息的模式如下:
[%p] %t: %m
它會變為:
[%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
使用結構化布局輸出棧跟蹤信息
如你在“解析多行棧跟蹤信息”一節中所見,對于站跟蹤信息來說,使用結構化布局來記錄是最合適的方式,例如JSON和XML。 這些布局會自動將棧跟蹤信息按照核心組件進行分解,這樣我們可以很容易將其導出到其他程序或者日志服務中。對于上述站跟蹤信息,如果使用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",
...
記錄未捕獲異常
通常情況下,我們通過捕獲的方式來處理異常。如果一個異常沒有被捕獲,那么它可能會導致程序終止。如果能夠留存任何日志,那么這是一個可以幫助我們調試為什么會發生異常的好辦法,這樣你就可以找到發生異常的根本原因并解決它。下面來說明我們如何建立一個默認的異常處理器來記錄這些錯誤。
Thread類中有兩個方法,我們可以用它來為未捕獲的異常指定一個ExceptionHandler:
setDefaultUncaughtExceptionHandler 可以讓你在任何線程上處理任何異常。setUncaughtExceptionHandler可以讓你針對一個指定的線程設定一個不同的處理方法。而ThreadGroup則允許你設定一個處理方法。大部分人會使用默認的異常處理方法。
下面是一個示例,它設定了一個默認的異常處理方法,來創建一個日志事件。它要求你傳入一個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)是一種用來存儲結構化數據的格式,它將數據存儲成鍵值對的集合,類似于HashMap或者Hashtable。JSON具有的可移植性和通用性,大部分現代語言都內置支持它或者通過已經準備好的第三方類庫來支持它。
JSON支持許多基本數據類型,包括字符串、數字、布爾、數組和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日志可以被任何數目的JSON解釋器所讀取。因為數據已經是結構化的,所以解析JSON日志要遠比解析純文本日志容易。
Java中的JSON
對于Java來說,有大量的JSON實現,其中一個是JSON.simple。JSON.simple是輕量級的、易于使用,并且全部符合JSON標準。
如果想將上面的computer對象轉換成可用的Java對象,我們可以從文件中讀取JSON內容,將其傳遞給JSON.simple,然后返回一個Object,接著我們可以將Object轉換成JSONObject:
Object computer = JSONValue.parse(new FileReader("computer.json"));
JSONObject computerJSON = (JSONObject)computer;
另外,為了取得鍵值對的信息,你可以使用任何日志框架來記錄一個JSONObject,JSONObject對象包含一個toString()方法, 它可以將JSON轉換成文本:
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,但如果你使用結構化的Layouts,例如JSONLayout或者XMLLayout,可能會導致意想不到的結果:
...
"message" : "{"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}",
...
Log4j中的JSONLayout并沒有內置支持內嵌JSON對象,但你可以通過創建自定義Layout的方式來添加一個JSONObject字段,這個Layout會繼承或者替換JSONLayout。然 而,如果你使用一個日志管理系統,需要記住許多日志管理系統會針對某些字段使用預定義的數據類型。如果你創建一個Layout并將JSONObject存 儲到message字段中,那么它可能會和日志系統中使用的String數據類型相沖突。一種解決辦法是將JSON數據存儲到一個字段中,然后將字符串類 型的日志消息存儲到另外一個字段中。
其它JSON庫
除了JSON.simple,Java中還有很多其它JSON庫。JSON-java是由JSON創建者開發的一個參考實現,它包含了額外的一些功能,可以轉換其它數據類型,包括web元素。但是目前JSON-java已經沒有人來維護和提供支持了。
如果想將JSON對象轉換成Java對象或者逆向轉換,Google提供了一個Gson庫。使 用Gson時,可以很簡單使用 toJson() 和 fromJson() 方法來解析JSON,這兩個方法分別用來將Java對象轉換成JSON字符串以及將JSON字符串轉換成Java對象。Gson甚至可以應用在內存對象 中,允許你映射到那些沒有源代碼的對象上。
Jackson
Jackson是一個強大的、流行的、功能豐富的庫,它可以在Java中管理JSON對象。有一些框架甚至使用Jackson作為它們的JSONLayouts。盡管它很大并且復雜,但Jackson對于初學者和高級用戶來說,是很容易使用的。
Logback通過logback-jackson和logback-json-classic庫繼承了Jackson,這兩個庫也是logback-contrib項目的一部分。在集成了Jackson后,你可以將日志以JSON的格式導出到任何Appender中。
Logback Wiki詳細解釋了如何將JSON添加到logback中,在Wiki頁面中的示例使用了LogglyAppender,這里的配置也可以應用到其他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找到更多關于Jackson的深度介紹。
了解更多JSON相關信息
你可以通過JSON主頁學習更多JSON相關信息,或者通過CodeAcademy來通過學習一個交互式的快速上手教程(請注意這個課程是基于JavaScript的,而不是Java)。有一些在線工具例如JSONLint和JSON在線編輯器可以幫助你解析、驗證以及格式化JSON代碼。
NDC、MDC以及ThreadContext
當處理多線程應用程序,特別是web服務時,跟蹤事件可能會變得困難。當針對多個同時存在的多個用戶生成日志記錄時,你如何區分哪個行為和哪個日志 事件有關呢?如何兩個用戶沒有成功打開一個相同的文件,或者在同一時間沒有成功登陸,那么怎么處理日志記錄?你可能需要一種方式來將日志記錄和程序中的唯 一標示符關聯起來,這些標識符可能是用戶ID,會話ID或者設備ID。而這就是NDC、MDC以及ThreadContext的用武之地。
NDC、MDC和ThreadContext通過向單獨的日志記錄中添加***的數據戳,來創建日志足跡(log trails)。這些數據戳也被稱為魚標記(fish tagging),我們可以通過一個或者多個***的值來區分日志。這些數據戳在每個線程級別上進行管理,并且一直持續到線程結束,或者直到數據戳被刪 掉。例如,如果你的Web應用程序為每個用戶生成一個新的線程,那么你可以使用這個用戶的ID來標記日志記錄。當你想在一個復雜的系統中跟蹤特定的請求、 事務或者用戶,這是一種非常有用的方法。
嵌套診斷上下文(NDC)
NDC或者嵌套診斷上下文(Nested Diagnostic Context)是基于棧的思想,信息可以被放到棧上或者從棧中移除。而棧中的值可以被Logger訪問,并且Logger無需顯示想日志方法中傳入任何值。
下面的代碼示例使用NDC和Log4j來將用戶姓名和一條日志記錄關聯起來。NDC是一個靜態類,因此我們可以直接訪問它的方法,而無需實例化一個 NDC對象。在這個示例中, NDC.oush(username) 和 NDC.push(sessionID) 方法在棧中存儲了當前的用戶名(admin)和會話ID(1234),而NDC.pop()方法將一些項從棧中移除,NDC.remove()方法讓 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轉換字符從NDC中提取值。如果一個日志事件被觸發,那么完整的NDC棧就被傳到Log4j:
<PatternLayout pattern="%x %-5p - %m%n" />
運行程序后,我們可以得出下面的輸出:
"admin 1234 ERROR – 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();
}
這里再一次強調,在不需要使用Context后,我們需要使用 MDC.clear() 方法將所有的鍵值對從MDC中移除,這樣會降低內存的使用量,并阻止MDC在后面試圖調用那些已經過期的數據。
在日志框架中訪問MDC的值時,也稍微有些區別。對于存儲在上下文中的任何鍵,我們可以使用%X(鍵)的方式來訪問對應的值。這樣,我們可以使用 %X(username) 和 %X(sessionID) 來獲取對應的用戶名和會話ID:
<PatternLayout pattern="%X{username} %X{sessionID} %-5p - %m%n" />
"admin 1234 ERROR – Unable to open file!"
如果我們沒有指定任何鍵,那么MDC上下文就會被以 {(key, value),(key, value)} 的方式傳遞給Appender。
Logback中的NDC和MDC
和Log4j不同,Logback內置沒有實現NDC。但是slf4j-ext包提供了一個NDC實現,它使用MDC作為基礎。在Logback內部,你可以使用 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中將如下模式應用到Appender上,它可以輸出和上面Log4j相同的結果:
<Pattern>[%X{username}] %X{sessionID} %-5p - %m%n</Pattern>
"[admin] 1234 ERROR - Unable to open file."
針對MDC的訪問并不僅僅限制在PatternLayout上,例如,當使用JSONFormatter時,MDC中的所有值都會被導出:
{
"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’s MDC and NDC classes.
Log4j版本2中將MDC和NDC合并到一個單獨的組件中,這個組件被稱為線程上下文。線程上下文是針對MDC和NDC的進化,它分別用線程上下文Map映射線程上下文棧來表示MDC和NDC。我們可以通過ThreadContext靜態類來管理線程上下文,這個類在實現上類似于Log4j版本1中的MDC和NDC。
When using the Thread Context Stack, data is pushed to and popped from a stack just like with NDC:
當使用線程上下文棧時,我們可以向NDC那樣向棧中添加或者刪除數據:
import org.apache.logging.Log4j.ThreadContext;
...
ThreadContext.push(username);
ThreadContext.push(sessionID);
// Logging methods go here
ThreadContext.pop();
...
當使用線程上下文映射時,我們可以像MDC那樣將值和鍵結合在一起:
import org.apache.logging.Log4j.ThreadContext;
...
ThreadContext.put(“username”,"admin");
ThreadContext.put("sessionID", "1234");
// Logging methods go here
ThreadContext.clearMap();
...
ThreadContext類提供了一些方法,用于清除棧、清除MDC、清除存儲在上下文中的所有值,對應的方法是ThreadContext.clearAll()、ThreadContext.clearMap()和ThreadContext.clearStack()。
和在MDC以及NDC中一樣,我們可以使用Layouts在線程上下文中訪問這些值。使用PatternLayout時,%x轉換模式會從棧中獲取值,%X和%X(鍵)會從圖中獲取值。
ThreadContext過濾
一些框架允許你基于某些屬性對日志進行過濾。例如,Log4j的DynamicThresholdFilter 會在鍵滿足特定條件的情況下,自動調整日志級別。再比如,如果我們想要觸發TRACE級別的日志消息,我們可以創建一個名為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 會決定如何處理它。關于 onMatch 和 onMismatch,我們有三個可選項:ACCEPT,它會處理過濾器的規則;DENY,它會忽略過濾器的規則;NEUTRAL,它會推遲到下一個過濾 器。除了這些,我們還定義一個鍵值對,當值為true時,我們啟用TRACE級別的日志。
現在,當trace-logging-enabled被設置成true時,即使根Logger設置的日志級別高于TRACE,Appender也會記錄TRACE級別的消息。
你可能還想過濾一些特定的日志到特定的Appender中,Log4j中提供了ThreadContextMapFilter來實現這一點。如果我們想要限制某個特定的Appender,只記錄針對某個用戶的TRACE級別的消息,我們可以基于username鍵添加一個ThreadContextMapFilter:
<Console name="ConsoleAppender" target="SYSTEM_OUT">
<ThreadContextMapFilter onMatch="ACCEPT" onMismatch="DENY">
<KeyValuePair key="username" value="admin" />
</ThreadContextMapFilter>
...
如果想了解更多信息,你可以查看Log4j和Logback文檔中關于DynamicThresholdFilter部分。
Markers
Markers允許你對單獨的日志記錄添加一些***的數據。它可以用來對日志記錄進行分組,觸發一些行為,或者對日志記錄進行過濾,并將過濾結果輸出到指定的Appender中。你甚至可以將Markers和ThreadContext結合在一起使用,以提高搜索和過濾日志數據的能力。
例如,假設我們有一個可以連接到數據庫的類,如果在打開數據庫的時候發生了異常,我們需要把異常記錄成fatal錯誤。我們可以創建一個名為DB_ERROR的Marker,然后將其應用到日志事件中:
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轉換模式:
<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數據進行自動解析和排序,集中式的日志服務可以很容易對日志進行搜索處理。
Markers過濾
Marker過濾器可以讓你決定哪些Marker由哪些Logger來處理。marker字段會比較在日志事件里面的Marker名字,如果名字匹配,那么Logger會執行后續的行為。例如,在Log4j中,我們可以配置一個Appender來只顯示哪些使用了DB_ERROR Marker的消息,這可以通過log4j2.xml中的Appender添加如下信息來實現:
<MarkerFilter marker="DATABASE_ERROR" onMatch="ACCEPT" onMismatch="DENY" />
如果日志記錄中某一條的Marker可以匹配這里的marker字段,那么onMatch會決定如何處理這條記錄。如果不能夠匹配,或者日志記錄中 沒有Marker信息,那么onMismatch就會決定如何處理這條記錄。對于onMatch和onMismatch來說,有3個可選 項:ACCEPT,它允許記錄事件;DENY,它會阻塞事件;NEUTRAL,它不會對事件進行任何處理。
在Logback中,我們需要更多一些設置。首先,想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結合使用
Marker的功能和ThreadContext類似,它們都是向日志記錄中添加***的數據,這些數據可以被Appender訪問。如果把這兩者結合使用,可以讓你更容易的對日志數據進行索引和搜索。如果能夠知道何時使用哪一種技術,會對我們有所幫助。
NDC、MDC和ThreadContext被用于將相關日志記錄結 合在一起。如果你的應用程序會處理多個同時存在的用戶,ThreadContext可以讓你將針對某個特定用戶的一組日志記錄組合在一起。因為 ThreadContext針對每個線程都是不一樣的,所以你可以使用同樣的方法來對相關的日志記錄進行自動分組。
另一方面,Marker通常用于標記或者高亮顯示某些特殊事件。在上述示例中,我們使用DB_ERROR Marker來標明在方法中發生的SQL相關異常。我們可以使用DB_ERROR Marker來將這些事件的處理過程和其他事件區分開來,例如我們可以使用SMTP Appender來將這些事件通過郵件發送給數據庫管理員。
“Java日志的核心組件是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。