怎么打印日志?.md 19 KB

参考:

logger 使用步骤

  1. 引入依赖

    <project>
    <properties>
        <slf4j.version>1.7.25</slf4j.version>
        <logback.version>1.1.11</logback.version>
    	</properties>
    <dependencies>
        <!-- slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        
        <!-- logback -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>${logback.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-access</artifactId>
            <version>${logback.version}</version>
        </dependency>
    </dependencies>
    </project>
    
    1. 配置 logback.xml > 实际项目中,可修改下面模板使用 ```xml <?xml version="1.0" encoding="UTF-8"?>
    2. <!-- Appender: 设置日志信息的去向,常用的有以下几个

          ch.qos.logback.core.ConsoleAppender (控制台)
          ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
          ch.qos.logback.core.FileAppender (文件)
      

      --> System.out ${pattern}

      ${LOG_DIR}/info.log ${LOGDIR}/info%d{yyyy-MM-dd}.log.%i.gz ${maxFileSize} ${maxHistory} ${pattern} INFO ACCEPT DENY

      ${LOG_DIR}/error.log ${LOGDIR}/error%d{yyyy-MM-dd}.log.%i.gz ${maxFileSize} ${maxHistory} ${pattern} ERROR ACCEPT DENY

      ${LOG_DIR}/debug.log ${LOG_DIR}/debug.%d{yyyy-MM-dd}.log ${pattern} DEBUG ACCEPT DENY

      <!--

      -->

    ```

  2. 打印日志

    public class LogGuideApplication {
    
    private static Logger logger = LoggerFactory.getLogger(LogGuideApplication.class);
    
    public static void main(String[] args) {
        logger.trace("trace ...");
        logger.debug("debug ...");
        logger.info("info ...");
        logger.warn("warn ...");
        logger.error("error ...");
    }
    }
    

    logger 配置

    logback.xml 的结构: ```

configuration

|- property
|- appender
|- logger
|- root

**第1种情况:只配置root**
```xml
<configuration>
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>%d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %msg%n</pattern>
		</encoder>
	</appender>
	<!-- 将 >= INFO 级别的日志信息,交给 STDOUT 处理-->
	<root level="INFO">
		<appender-ref ref="STDOUT"/>
	</root>
</configuration>

结果:控制台只会打印大于等于 INFO 级别的日志信息

第2种情况:配置一个logger

<configuration>
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>%d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %msg%n</pattern>
		</encoder>
	</appender>
    
    <logger name="liu.log.guide">
		<appender-ref ref="STDOUT"/>
	</logger>
    
	<!-- 将 >= INFO 级别的日志信息,交给 STDOUT 处理-->
	<root level="INFO">
		<appender-ref ref="STDOUT"/>
	</root>
</configuration>

logger 有3个属性:

  • name 日志范围,包名或者类名。root没有name属性,默认根目录。日志范围会优先匹配最精确的包名。 比如说上面这个例子,在没有name="liu.log.guide"的logger之前,是由root记录所有类包日志的。有了logger之后,包"liu.log.guide"里的日志会交给最精确匹配的logger来记录。
  • level 日志级别,默认继承上级的日志级别。用来筛选不同日志级别的日志信息。
  • additivity 是否将日志信息传递给上级,默认为true

上面的情况会发生如下过程:

  1. logger 默认继承了上级root的level,所以会记录所有日志级别level大于等于INFO的日志信息。
  2. logger 将筛选后的日志信息交给appender处理。除此之外,additivity属性默认true,所以还会将日志信息传递给上级logger,即root
  3. root 收到下级logger传递来的日志信息后,将其全部交给自己的appender处理
  4. appender 收到日志信息后,如果设置了levelFilter日志级别过滤器,就会只将过滤后的日志信息输出到目的地。 > logger上下级关系使有name表示的包或类决定的。比如 name="liu.log" 就是 name="liu.log.guide" 的上级

所以最明显的结果就是:日志级别大于等于INFO的日志都会被记录2次,这2次分别来自于logger和 root

扩展 如果将上面的logback.xml改为如下这样,会发生什么呢?

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

appender

参考 https://www.cnblogs.com/cb0327/archive/2004/01/13/5770794.html

自定义 appender

本身logback提供了AppenderBase和UnsynchronizedAppenderBase两个抽象类(同步和异步),所以我们自定义时,只需要看实际业务继承其中的一个即可。

  1. 自定义appender类

    public class MyDbAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
    
    Layout<ILoggingEvent> layout;
    
    //自定义配置
    String printString;
    
    @Override
    public void start(){
        //这里可以做些初始化判断 比如layout不能为null ,
        if(layout == null) {
            addWarn("Layout was not defined");
        }
        //或者写入数据库 或者redis时 初始化连接等等
        super.start();
    }
    @Override
    public void stop() {
        //释放相关资源,如数据库连接,redis线程池等等
        System.out.println("logback-stop方法被调用");
        if(!isStarted()) {
            return;
        }
        super.stop();
    }
    @Override
    public void append(ILoggingEvent event) {
        if (event == null || !isStarted()){
            return;
        }
        // 此处自定义实现输出
        // 获取输出值:event.getFormattedMessage()
        // System.out.print(event.getFormattedMessage());
        // 格式化输出
        System.out.print(printString + ":" + layout.doLayout(event));
    
    }
    // 省略 setter 和 getter 
    }
    
    1. logback配置 ```xml INFO ${pattern} 刘传伟(logback)
    2. <appender-ref ref="MyDbAppender"/>
      

      ```

在 springboot-log-guide 中会用spring 继承logback将日志写入到数据库中

参考:https://www.cnblogs.com/zimublog/p/12923786.html

@Component
public class MyLogDbAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {

    @Autowired
    private BusinessLogService businessLogService;

    @PostConstruct
    public void init() {
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        ThresholdFilter filter = new ThresholdFilter();
        filter.setLevel("ERROR");
        filter.setContext(context);
        filter.start();
        this.addFilter(filter);
        this.setContext(context);

        // 定义 logger (和在 logback.xml 中的配置是对应的)
        Logger logger = context.getLogger("liu.log.guide");
        logger.setLevel(Level.DEBUG);
        logger.setAdditive(false);
        logger.addAppender(MyLogDbAppender.this);
        // 定义要有下面这句
        super.start();
    }

    @Override
    public void append(ILoggingEvent event) {
        if (event == null || !isStarted()) {
            return;
        }
        // TODO 根据实际情况替换
        BusinessLog businessLog = new BusinessLog();
        businessLog.setLevel(event.getLevel().levelStr);
        businessLog.setClassPath(event.getLoggerName());
        businessLog.setMessage(event.getMessage());
        businessLog.setCreateBy(1L);
        businessLog.setCreateDate(new Date());
        try {
            businessLogService.add(businessLog);
        } catch (Exception e) {
            this.addError("上报错误日志失败:" + e.getMessage());
        }
    }
}

对比 log4j.properties

参考:https://blog.csdn.net/drift_away/article/details/7403658

log4j.rootLogger=DEBUG,CONSOLE,A1,im 
#DEBUG,CONSOLE,FILE,ROLLING_FILE,MAIL,DATABASE
################### 
# Console Appender 
################### 
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 
log4j.appender.Threshold=DEBUG 
log4j.appender.CONSOLE.Target=System.out 
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout 
log4j.appender.CONSOLE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n 
#log4j.appender.CONSOLE.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[THREAD] n%c[CATEGORY]%n%m[MESSAGE]%n%n
##################### 
# File Appender 
##################### 
log4j.appender.FILE=org.apache.log4j.FileAppender 
log4j.appender.FILE.File=file.log 
log4j.appender.FILE.Append=false 
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout 
log4j.appender.FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n 
# Use this layout for LogFactor 5 analysis


打印日志的最佳实践

参考:别再乱打日志了,这份 Java 日志规范,应有尽有,建议收藏!!

占位符

slf4j logger 占位符非常简单,并没有类型的区别

String name = "中国";
logger.info("我的名字叫{}", name);

isDebugEnabled

参考:Java日志框架中需要判断log.isDebugEnabled()吗?

很多代码中都用到了 isDebugEnabled ,如下:

if (logger.isTraceEnabled()) {  
   logger.trace("Detected " + this.multipartResolver);  
}  
else if (logger.isDebugEnabled()) {  
   logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());  
}

看源码可知,isDebugEnabled() 用来判断当前日志级别是否低于debug,比如info级别高于debug,则不会打印日志。

public boolean isDebugEnabled(Marker marker) {
	final FilterReply decision = callTurboFilters(marker, Level.DEBUG);
	if (decision == FilterReply.NEUTRAL) {
		return effectiveLevelInt <= Level.DEBUG_INT;
	} else {
		throw new IllegalStateException("Unknown FilterReply value: " + decision);
	}
}

为什么要加if判断? 如果不加if判断,即使最终不需要debug日志输出,编译时也会自动执行字符串拼接,多少会消耗一部分性能。 而加上if判断之后,就会跳过if条件内的代码

//代码1
logger.debug("这是一条debug日志:" + msg);
//代码2
if(logger.isDebugEnabled()) {
	logger.debug("这是一条debug日志:" + msg);
}

网上有部分人说,不需要加if判断,使用占位符即可。

logger.debug("这是一条debug日志:{}", msg);

但在某些情况下,占位符依然不能避免性能消耗。

logger.debug("这是一条debug日志:{}", obj.toString());

如上面这段代码,最终不会打印debug日志,但是会执行 obj.toString()

最佳实践

==原则一:如果打印字符串常量,不需要isDebugEnabled==

比较下面两段代码:

//代码1
logger.debug("hello, world");
//代码2
if(logger.isDebugEnabled()){
    logger.debug("hello, world");
}

因为打印的日志是字面常量,没有计算逻辑。两段代码的性能是几乎一样的。添加isDebugEnabled反而会导致额外的代码。

==原则二:如果有参数,且参数只是字符串常量或计算简单,使用占位符==

考虑如下代码,debug方法包含了参数user.getName()。虽然执行debug方法时,会计算user.getName(),但只是一个简单的get方法,没有复杂计算,这时候,也可以不添加isDebugEnabled

logger.debug("hello, {}", user.getName());

==原则三:如果有参数,且参数计算复杂,添加isDebugEnabled==

logger.debug("order price: {}", calculatePrice());

假设calculatePrice方法需要经过复杂计算。那么就应该添加isDebugEnabled判断,使用如下的代码:

if(logger.isDebugEnabled()){
    logger.debug("order price: {}", calculatePrice());
}