参考: * [工作3年啦,你竟然连Java日志体系都没闹懂?说不过去~ ](https://mp.weixin.qq.com/s/8gTsdstUqHdyjLvRkv7nqw) * https://www.cnblogs.com/cb0327/p/5759441.html ## logger 使用步骤 1. 引入依赖 ```xml 1.7.25 1.1.11 org.slf4j slf4j-api ${slf4j.version} ch.qos.logback logback-core ${logback.version} ch.qos.logback logback-classic ${logback.version} ch.qos.logback logback-access ${logback.version} ``` 2. 配置 logback.xml > 实际项目中,可修改下面模板使用 ```xml System.out ${pattern} ${LOG_DIR}/info.log ${LOG_DIR}/info_%d{yyyy-MM-dd}.log.%i.gz ${maxFileSize} ${maxHistory} ${pattern} INFO ACCEPT DENY ${LOG_DIR}/error.log ${LOG_DIR}/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 ``` 3. 打印日志 ```java 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 %d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %msg%n ``` 结果:控制台只会打印大于等于 INFO 级别的日志信息 **第2种情况:配置一个logger** ```xml %d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %msg%n ``` 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改为如下这样,会发生什么呢? ```xml %d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %msg%n ``` ## appender 参考 https://www.cnblogs.com/cb0327/archive/2004/01/13/5770794.html ### 自定义 appender > 本身logback提供了AppenderBase和UnsynchronizedAppenderBase两个抽象类(同步和异步),所以我们自定义时,只需要看实际业务继承其中的一个即可。 1. 自定义appender类 ```java public class MyDbAppender extends UnsynchronizedAppenderBase { Layout 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 } ``` 2. logback配置 ```xml INFO ${pattern} 刘传伟(logback) ``` > 在 springboot-log-guide 中会用spring 继承logback将日志写入到数据库中 参考:https://www.cnblogs.com/zimublog/p/12923786.html ```java @Component public class MyLogDbAppender extends UnsynchronizedAppenderBase { @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 ```properties 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 日志规范,应有尽有,建议收藏!!](https://mp.weixin.qq.com/s/UqvMC6t39YTPDa_FqrkbaA) ## 占位符 slf4j logger 占位符非常简单,并没有类型的区别 ```java String name = "中国"; logger.info("我的名字叫{}", name); ``` ## isDebugEnabled 参考:[Java日志框架中需要判断log.isDebugEnabled()吗?](https://www.cnblogs.com/leiwei/p/14674143.html) 很多代码中都用到了 isDebugEnabled ,如下: ```java if (logger.isTraceEnabled()) { logger.trace("Detected " + this.multipartResolver); } else if (logger.isDebugEnabled()) { logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName()); } ``` 看源码可知,isDebugEnabled() 用来判断当前日志级别是否低于debug,比如info级别高于debug,则不会打印日志。 ```java 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条件内的代码 ```java //代码1 logger.debug("这是一条debug日志:" + msg); //代码2 if(logger.isDebugEnabled()) { logger.debug("这是一条debug日志:" + msg); } ``` 网上有部分人说,不需要加if判断,使用占位符即可。 ```java logger.debug("这是一条debug日志:{}", msg); ``` 但在某些情况下,占位符依然不能避免性能消耗。 ```java logger.debug("这是一条debug日志:{}", obj.toString()); ``` 如上面这段代码,最终不会打印debug日志,但是会执行 obj.toString() ### 最佳实践 ==**原则一:如果打印字符串常量,不需要`isDebugEnabled`**== 比较下面两段代码: ```java //代码1 logger.debug("hello, world"); //代码2 if(logger.isDebugEnabled()){ logger.debug("hello, world"); } ``` 因为打印的日志是字面常量,没有计算逻辑。两段代码的性能是几乎一样的。添加`isDebugEnabled`反而会导致额外的代码。 ==**原则二:如果有参数,且参数只是字符串常量或计算简单,使用占位符**== 考虑如下代码,`debug`方法包含了参数`user.getName()`。虽然执行`debug`方法时,会计算`user.getName()`,但只是一个简单的**get**方法,没有复杂计算,这时候,也可以不添加`isDebugEnabled`。 ```java logger.debug("hello, {}", user.getName()); ``` ==**原则三:如果有参数,且参数计算复杂,添加`isDebugEnabled`**== ```java logger.debug("order price: {}", calculatePrice()); ``` 假设`calculatePrice`方法需要经过复杂计算。那么就应该添加`isDebugEnabled`判断,使用如下的代码: ```java if(logger.isDebugEnabled()){ logger.debug("order price: {}", calculatePrice()); } ```