我們都知道日誌對於一個系統的重要性,在編碼調試階段我們可以通過控制檯的輸出來找出並解決bug,當向系統發一個請求時,我們可以通過日誌來查看代碼的執行流程是否與預期相符從而做出調整。
下文中的Log指的是log4j、logback等日誌工具。用到的IDE工具是IDEA。
一.不用System.out.print
1.System.out.print/println
java.Lang.System類下有個靜態PrintStream變量out,如下圖:
再來看看這個PrintStream類
特點:方便,直觀,是jre包中類,隨時可用。
2.Log
通常情況下在實際項目中,我們用一套專門的框架來記錄系統運行時的信息。基於spring AOP的思想,業務代碼和日誌代碼隔離開,降低他們之間的耦合性。業務代碼的運行與日誌的打印並非同步。各自執行各自的部分。
特點:異步、解耦、靈活、策略多
3.結論
作用,用System.out和Log幾乎是樣的。
System.out 是java先天帶來的,只要能java程序能運行,System.out就可用;而Log要引入一些包,做一些配置後,整個項目一直都能用。可是如果在項目中如果使用了System.out作爲輸出,它就是業務邏輯代碼的一部分,大量的System.out就會產生大量的IO,無疑會對系統性能產生影響。
下面來看大佬的一些觀點:
- System.out.print和java運行程序運行在同一線程,也就是說,業務程序會等待System.out的動作,導致資源被佔用,log4j、logback等日誌工具進行調試信息的打印,這類工具是異步線程的,不會使程序處於等待狀態。
- System.out.print是在控制檯輸出,只能輸出到控制檯,功能上線後,總不能一直盯着控制檯吧,而且日誌文件需要保留,以供日後分析,是需要一套管理規範的,即便使用tomcat服務器,System.out會輸出到catalina.out文件,catalina.out文件也不會一直存在,需要定期清空,如果不清空,大文件的讀寫也是挺影響性能的。說到這裏,System.out.print寫入的文件只有一個,對於一個文件的讀寫,這個io肯定會排隊寫,且System.out.print在當前線程,肯定對性能會有稍微的影響。
- 程序中充斥着大量的System.out.print打印代碼是相當不規範的。
綜上所述,我們在實際項目中不使用System.out來打印日誌。
二.日誌框架
1.概況
根據jdbc——數據驅動的思想,寫一個統一的接口層;日誌門面(日誌的一個抽象層);loging-abstract.jar,給項目中導入具體的日誌實現就行了。我們之前的日誌框架都是實現的抽象層。
日誌門面(日誌的抽象層) | 日誌實現 |
JCL(Jakarta Commons Logging) 、 SLF4J(Simple Logging Facade for Java) 、 Jboss-logging |
log4J、log4J2、 logback、 JUL(java.util.logging) |
左邊選一個門面(抽象層),右邊選一個來實現。
日誌門面:
JCL(Jakarta Commons Logging) : 提供統一的日誌記錄接口,也常稱爲commons-logging。2014年更新的,太舊不用了。
Jboss-logging: 一般我們用不到。
SLF4J:與JCL一樣提供統一的日誌記錄接口,可以方便地切換看具體的實現框架。
日誌實現:
JUL:JDK中的日誌記錄工具,也常稱爲JDKLog、jdk-logging。
LOG4J1:一個具體的日誌實現框架。
LOG4J2:一個具體的日誌實現框架,是LOG4J1的下一個版本。
LOGBACK:一個具體的日誌實現框架,但其性能更好。
2.組合
springboot:底層是spring框架,spring框架默認是JCL
springboot選中的是:SLF4J 和 logback
LogBack 被分爲3個組件:logback-core、logback-classic 和 logback-access。
- logback-core 提供了 LogBack 的核心功能,是另外兩個組件的基礎。
- logback-classic 則實現了 SLF4J 的API,所以當想配合 SLF4J 使用時,需要將 logback-classic 引入依賴中。
- logback-access 是爲了集成Servlet環境而準備的,可提供HTTP-access的日誌接口。
三.springboot使用日誌
1.springboot日誌關係
在任意一個springboot的web項目中,在IDEA中打開pom文件,看到下面的依賴(點進去spring-boot-starter依賴就可以看到
spring-boot-starter-logging依賴),如下圖:
緊接着在pom文件中任意一處右鍵,點擊Diagrams後,選中Show Dependenes…,如下圖:
可以看到spring-boot-starter-logging底層依賴關係,如下圖:
2.一個簡單示例
在springboot項目的測試類中,我們簡單的使用下Log,代碼如下:
package com.example.demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);
@Test
public void contextLoads() {
// System.out.println("不用這種方式打印日誌了");
/**
* 日誌級別:由低到高,trace -> debug -> info -> warn -> error
* 以後可以調整日誌級別,只產生當前日誌級別及其以上級別的日誌
* springboot默認使用的是info級別的日誌
*/
log.trace("...............trace............");
log.debug("..........debug.............");
log.info(".............info...........");
log.warn("...............warn................");
log.error("...............error............");
//參數使用
String car = "taxi";
String color = "yellow";
log.info("這個{},是{}的",car,color);
}
}
運行該測試類,可以看到日誌輸出,的確只打印了info、warn、error級別的日誌,如下圖:
3.日誌配置文件
LogBack的日誌記錄數據流是從 Class(Package)到 Logger,再從Logger到Appender,最後從Appender到具體的輸出終端。
如下圖所示:
LogBack配置文件可以分爲幾個節點,其中 Configuration 是根節點,Appender、Logger、Root是Configuration的子節點。
(1).appender節點
是Configuration 的子節點,是負責寫日誌的組件。appender有兩個必要屬性name、class 。name指定appender的名稱,class指定appender的全限定名。
class,主要包括:
- ch.qos.logback.core.ConsoleAppender 控制檯輸出
- ch.qos.logback.core.FileAppender 文件輸出
- ch.qos.logback.core.RollingFileAppender 文件滾動輸出
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration debug="true" scan="true" scanPeriod="2">
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
</appender>
<!-- conf file out -->
<appender name="file_out" class="ch.qos.logback.core.FileAppender">
</appender>
<!-- conf file out -->
<appender name="file_out" class="ch.qos.logback.core.RollingFileAppender">
</appender>
<root></root>
<logger></logger>
</configuration>
a.ch.qos.logback.core.ConsoleAppender
把日誌添加到控制檯,有如下節點:
<encoder>
: 對日誌進行格式化。<target>
: 字符串System.out 或者 System.err, 默認 System.out;
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console_out" />
</root>
</configuration>
b.FileAppender
把日誌添加到文件,有如下節點:
<file>
:被寫入的文件名,可以是相對目錄 , 也可以是絕對目錄 , 如果目錄不存在則會自動創建。<append>
:如果是true , 日誌被追加到文件結尾 , 如果是false,清空現存文件 , 默認是true。<encoder>
:對日誌進行格式化 [具體的轉換符說明請參見官網.]
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appender name="file_out" class="ch.qos.logback.core.FileAppender">
<file>logs/debug.log</file>
<encoder>
<pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
</encoder>
</appender>
</configuration>
c.RollingFileAppender
滾動紀錄文件,先將日誌記錄到指定文件,當符合某種條件時,將日誌記錄到其他文件,有如下節點:
<file>
:被寫入的文件名,可以是相對目錄,也可以解決目錄,如果目錄不存在則自動創建。<append>
:如果是true,日誌被追加到文件結尾,如果是false,清空現存文件,默認是true。<encoder>
:對日誌進行格式化。<rollingPolicy>
:當發生滾動時,決定 RollingFileAppender 的行爲,涉及文件移動和重命名。
<rollingPolicy>節點有如下策略:
TimeBaseRollingPolicy :最常用的滾動策略,根據時間來制定滾動策略,即負責滾動也負責觸發滾動。有如下節點;
<fileNamePattern>
:必要節點,包含文件及“%d” 轉換符,“%d”可以包含一個java.text.SimpleDateFormat 制定的時間格式,如:%d{yyyy-MM},如果直接使用%d ,默認格式是 yyyy-MM-dd。<maxHistory>
:可選節點,控制保留的歸檔文件的最大數量,超出數量就刪除舊文件,假設設置每個月滾動,且 是 6,則只保存最近6個月的文件,刪除之前的舊文件,注意:刪除舊文件是哪些爲了歸檔而創建的目錄也會被刪除。<filenamePattern>
:必須包含“%i” 例如:設置最小值,和最大值分別爲1和2,命名模式爲 log%i.log,會產生歸檔文件log1.log和log2.log,還可以指定文件壓縮選項,例如:log%i.log.gz 或者 log%i.log.zip
triggeringPolicy:告知RollingFileAppender,激活RollingFileAppender滾動。
示例如下:
<!-- 03:conf errorAppender out -->
<appender name="errorAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/error.log</file>
<!-- 設置滾動策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--設置日誌命名模式-->
<fileNamePattern>errorFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 超過150MB時,觸發滾動策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
(2).logger節點
logger是的子節點,來設置某一個包或者具體的某一個類的日誌打印級別,以及指定。logger僅有一個name屬性,兩個可選屬性 level/addtivity。
- name:用來指定受此loger約束的某一個包或者具體的某一個類。
- level:用來設置打印級別,大小寫無關。可選值有TRACE、DEBUG、INFO、WARN、ERROR、ALL和OFF。還有一個特俗值INHERITED 或者 同義詞NULL,代表強制執行上級的級別。如果未設置此屬性,那麼當前logger將會繼承上級的級別。
- addtivity:是否向上級logger傳遞打印信息,默認爲true;
<logger>可以包含零個或多個<appender-ref>元素,表示這個appender將會添加到logger。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 過濾掉非INFO級別 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- conf infoAppender out -->
<appender name="infoAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/info.log</file>
<!-- 設置滾動策略 -->
<rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--設置日誌命名模式-->
<fileNamePattern>infoFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPoliy>
<!-- 超過150MB時,觸發滾動策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
<!-- 添加兩個appender節點 -->
<logger name="logback.olf.log" level="info">
<appender-ref ref = "console_out"/>
<appender-ref ref = "infoAppender"/>
</logger>
</configuration>
(3).root節點
元素配置根logger。該元素有一個level屬性,沒有name屬性,因爲已經被命名 爲root。Level屬性的值大小寫無關,其值爲下面其中一個字符串:TRACE、DEBUG、INFO、 WARN、ERROR、ALL 和 OFF。如果 root 元素沒 有引用任何 appender,就會失去所有 appender。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 過濾掉非INFO級別 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 01:conf infoAppender out -->
<appender name="infoAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/info.log</file>
<!-- 設置滾動策略 -->
<rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--設置日誌命名模式-->
<fileNamePattern>infoFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPoliy>
<!-- 超過150MB時,觸發滾動策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
<!-- 02:conf debugAppender out -->
<appender name="debugAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/debug.log</file>
<!-- 設置滾動策略 -->
<rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--設置日誌命名模式-->
<fileNamePattern>debugFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPoliy>
<!-- 超過150MB時,觸發滾動策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
<!-- 03:conf errorAppender out -->
<appender name="errorAppender" class="ch.qos.logback.core.RollingFileAppender">
<file>logs/error.log</file>
<!-- 設置滾動策略 -->
<rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--設置日誌命名模式-->
<fileNamePattern>errorFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--最多保留30天log-->
<maxHistory>30</maxHistory>
</rollingPoliy>
<!-- 超過150MB時,觸發滾動策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>150</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
</encoder>
</appender>
<root level="ALL">
<appender-ref ref="infoAppender"/>
<appender-ref ref="debugAppender"/>
<appender-ref ref="errorAppender"/>
</root>
</configuration>
(4).filter過濾節點
a.級別過濾器(LevelFilter)
LevelFilter 根據記錄級別對記錄事件進行過濾。如果事件的級別等於配置的級別,過濾 器會根據 onMatch 和 onMismatch 屬性接受或拒絕事件。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 過濾掉非INFO級別 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
b.臨界值過濾器(ThresholdFilter)
ThresholdFilter過濾掉低於指定臨界值的事件。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 過濾掉TRACE和DEBUG級別的日誌 -->
<level>INFO</level>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
c.求值過濾器(EvaluatorFilter)
評估是否符合指定的條件
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.EvaluatorFilter">
<evaluator>
<!--過濾掉所有日誌中不包含hello字符的日誌-->
<expression>
message.contains("hello")
</expression>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</evaluator>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
d.匹配器(Matchers)
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- conf consoel out -->
<appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.EvaluatorFilter">
<evaluator>
<matcher>
<Name>odd</Name>
<!-- 過濾掉序號爲奇數的語句-->
<regex>statement [13579]</regex>
</matcher>
<expression>odd.matches(formattedMessage)</expression>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</evaluator>
</filter>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console_out" />
</root>
</configuration>
4.日誌框架切換
在實際開發過程中,我們的項目可能用了不止一種框架,例如:spring(common-logging)、hibernate(Jboss-logging)、mybatis、xxxx等等其他框架,括號中是各框架用的日誌實現,那麼如何統一使用slf4j進行輸出呢?
(1).概述
下面是slf4j官網的一個圖解說明
從日誌框架轉向SLF4J
- jul-to-slf4j:jdk-logging到slf4j的橋樑
- log4j-over-slf4j:log4j1到slf4j的橋樑
- jcl-over-slf4j:commons-logging到slf4j的橋樑
從SLF4J轉向具體的日誌框架
- slf4j-jdk14:slf4j到jdk-logging的橋樑
- slf4j-log4j12:slf4j到log4j1的橋樑
- log4j-slf4j-impl:slf4j到log4j2的橋樑
- logback-classic:slf4j到logback的橋樑
- slf4j-jcl:slf4j到commons-logging的橋樑
(2).示例
SLF4J+Logback 切換成 slf4j+log4J
上面的示例中所用的項目日誌門面和日誌實現組合是(SLF4J+Logback,springboot項目都是這種組合)。
a.先把logback-classic和log4j-over-slf4j依賴從依賴樹中移除,移除方式如下圖:
b.在pom文件中添加slf4j-log4j12的依賴,如下圖:
再次打開依賴樹,可以看到slf4j-log4j12以及log4j依賴已經添加上了,如下圖:
c.啓動項目,查看控制檯日誌,紅色字體的warn是因爲項目中沒有log4j日誌框架相關的配置文件,如下圖:
d.接下來我們給項目的類路徑下添加log4j日誌框架的配置文件log4j.properties,如下圖:
e.再次啓動項目,查看控制檯輸出,日誌多了。且沒有剛纔的警告了
slf4j+logback 換成 slf4j+log4j2
a.將依賴樹中的spring-boot-starter-logging依賴先排除,如下圖:
b.接着在pom.xml文件中添加上spring-boot-starter-log4j2依賴,如下圖:
·
c.接下來再看依賴樹,如下圖,spring-boot-log4j2依賴已經添加進來了。
d.接下來啓動項目,查看控制檯輸出如下:
注: 這裏的改動參考spring官方文檔springboot官方文檔starter部分 ,如下圖:
本文是通過查閱了很多大佬的日誌相關文章和觀看了一些教學視頻後總結的,其中有很多地方都是前人整理的,例如,配置文件部分,拿到這裏一是方便我自己看,而是希望這部分知識能夠傳的更遠,如有侵權,聯繫可刪。其餘部分均爲我自己所得,歡迎大家批評指正,一起進步!
參考資料:
https://www.cnblogs.com/han-1034683568/p/6637914.html
https://www.cnblogs.com/chanshuyi/p/something_about_java_log_framework.html
https://www.imooc.com/article/details/id/21908#
https://docs.spring.io/spring-boot/docs/2.0.7.RELEASE/reference/htmlsingle/#using-boot-starter