日誌的列印在軟體開發過程中必不可少,一般分為兩個大類: 操作日誌 系統日誌 操作日誌,主要針對的是用戶,例如在Photoshop軟體中會記錄自己操作的步驟,便於用戶自己查看。 系統日誌,主要針對的是軟體開發人員(包括測試、維護人員),也就是說這部分的日誌用戶是看不到的,也就是我們通常所說的debug ...
日誌的列印在軟體開發過程中必不可少,一般分為兩個大類:
- 操作日誌
- 系統日誌
操作日誌,主要針對的是用戶,例如在Photoshop軟體中會記錄自己操作的步驟,便於用戶自己查看。
系統日誌,主要針對的是軟體開發人員(包括測試、維護人員),也就是說這部分的日誌用戶是看不到的,也就是我們通常所說的debug日誌。
在大學中所謂的實踐項目或者老師佈置的作用中,通常是不會在意日誌,除非在作業中有特別的需要,往往在開發過程中直接列印控制台語句來調試程式,這是極為不專業的調試開發過程。所以這也就導致了一個問題,大學畢業和工作時銜接不上最大的問題不在於技術上的難度,而是日誌列印的問題。這個看似不起眼的問題對於應屆生來說往往是“惡夢”,操作日誌相對比較好理解,用戶做了什麼就記錄什麼;而列印系統日誌則無從下手,往往一般有下麵幾個方面——3W:
- Where:不清楚在何處列印日誌
- Who:不清楚列印什麼級別的日誌
- What:不清楚日誌應該包含什麼內容
本篇著重講解系統日誌,所以以下“日誌”均為“系統日誌”的簡稱。我將針對這幾個方面對系統日誌的列印做一個簡要的總結。另外對Java中常用的日誌列印框架(log4j)的幾種使用方式做一個示範。
WHERE
1.程式入口
在入口列印日誌是因為這個時候傳遞進來的參數沒有經過任何處理,將它列印在日誌文件中能一眼就知道程式的原始數據是否符合我們的預期,是不是傳遞進來的原始數據就出現 的問題。
2.異常捕獲
在異常列印出詳細的日誌能讓你快速定位錯誤在哪裡,例如在程式拋出異常捕獲時,在平時我們經常就是直接在控制台列印出堆棧信息e.printStackTrace(),但在實際的生產環境更加艱苦,更別說有IDE來讓你查看控制台信息,此時就需要我們將堆棧信息記錄在日誌中,以便發生異常時我們能準確定位程式在哪裡出錯。
3.重要信息
這一點可能很寬泛,因為不同的業務邏輯重點可能並不一樣,例如在有的重要參數不能為空,此時就需要判斷是否為空,如果為空則記錄到日誌中;還有的例如傳遞進來的參數經過一系列的演算法處理過後,此時也需要列印日誌來查看是否計算正確。但切記,儘量不要直接在for迴圈中列印日誌,特別是for迴圈特別大時,這樣你的日誌可能分分鐘被沖得不見蹤跡,甚至帶來性能上的影響。
WHO
日誌列印通常有四種級別,從高到底分別是:ERROR、WARN、INFO、DEBUG。應該選用哪種級別就是個很重要的問題。
首先明確日誌級別中的優先順序是什麼意思,在你的系統中如果開啟了某一級別的日誌後,就不會列印比它級別低的日誌。例如,程式如果開啟了INFO級別日誌,DEBUG日誌就不會列印,但不列印不代表不產生,這在後面會提到。通常在生產環境中開啟INFO日誌。
那麼應該列印什麼級別的日誌呢?首先我們應該明確誰在看日誌。
通常來說,系統出了問題客戶不會進到系統對著黑黢黢的控制台查看日誌輸出,所以日誌所面對的主體對象必然是軟體開發人員(包括測試測試、維護人員)。
下麵我們假設幾種場景來幫助我們理解日誌級別。
首先,程式開髮結束後交由給測試人員進行測試,測試人員根據測試用例發現某個用例的輸出和預期不符,此時他的第一反應該是查看日誌。此時的日誌是INFO級別日誌不會出現DEBUG級別的日誌,現在就需要根據日誌列印分為兩種情況決定他下一步操作:
- 通過查看INFO日誌發現是由於自己操作失誤,造成了程式結果和預期不符合,這種情況不是程式出錯,所以並不是bug,不需要開發人員到場。
- 通過查看INFO日誌發現自己的操作正確,根據INFO日誌查看並不是操作失誤造成,這個時候就需要開發人員到場確認。
- 以上兩種情況是理想情況,測試人員僅根據INFO級別的日誌就能判斷出程式的輸出結果與預期不符是因為自己操作失誤還是程式bug。更為現實的情況實際是測試人員並不能根據INFO級別的日誌判斷是否是自己失誤還是程式bug。
綜上,INFO級別的日誌應該是能幫助測試人員判斷這是否是一個真正的bug,而不是自己操作失誤造成的。
假設測試人員現在已經初步判斷這是一個bug,並且這個bug不那麼明顯,此時就需要開發人員到場確認。
開發人員到達現場後,第一步應該是查看INFO日誌初步作初步判斷驗證測試人員的看法,接著如果不能判斷出問題所在則應該是將日誌級別調整至DEBUG級別,列印出DEBUG級別的日誌,通過DEBUG日誌來分析定位bug出在哪裡。
所以,DEBUG級別的日誌應該是能幫助開發人員分析定位bug所在的位置。
ERROR和WARN的級別都比INFO要高,所以在設定日誌級別在INFO時,這兩者的日誌也會被列印。根據上面INFO和DEBUG級別的區別以及適用人員可以知道,ERROR和WARN是同時給測試和開發觀察的。
WARN級別稱之為“警告”,這個“警告”實際上就有點含糊了,它不算錯,你可以選擇忽視它,但也可以選擇重視它。例如,現在一個WARN日誌打出這麼一條日誌“系統有崩潰的風險”,這個時候就需要引起足夠的重視,它代表現在不會崩潰,但是它有崩潰的風險。或者出現“某用戶在短時間內將密碼輸出很多次過後才進入了系統”,這個時候是不是系統被暴力破解了呢?等等,這個級別日誌如同它的字面含義,給你一個警告,你可以選擇忽視,也可以重視,但至少它現在不會給系統帶來其他影響。
ERROR級別稱之為“錯誤”,這個含義就更明顯了,就是系統出現了錯誤,需要處理。最為常見的就是捕獲異常時所列印的日誌。
上面我們介紹了四種日誌級別的區別,特別需要註意的是INFO級別和DEBUG級別所適用的人員。那麼我們該如何選擇哪個級別的日誌輸出呢?
以下是我的個人理解:
INFO
- 程式入口,這能讓開發人員確認參數是否為自己所為。
- 計算結果,測試關心的程式的輸出結果是否符合預期,那麼對於計算過程不應該關心,僅給出計算結果就能判斷是否符合預期。
DEBUG
對於DEBUG級別,我認為更關心的是過程,以及更為具體的相關信息,因為幫助它的定位在於幫助開發人員定位bug,定位bug就需要較為詳細的參數信息才能定位。例如對於某個具體的演算法過程,可以使用DEBUG列印,開發人員不僅關心結果,同時在結果不正確時應該能根據DEBUG日誌查詢計算過程是否出現偏差
WARN
某個不常走到的分支,對於常規的操作是不應該列印WARN日誌的,只有在滿足某個條件才能走到的分支,且這個分支引起了“警覺”,此時就應該列印WARN日誌。
ERROR
毫無疑問出現錯誤,程式不能繼續運行下去就應該列印ERROR日誌,這個錯誤並不是業務上的錯誤。例如,新增某個用戶發現已經存在時,此時雖然新增失敗,但不能說程式出現錯誤就列印ERROR日誌;在刪除某個用戶發現用戶已經被鎖定時,此時也不能說因為程式不能按照刪除的邏輯繼續運行下去就應該列印ERROR日誌。
WHAT
應該列印什麼內容?列印的內容一定要從實際出發。也就是說如果在實際的生產環境中,你的用戶量很大,日誌在不停地刷新,如何定位某個用戶的整個登錄以及後續的操作呢?當然就是根據用戶名來跟蹤。所以列印內容的第一要素就是要能便於定位;定位過後也許用戶在好幾個模板中進行操作,還是定位,這個時候定的是模塊的位;還有一點當然就是用戶操作時的具體參數;最後一點就是用戶幹了什麼。
總結就是,[id, module, params, content](關鍵字,模塊,參數,內容)。
以上就是對日誌列印的幾點建議,說的不全面,拋磚引玉。下麵是對日誌列印框架(log4j)的非最佳實踐。
在Spring中使用log4j日誌框架
Spring中使用log4j日誌框架可以說是最為常見的應用場景了,我們將結合Spring對log4j做一個簡單的示範。
在IDEA中創建一個Maven構建的Web項目,項目結構如下圖所示:
pom.xml中的依賴如下:
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.6.6</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.6.6</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> </dependency>
在resources(IDEA中resources就是classpath路徑)中新建一個log4j.properties文件,如下所示:
1 log4j.rootLogger = INFO, stdout, logfile 2 #日誌輸出到控制台 3 log4j.appender.stdout = org.apache.log4j.ConsoleAppender 4 log4j.appender.stdout.layout = org.apache.log4j.PatternLayout 5 log4j.appender.stdout.ConversionLayout = %d [%t] %-5p %c - %m%n 6 #日誌輸出到文件 7 log4j.appender.logfile = org.apache.log4j.DailyRollingFileAppender 8 log4j.appender.logfile.File = /Users/yulinfeng/Log/log 9 log4j.appender.logfile.maxFileSize=10240KB #日誌的最大容量為10M 10 log4j.appender.logfile.Append = true #是否追加寫進文件 11 log4j.appender.logfile.Threshold = DEBUG #輸出DEBUG級別日誌到文件中 12 log4j.appender.logfile.layout = org.apache.log4j.PatternLayout 13 log4j.appender.logfile.layout.ConversionPattern = %d [%t] %-5p %c - %m%n
第1行,log4j.rootLogger=INFO, stdout, logfile。這是log4j的根配置,第一個參數表示輸出什麼級別的日誌,後面的參數表示輸出的位置,位置可以是控制台,也可以是文件,語法為log4j.rootLogger=[level], appendername……,在這裡定義了兩個輸出位置,名字無所謂取設麽,有意義即可。日誌級別從高到低分別是:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL,log4j建議只使用ERROR、WARN、INFO、DEBUG四個級別,也就是也就是在上面提到過的。
第3、7行就分別指定了stdout和logfile日誌的輸出位置,log4j一共提供了5個。
org.apache.log4j.ConsoleAppender(控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(每天產生一個日誌文件)
org.apache.log4j.RollingFileAppender(文件大小達到一定大小後產生一個新的文件)
org.apache.log4j.WriterAppender(將日誌信息以流格式發送到任意位置)
第4行表示日誌信息的格式,一共有以下幾種。
org.apache.log4j.HTMLLayout(以HTML表格輸出)
org.apache.log4j.PatternLayout(靈活的自定義格式輸出)
org.apache.log4j.SimpleLayout(簡單的格式輸出,只包括日誌級別和日誌信息的字元串)
org.apache.log4j.TTCCLayout(包含線程、日誌級別、日誌所在類和日誌信息的字元串)
通常為了更為靈活的列印日誌,我們會選擇PatternLayout佈局的日誌,同時通過ConversionPattern自定義輸出格式。
按照上面的配置,我們就可以在代碼中進行日誌的輸出了。由於是在Spring框架下使用log4j,所以就要使用Spring對log4j進行初始化,在web.xml中對log4j進行初始化。
<web-app> <display-name>log web</display-name> <context-param> <param-name>log4jConfigurationLocation</param-name> <param-value>classpath*:log4j.properties</param-value> </context-param> <!--每隔60s掃描log4j的配置文件,這裡配置的log4jRefreshInterval參數表示能不用重啟web伺服器就能動態更改log4j日誌級別,這也是和Spring整合的一大好處--> <context-param> <param-name>log4jRefreshInterval</param-name> <param-value>60000</param-value> </context-param> <listener> <!--從spring4.2.1開始Log4jConfigListener已經被廢棄,最好使用log4j2對應的org.apache.logging.log4j.web.Log4jServletContextListener .Log4jServletContextListener--> <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> </listener> </web-app>
此時在代碼邏輯中加入以下代碼即可根據我們的配置輸出系統日誌。
private Logger log = Logger.getLogger(Test.class); log.info(“test info”);
上面是所有日誌文件都輸出到一個文件的情況,在實際中我們很有可能針對不同的模塊輸出到不同到日誌文件。
修改log4j.properties:
1 log4j.rootLogger = INFO, module1, module2 2 #輸出到控制台 3 log4j.appender.stdout = org.apache.log4j.ConsoleAppender 4 log4j.appender.stdout.layout = org.apache.log4j.PatternLayout 5 log4j.appender.stdout.ConversionLayout = %d [%t] %-5p %c - %m%n 6 #模塊1輸出的日誌文件 7 log4j.appender.module1 = org.apache.log4j.DailyRollingFileAppender 8 log4j.appender.module1.File = /Users/yulinfeng/Log/module1 9 log4j.appender.module1.maxFileSize=10240KB #日誌的最大容量為10M 10 log4j.appender.module1.Append = true #是否追加寫進文件 11 log4j.appender.module1.Threshold = DEBUG #輸出DEBUG級別日誌到文件中 12 log4j.appender.module1.layout = org.apache.log4j.PatternLayout 13 log4j.appender.module1.layout.ConversionPattern = %d [%t] %-5p %c - %m%n 14 #模塊2輸出的日誌文件 15 log4j.appender.module2 = org.apache.log4j.DailyRollingFileAppender 16 log4j.appender.module2.File = /Users/yulinfeng/Log/module2 17 log4j.appender.module2.maxFileSize=10240KB #日誌的最大容量為10M 18 log4j.appender.module2.Append = true #是否追加寫進文件 19 log4j.appender.module2.Threshold = DEBUG #輸出DEBUG級別日誌到文件中 20 log4j.appender.module2.layout = org.apache.log4j.PatternLayout 21 log4j.appender.module2.layout.ConversionPattern = %d [%t] %-5p %c - %m%n
在模塊1中輸出日誌文件時其實就是參數不同而已:
private Logger log = Logger.getLogger(“module1”); log.info(“test info”);
在模塊2中:
private Logger log = Logger.getLogger(“module2”); log.info(“test info”);
以上就是在Spring中使用log4j日誌框架的非最佳實踐。
最後,還要介紹另外一種列印日誌的方式,上面的方式將會在每個類中都定義一個Logger對象,這樣的代碼相對於業務邏輯來說實際是不想關,此時就可以利用Spring中的AOP面向切麵編程列印日誌。這裡可能不是所有的人都能接觸到利用AOP來列印日誌,這裡暫時不做詳細介紹。
這是一個能給程式員加buff的公眾號