溫馨提示×

溫馨提示×

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

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

如何使用Java日志框架的logback

發(fā)布時間:2020-08-01 09:02:22 來源:億速云 閱讀:121 作者:小豬 欄目:開發(fā)技術(shù)

這篇文章主要講解了如何使用Java日志框架的logback,內(nèi)容清晰明了,對此有興趣的小伙伴可以學(xué)習(xí)一下,相信大家閱讀完之后會有幫助。

為什么使用logback

記得前幾年工作的時候,公司使用的日志框架還是log4j,大約從16年中到現(xiàn)在,不管是我參與的別人已經(jīng)搭建好的項目還是我自己主導(dǎo)的項目,日志框架基本都換成了logback,總結(jié)一下,logback大約有以下的一些優(yōu)點:

  • 內(nèi)核重寫、測試充分、初始化內(nèi)存加載更小,這一切讓logback性能和log4j相比有諸多倍的提升
  • logback非常自然地直接實現(xiàn)了slf4j,這個嚴(yán)格來說算不上優(yōu)點,只是這樣,再理解slf4j的前提下會很容易理解logback,也同時很容易用其他日志框架替換logback
  • logback有比較齊全的200多頁的文檔
  • logback當(dāng)配置文件修改了,支持自動重新加載配置文件,掃描過程快且安全,它并不需要另外創(chuàng)建一個掃描線程
  • 支持自動去除舊的日志文件,可以控制已經(jīng)產(chǎn)生日志文件的最大數(shù)量

總而言之,如果大家的項目里面需要選擇一個日志框架,那么我個人非常建議使用logback。

logback加載

我們簡單分析一下logback加載過程,當(dāng)我們使用logback-classic.jar時,應(yīng)用啟動,那么logback會按照如下順序進行掃描:

  • 在系統(tǒng)配置文件System Properties中尋找是否有l(wèi)ogback.configurationFile對應(yīng)的value
  • 在classpath下尋找是否有l(wèi)ogback.groovy(即logback支持groovy與xml兩種配置方式)
  • 在classpath下尋找是否有l(wèi)ogback-test.xml
  • 在classpath下尋找是否有l(wèi)ogback.xml

以上任何一項找到了,就不進行后續(xù)掃描,按照對應(yīng)的配置進行l(wèi)ogback的初始化,具體代碼實現(xiàn)可見ch.qos.logback.classic.util.ContextInitializer類的findURLOfDefaultConfigurationFile方法。

當(dāng)所有以上四項都找不到的情況下,logback會調(diào)用ch.qos.logback.classic.BasicConfigurator的configure方法,構(gòu)造一個ConsoleAppender用于向控制臺輸出日志,默認(rèn)日志輸出格式為"%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"。

logback的configuration

logback的重點應(yīng)當(dāng)是Appender、Logger、Pattern,在這之前先簡單了解一下logback的<configuration>,<configuration>只有三個屬性:

  • scan:當(dāng)scan被設(shè)置為true時,當(dāng)配置文件發(fā)生改變,將會被重新加載,默認(rèn)為true
  • scanPeriod:檢測配置文件是否有修改的時間間隔,如果沒有給出時間單位,默認(rèn)為毫秒,當(dāng)scan=true時這個值生效,默認(rèn)時間間隔為1分鐘
  • debug:當(dāng)被設(shè)置為true時,將打印出logback內(nèi)部日志信息,實時查看logback運行信息,默認(rèn)為false

<logger>與<root>

先從最基本的<logger>與<root>開始。

<logger>用來設(shè)置某一個包或者具體某一個類的日志打印級別、以及指定<appender>。<logger>可以包含零個或者多個<appender-ref>元素,標(biāo)識這個appender將會添加到這個logger。<logger>僅有一個name屬性、一個可選的level屬性和一個可選的additivity屬性:

  • name:用來指定受此logger約束的某一個包或者具體的某一個類
  • level:用來設(shè)置打印級別,五個常用打印級別從低至高依次為TRACE、DEBUG、INFO、WARN、ERROR,如果未設(shè)置此級別,那么當(dāng)前l(fā)ogger會繼承上級的級別
  • additivity:是否向上級logger傳遞打印信息,默認(rèn)為true

<root>也是<logger>元素,但是它是根logger,只有一個level屬性,因為它的name就是ROOT,關(guān)于這個地方,有朋友微信上問起,源碼在LoggerContext中:

public LoggerContext() {
  super();
  this.loggerCache = new ConcurrentHashMap<String, Logger>();

  this.loggerContextRemoteView = new LoggerContextVO(this);
  this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
  this.root.setLevel(Level.DEBUG);
  loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
  initEvaluatorMap();
  size = 1;
  this.frameworkPackages = new ArrayList<String>();
}

Logger的構(gòu)造函數(shù)為:

Logger(String name, Logger parent, LoggerContext loggerContext) {
  this.name = name;
  this.parent = parent;
  this.loggerContext = loggerContext;
}

看到第一個參數(shù)就是Root的name,而這個Logger.ROOT_LOGGER_NAME的定義為final public String ROOT_LOGGER_NAME = "ROOT",由此可以看出<root>節(jié)點的name就是"ROOT"。

接著寫一段代碼來測試一下:

public class Slf4jTest {

  @Test
  public void testSlf4j() {
    Logger logger = LoggerFactory.getLogger(Object.class);
    logger.trace("=====trace=====");
    logger.debug("=====debug=====");
    logger.info("=====info=====");
    logger.warn("=====warn=====");
    logger.error("=====error=====");
  }

}

logback.xml的配置為:

<&#63;xml version="1.0" encoding="UTF-8" &#63;>
<configuration scan="false" scanPeriod="60000" debug="false">
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </layout>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>

</configuration>

root將打印級別設(shè)置為"info"級別,<appender>暫時不管,控制臺的輸出為:

2018-03-26 22:57:48.779 [main] INFO  java.lang.Object - =====info=====
2018-03-26 22:57:48.782 [main] WARN  java.lang.Object - =====warn=====
2018-03-26 22:57:48.782 [main] ERROR java.lang.Object - =====error=====

logback.xml的意思是,當(dāng)Test方法運行時,root節(jié)點將日志級別大于等于info的交給已經(jīng)配置好的名為"STDOUT"的<appender>進行處理,"STDOUT"將信息打印到控制臺上。

接著理解一下<logger>節(jié)點的作用,logback.xml修改一下,加入一個只有name屬性的<logger>:

<&#63;xml version="1.0" encoding="UTF-8" &#63;>
<configuration scan="false" scanPeriod="60000" debug="false">

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </layout>
  </appender>

  <logger name="java" />

   <root level="debug">
     <appender-ref ref="STDOUT" />
   </root>

</configuration>

注意這個name表示的是LoggerFactory.getLogger(XXX.class),XXX的包路徑,包路徑越少越是父級,我們測試代碼里面是Object.class,即name="java"是name="java.lang"的父級,root是所有<logger>的父級??匆幌螺敵鰹椋?/p>

2018-03-27 23:02:02.963 [main] DEBUG java.lang.Object - =====debug=====
2018-03-27 23:02:02.965 [main] INFO  java.lang.Object - =====info=====
2018-03-27 23:02:02.966 [main] WARN  java.lang.Object - =====warn=====
2018-03-27 23:02:02.966 [main] ERROR java.lang.Object - =====error=====

出現(xiàn)這樣的結(jié)果是因為:

  • <logger>中沒有配置level,即繼承父級的level,<logger>的父級為<root>,那么level=debug
  • 沒有配置additivity,那么additivity=true,表示此<logger>的打印信息向父級<root>傳遞
  • 沒有配置<appender-ref>,表示此<logger>不會打印出任何信息

由此可知,<logger>的打印信息向<root>傳遞,<root>使用"STDOUT"這個<appender>打印出所有大于等于debug級別的日志。舉一反三,我們將<logger>的additivity配置為false,那么控制臺應(yīng)該不會打印出任何日志,因為<logger>的打印信息不會向父級<root>傳遞且<logger>沒有配置任何<appender>,大家可以自己試驗一下。

接著,我們再配置一個<logger>:

<&#63;xml version="1.0" encoding="UTF-8" &#63;>
<configuration scan="false" scanPeriod="60000" debug="false">

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </layout>
  </appender>

  <logger name="java" additivity="false" />
  <logger name="java.lang" level="warn">
    <appender-ref ref="STDOUT" />
  </logger>

  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>

</configuration>

如果讀懂了上面的例子,那么這個例子應(yīng)當(dāng)很好理解:

  • LoggerFactory.getLogger(Object.class),首先找到name="java.lang"這個<logger>,將日志級別大于等于warn的使用"STDOUT"這個<appender>打印出來
  • name="java.lang"這個<logger>沒有配置additivity,那么additivity=true,打印信息向上傳遞,傳遞給父級name="java"這個<logger>
  • name="java"這個<logger>的additivity=false且不關(guān)聯(lián)任何<appender>,那么name="java"這個<appender>不會打印任何信息

由此分析,得出最終的打印結(jié)果為:

2018-03-27 23:12:16.147 [main] WARN  java.lang.Object - =====warn=====
2018-03-27 23:12:16.150 [main] ERROR java.lang.Object - =====error=====

舉一反三,上面的name="java"這個<appender>可以把additivity設(shè)置為true試試看是什么結(jié)果,如果對前面的分析理解的朋友應(yīng)該很容易想到,有兩部分日志輸出,一部分是日志級別大于等于warn的、一部分是日志級別大于等于debug的。

<appender>

接著看一下<appender>,<appender>是<configuration>的子節(jié)點,是負(fù)責(zé)寫日志的組件。<appender>有兩個必要屬性name和class:

  • name指定<appender>的名稱
  • class指定<appender>的全限定名

<appender>有好幾種,上面我們演示過的是ConsoleAppender,ConsoleAppender的作用是將日志輸出到控制臺,配置示例為:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
   <encoder>
     <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
   </encoder>
 </appender>

其中,encoder表示對參數(shù)進行格式化。我們和上一部分的例子對比一下,發(fā)現(xiàn)這里是有所區(qū)別的,上面使用了<layout>定義<pattern>,這里使用了<encoder>定義<pattern>,簡單說一下:

  • <encoder>是0.9.19版本之后引進的,以前的版本使用<layout>,logback極力推薦的是使用<encoder>而不是<layout>
  • 最常用的FileAppender和它的子類的期望是使用<encoder>而不再使用<layout>

關(guān)于<encoder>中的格式下一部分再說。接著我們看一下FileAppender,F(xiàn)ileAppender的作用是將日志寫到文件中,配置示例為:

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
  <file>D:/123.log</file>
  <append>true</append>
  <encoder>
    <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
  </encoder>
</appender>

它的幾個節(jié)點為:

  • <file>表示寫入的文件名,可以使相對目錄也可以是絕對目錄,如果上級目錄不存在則自動創(chuàng)建
  • <appender>如果為true表示日志被追加到文件結(jié)尾,如果是false表示清空文件
  • <encoder>表示輸出格式,后面說
  • <prudent>如果為true表示日志會被安全地寫入文件,即使其他的FileAppender也在向此文件做寫入操作,效率低,默認(rèn)為false

接著來看一下RollingFileAppender,RollingFileAppender的作用是滾動記錄文件,先將日志記錄到指定文件,當(dāng)符合某個條件時再將日志記錄到其他文件,RollingFileAppender配置比較靈活,因此使用得更多,示例為:

<appender name="ROLLING-FILE-1" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>rolling-file-%d{yyyy-MM-dd}.log</fileNamePattern>
    <maxHistory>30</maxHistory>
  </rollingPolicy>
  <encoder>
    <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
  </encoder>
</appender>

這種是僅僅指定了<rollingPolicy>的寫法,<rollingPolicy>的作用是當(dāng)發(fā)生滾動時,定義RollingFileAppender的行為,其中上面的TimeBasedRollingPolicy是最常用的滾動策略,它根據(jù)時間指定滾動策略,既負(fù)責(zé)滾動也負(fù)責(zé)觸發(fā)滾動,有以下節(jié)點:

  • <fileNamePattern>,必要節(jié)點,包含文件名及"%d"轉(zhuǎn)換符,"%d"可以包含一個Java.text.SimpleDateFormat指定的時間格式,如%d{yyyy-MM},如果直接使用%d那么格式為yyyy-MM-dd。RollingFileAppender的file子節(jié)點可有可無,通過設(shè)置file可以為活動文件和歸檔文件指定不同的位置
  • <maxHistory>,可選節(jié)點,控制保留的歸檔文件的最大數(shù)量,如果超出數(shù)量就刪除舊文件,假設(shè)設(shè)置每個月滾動且<maxHistory>是6,則只保存最近6個月的文件

向其他還有SizeBasedTriggeringPolicy,用于按照文件大小進行滾動,可以自己查閱一下資料。

異步寫日志

日志通常來說都以文件形式記錄到磁盤,例如使用<RollingFileAppender>,這樣的話一次寫日志就會發(fā)生一次磁盤IO,這對于性能是一種損耗,因此更多的,對于每次請求必打的日志(例如請求日志,記錄請求API、參數(shù)、請求時間),我們會采取異步寫日志的方式而不讓此次寫日志發(fā)生磁盤IO,阻塞線程從而造成不必要的性能損耗。(不要小看這個點,可以網(wǎng)上查一下服務(wù)端性能優(yōu)化的文章,只是因為將日志改為異步寫,整個QPS就有了大幅的提高)。

接著我們看下如何使用logback進行異步寫日志配置:

<&#63;xml version="1.0" encoding="UTF-8" &#63;>
<configuration scan="false" scanPeriod="60000" debug="false">

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="ROLLING-FILE-1" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
       <fileNamePattern>D:/rolling-file-%d{yyyy-MM-dd}.log</fileNamePattern>
       <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
       <pattern>%-4relative [%thread] %-5level %lo{35} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- 異步輸出 -->
  <appender name ="ASYNC" class= "ch.qos.logback.classic.AsyncAppender">
    <!-- 不丟失日志.默認(rèn)的,如果隊列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日志 -->
    <discardingThreshold>0</discardingThreshold>
    <!-- 更改默認(rèn)的隊列的深度,該值會影響性能.默認(rèn)值為256 -->
    <queueSize>256</queueSize>
    <!-- 添加附加的appender,最多只能添加一個 -->
    <appender-ref ref ="ROLLING-FILE-1"/>
  </appender>

  <logger name="java" additivity="false" />
  <logger name="java.lang" level="DEBUG">
    <appender-ref ref="ASYNC" />
  </logger>

  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>

</configuration>

即,我們引入了一個AsyncAppender,先說一下AsyncAppender的原理,再說一下幾個參數(shù):

當(dāng)我們配置了AsyncAppender,系統(tǒng)啟動時會初始化一條名為"AsyncAppender-Worker-ASYNC"的線程當(dāng)Logging Event進入AsyncAppender后,AsyncAppender會調(diào)用appender方法,appender方法中再將event填入Buffer(使用的Buffer為BlockingQueue,具體實現(xiàn)為ArrayBlockingQueye)前,會先判斷當(dāng)前Buffer的容量以及丟棄日志特性是否開啟,當(dāng)消費能力不如生產(chǎn)能力時,AsyncAppender會將超出Buffer容量的Logging Event的級別進行丟棄,作為消費速度一旦跟不上生產(chǎn)速度導(dǎo)致Buffer溢出處理的一種方式。上面的線程的作用,就是從Buffer中取出Event,交給對應(yīng)的appender進行后面的日志推送從上面的描述我們可以看出,AsyncAppender并不處理日志,只是將日志緩沖到一個BlockingQueue里面去,并在內(nèi)部創(chuàng)建一個工作線程從隊列頭部獲取日志,之后將獲取的日志循環(huán)記錄到附加的其他appender上去,從而達(dá)到不阻塞主線程的效果。因此AsyncAppender僅僅充當(dāng)?shù)氖鞘录D(zhuǎn)發(fā)器,必須引用另外一個appender來做事。

從上述原理,我們就能比較清晰地理解幾個參數(shù)的作用了:

  • discardingThreshold,假如等于20則表示,表示當(dāng)還剩20%容量時,將丟棄TRACE、DEBUG、INFO級別的Event,只保留WARN與ERROR級別的Event,為了保留所有的events,可以將這個值設(shè)置為0,默認(rèn)值為queueSize/5
  • queueSize比較好理解,BlockingQueue的最大容量,默認(rèn)為256
  • includeCallerData表示是否提取調(diào)用者數(shù)據(jù),這個值被設(shè)置為true的代價是相當(dāng)昂貴的,為了提升性能,默認(rèn)當(dāng)event被加入BlockingQueue時,event關(guān)聯(lián)的調(diào)用者數(shù)據(jù)不會被提取,只有線程名這些比較簡單的數(shù)據(jù)
  • appender-ref表示AsyncAppender使用哪個具體的<appender>進行日志輸出

<encoder>

<encoder>節(jié)點負(fù)責(zé)兩件事情:

  • 把日志信息轉(zhuǎn)換為字節(jié)數(shù)組
  • 把字節(jié)數(shù)組寫到輸出流

目前PatternLayoutEncoder是唯一有用的且默認(rèn)的encoder,有一個<pattern>節(jié)點,就像上面演示的,用來設(shè)置日志的輸入格式,使用“%+轉(zhuǎn)換符"的方式,如果要輸出"%"則必須使用"\%"對"%"進行轉(zhuǎn)義。

<encoder>的一些可用參數(shù)用表格表示一下:

轉(zhuǎn)換符作  用是否避免使用

c{length}

lo{length}

logger{length}

輸出日志的logger名稱,可有一個整型參數(shù)來縮短<logger>名稱,有幾種情況:

1、不輸入表示輸出完整的<logger>名稱

2、輸入0表示只輸出<logger>最右邊點號之后的字符串

3、輸入其他數(shù)字表示輸出小數(shù)點最后邊點號之前的字符數(shù)量

C{length}

class{length}

輸出指定記錄的請求的調(diào)用者的全限定名,length同上

d{pattern}

date{pattern}

輸出時間格式,模式語法同java.text.SimpleDateFormat兼容
caller{depth}輸出生成日志的調(diào)用者的位置信息,整數(shù)選項表示輸出信息深度
L輸出執(zhí)行日志的請求行號

m

msg

message

輸出應(yīng)用程序提供的信息
m輸入執(zhí)行日志請求的方法名
n輸出平臺相關(guān)的分行符"\n"或者"\r\n",即換行

p

le

level

輸出日志級別

r

relative

輸出從程序啟動到創(chuàng)建日志記錄的時間,單位為毫秒

t

thread

輸出產(chǎn)生日志的線程名稱

Filter

最后來看一下<filter>,<filter>是<appender>的一個子節(jié)點,表示在當(dāng)前給到的日志級別下再進行一次過濾,最基本的Filter有ch.qos.logback.classic.filter.LevelFilter和ch.qos.logback.classic.filter.ThresholdFilter,首先看一下LevelFilter:

<configuration scan="false" scanPeriod="60000" debug="false">

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>WARN</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
  </appender>

  <logger name="java" additivity="false" />
  <logger name="java.lang" level="DEBUG">
    <appender-ref ref="STDOUT" />
  </logger>

  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>

</configuration>

看一下輸出:

2018-03-31 22:22:58.843 [main] WARN  java.lang.Object - =====warn=====

看到盡管<logger>配置的是DEBUG,但是輸出的只有warn,因為在<filter>中對匹配到WARN級別時做了ACCEPT(接受),對未匹配到WARN級別時做了DENY(拒絕),當(dāng)然只能打印出WARN級別的日志。

再看一下ThresholdFilter,配置為:

<&#63;xml version="1.0" encoding="UTF-8" &#63;>
<configuration scan="false" scanPeriod="60000" debug="false">

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>INFO</level>
    </filter>
  </appender>

  <logger name="java" additivity="false" />
  <logger name="java.lang" level="DEBUG">
    <appender-ref ref="STDOUT" />
  </logger>

  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>

</configuration>

看一下輸出為:

2018-03-31 22:41:32.353 [main] INFO  java.lang.Object - =====info=====
2018-03-31 22:41:32.358 [main] WARN  java.lang.Object - =====warn=====
2018-03-31 22:41:32.359 [main] ERROR java.lang.Object - =====error=====

因為ThresholdFilter的策略是,會將日志級別小于<level>的全部進行過濾,因此雖然指定了DEBUG級別,但是只有INFO及以上級別的才能被打印出來。

看完上述內(nèi)容,是不是對如何使用Java日志框架的logback有進一步的了解,如果還想學(xué)習(xí)更多內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。

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

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

AI