Java如何正确打印日志

在Java项目中,为了记录和跟踪程序的运行状态,我们通常会记录一些日志,那如何正确的打印日志呢?楼主看到很多同学还是用System.out.println()这种原始的方式,在高并发的情况下,这样玩你的程序真的会崩盘。

因为你可以看看其的源码实现是一种同步的方式,效率会比较低。除此之外,也不利于保存和查看。当然如果你说是为了方便在开发时查看日志,其实使用日志框架来记录也可以实现在控制台输出,供你在开发时使用。

 public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    } 

Log4jApache的一个开源项目,它为日志的输出提供了完美的解决方案,利用它可以在项目中高效的控制日志和归档。下面我们开看看日志输出中的级别问题。

日志的级别

我们项目中通常使用的日志级别从高到低分别为:ERRORWARNINFODEBUG。如果你定义项目的日志输出级别为INFO,则WARNERROR都可以输出,而DEBUG则不会输出。如果你是在调试阶段,可以定义输出级别为DEBUG

log4j,logback,slf4j的关系

slf4j是一种日志框架的抽象,它能使你方便的切换不同日志框架,你可以把slf4j理解为日志框架的一种抽象接口,遵循这个接口标准的话,具体的实现你可以随便切换。

log4j,logback都是提供了日志解决方案的框,其中logback也是log4j的作者们开发出来的,不过遗憾的是,他们是为了取代log4j才去开发logback的。log4j不是slf4j的原生实现,需要中间有个适配层,而logback则完全遵从slf4j的接口标准,所以logback可以提供更好的性能。

既然logback可以提供更好的性能,下面就主要介绍logback在项目中如何使用。

配置文件位置

配置文件名称为:logback.xml

spring-boot默认使用logback作为日志框架,不过都是按照默认配置来定义日志的,如果你需要覆盖默认配置,定义项目自己的配置文件,名称为:logback-spring.xml

不管是logback.xml还是spring-boot项目中的logback-spring.xml文件,它们都应该放在项目的resource目录下。

配置文件的定义

根结点configuration

<configuration scan="true" scanPeriod="60 seconds" debug="false">  
      
</configuration>  

scan属性如果设置为true,在配置文件发生变化时,会自动加载,默认时false
scanPeriod属性只有scan属性设置为true才会生效,表示扫描周期,默认为60s,注意要设置时间单位。
debugtrue时可以打印出logback内部日志信息,从而实时查看logback运行状态。

公共属性定义

<property name="APP_Name" value="hadu_test"
<property name="LOG_PATH" value="logs"/>
<property name="LOG_ARCHIVE" value="${LOG_PATH}/archive"/>
<timestamp key="timestamp-by-second" datePattern="yyyy-MM-dd HH:mm:ss"/>
<contextName>${APP_Name}</contextName>

其中property是定义变量,可以在上下文通过${LOG_PATH}这种形式来引用。timestamp是用来获取时间戳的,遵循java.txt.SimpleDateFormat的格式。contextName是用来定义上下文的,从而可以区分不同的应用程序。

子节点appender

<appender name="File-Appender" class="ch.qos.logback.core.FileAppender">
   <file>${LOG_PATH}/logfile-${timestamp-by-second}.log</file>
   <encoder>
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n</pattern>
       <outputPatternAsHeader>true</outputPatternAsHeader>
   </encoder>
</appender>

appender是负责写日志的组件,其中name定义当前appender的名称,class指定当前appender的全限定名称。

file定义了输出文件的名称,可以是绝对路径,也可以是相对路径。还可以定义append来决定是否是追加日志,默认true,表示追加。

encoder定义了日志输出的格式化信息,其中pattern定义了每一条日志输出的格式。outputPatternAsHeader决定是否将格式作为日志的头第一条输出。

日志格式化中,%d{yyyy-MM-dd HH:mm:ss.SSS}表示时间的格式化,[%thread]为产生日志的线程名称,%-5level输出日志的级别,%logger{36}输出日志的logger名称,36表示最多可以输出多少字节数,如果为0则只输出类名,如果大于1,则至少输出类的缩写路径和类名,如果不带{}限制,则输出全限定名称。%msg表示日志输出信息。

很多时候,我们希望能每天生成一个日志文件,并且之前的日志能够归档,那我们可以用下面的appender

<appender name="RollingFile-Appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
   <file>${LOG_PATH}/rollingfile.log</file>
   
   <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
       <fileNamePattern>${LOG_ARCHIVE}/rollingfile.%d{yyyy-MM-dd}.log</fileNamePattern>
       <!--归档文件保留周期,这里30表示30天,超过后最老的日志被删除-->
       <maxHistory>30</maxHistory>
       <!--定义日志最大量,超过了最老的日志将被删除,这里是10G-->
       <totalSizeCap>10240MB</totalSizeCap>
   </rollingPolicy>
   
   <encoder>
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n</pattern>
   </encoder>
</appender>

如果你项目记录的日志比较多,又不希望记录日志影响主程序的性能,不妨试试异步日志记录:

<!--异步appender日志,只需要把需要异步的appender放到Async-Appender中-->
<appender name="Async-Appender" class="ch.qos.logback.classic.AsyncAppender">
   <!-- 不丢失日志. -->
   <discardingThreshold>0</discardingThreshold>
   <!-- 更改默认的队列的大小,该值会影响性能.默认值为256 -->
   <queueSize>512</queueSize>
   <appender-ref ref="RollingFile-Appender"/>
</appender>

异步日志记录appender的原理其实很简单,其持有一个BlockingQueue的缓冲池,在接收到日志事件时,异步日志的appender会调用append方法将事件放到缓冲池,于此同时,异步日志的appender会启动一个Worker线程,从BlockingQueue缓冲池提取出来交给真正的appender去处理。上面的queueSize用来定义队列的大小。discardingThreshold是抛弃策略,默认时,如果队列的80%已满,则会丢弃TRACTDEBUGINFO级别的日志,为了不丢弃日志,可以设置为0。

子节点logger

logger是为了给特定路径下的日志设置单独的输出级别,例如:

<logger name="com.netease.kaola.gaoshengli.learnspringboot.controller" level="info">
   <!--日志记录文件的appender-->
   <!--<appender-ref ref="File-Appender"/>-->
   <!--归档文件的appender-->
   <!--<appender-ref ref="RollingFile-Appender"/>-->
   <!--选择异步方式-->
   <appender-ref ref="Async-Appender"/>
</logger>

其中name可以是一个类或者一个包,如果是包,则对包下面所有类生效。还有addtivity属性用来设置是否向上级传递日志信息,默认是true

子节点root

root字节点其实也是一种特殊的logger,普通的logger如果没有定义level这样的属性,会从root继承。

case

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
    <property name="APP_Name" value="hadu_test"/>
    <property name="LOG_PATH" value="logs"/>
    <property name="LOG_ARCHIVE" value="${LOG_PATH}/archive"/>
    <timestamp key="timestamp-by-second" datePattern="yyyy-MM-dd HH:mm:ss"/>
    <contextName>${APP_Name}</contextName>
    <appender name="Console-Appender" class="ch.qos.logback.core.ConsoleAppender">
        <layout>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{1} -%msg%n</pattern>
        </layout>
    </appender>

    <appender name="File-Appender" class="ch.qos.logback.core.FileAppender">
        <file>${LOG_PATH}/logfile-${timestamp-by-second}.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n</pattern>
            <outputPatternAsHeader>true</outputPatternAsHeader>
        </encoder>
    </appender>

    <appender name="RollingFile-Appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/rollingfile.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_ARCHIVE}/rollingfile.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--归档文件保留周期-->
            <maxHistory>1</maxHistory>
            <!--定义日志最大量,超过了最老的日志将被删除-->
            <totalSizeCap>10240MB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n</pattern>
        </encoder>
    </appender>

    <!--异步appender日志,只需要把需要异步的appender放到Async-Appender中-->
    <appender name="Async-Appender" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.-->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>512</queueSize>
        <appender-ref ref="RollingFile-Appender"/>
    </appender>

    <!--不会向上传递,IndexController的日志不会输出到root的appender-->
    <logger name="com.netease.kaola.gaoshengli.learnspringboot.controller.IndexController" level="INFO" additivity="false">
        <!--日志记录文件的appender-->
        <!--<appender-ref ref="File-Appender"/>-->
        <!--归档文件的appender-->
        <!--<appender-ref ref="RollingFile-Appender"/>-->
        <!--选择异步方式-->
        <appender-ref ref="Async-Appender"/>
    </logger>

    <!--会向上传递,包下的所有日志会输出到root的appender-->
    <!--<logger name="com.netease.kaola.gaoshengli.learnspringboot" level="INFO">-->
        <!--&lt;!&ndash;日志记录文件的appender&ndash;&gt;-->
        <!--&lt;!&ndash;<appender-ref ref="File-Appender"/>&ndash;&gt;-->
        <!--&lt;!&ndash;归档文件的appender&ndash;&gt;-->
        <!--&lt;!&ndash;<appender-ref ref="RollingFile-Appender"/>&ndash;&gt;-->
        <!--&lt;!&ndash;选择异步方式&ndash;&gt;-->
        <!--<appender-ref ref="Async-Appender"/>-->
    <!--</logger>-->

    <root level="INFO">
        <appender-ref ref="Console-Appender"/>
    </root>

    <!--<logger name="com.netease.kaola.gaoshengli.learnspringboot.controller" level="ERROR" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
        <!--<appender-ref ref="FILE"/>-->
    <!--</logger>-->
</configuration>

上面的例子是以异步的方式输出日志,com.netease.kaola.gaoshengli.learnspringboot.controller.IndexController的日志只会异步的输出到对应的异步appender,并且不会上传,所以这个类的日志不会在控制行里输出,而其他类的日志由于没有单独定义appender,都会走到root这里,都在Console-Appender输出。

以上,这篇文章应该可以让你如何在项目里正确使用日志了。

留下评论