乐趣区

关于后端:Java日志框架浅析

前段时间 Log4j 爆出相干平安问题,各大公司都在紧急修复该问题,我所处的公司也不例外。但在排查相干问题时,发现对 Java 日志框架整体的认知还是比拟乱的,于是乎,就收罗各大文章对其进行理解并记录,以备后续温习或用之。

Java 日志历史

  1. 最开始 Java 利用都是应用 System.out/err.println() 的形式来跟踪本人的程序运行状况;
  2. 1996 年,E.U.SEMPER(欧洲平安电子市场)我的项目编写己的跟踪 API,该 API 演变为Log4j,次要作者就有:CekiGülcü,后该我的项目退出了 Apache 基金会我的项目;
  3. 2002 年 2 月,Java1.4 公布,Sun 推出了本人的日志库:JUL(Java Util Logging)
  4. 2002 年 8 月,Apache 推出了日志接口 JCL(Jakarta Commons Logging)(日志形象层);
  5. 因为一些起因,CekiGülcü来到了 Apache,在 2005 年的时候推出了一套新的日志接口Slf4j(Simple Logging Facade for Java)
  6. 因为应用 Slf4j,之前的日志产品都不是正统的Slf4j 的实现,在 2006 年的时候,CekiGülcü又撸了一套日志产品LogBack,且是完满实现了Slf4j
  7. Slf4j+LogBack的模式冲击了之前的 JCL+Log4j 的模式,Apache 就在 2012 年推出了本人的新我的项目Log4j2,该我的项目是齐全不兼容 Lg4j1.x 的。

    适配模式

    后面咱们提到,Slf4j 的呈现晚于 JUL、JCL、log4j 等日志框架,所以,这些日志框架也不可能就义掉版本兼容性,将接口革新成合乎 Slf4j 接口标准。Slf4j 也当时思考到了这个问题,所以,它不仅仅提供了对立的接口定义,还提供了针对不同日志框架的适配器(slf4j-log4j12/slf4j-jdk14);´然而其实之前很多 Java 利用应该依赖的 JCL,所以光有日志产品适配包还不够,于是有了日志规范的适配包(slf4j-jcl)。

定义

顾名思义,适配模式就是用做适配的,它将不兼容的接口转换为可兼容的接口,让本来因为接口不兼容而不能一起工作的类能够一起工作。

实现形式

ITarget 示意要转化成的接口定义。Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组合乎 ITarget 接口定义的接口。

类适配器

// 类适配器: 基于继承
public interface ITarget {void f1();
  void f2();
  void fc();}

public class Adaptee {public void fa() {//...}
  public void fb() { //...}
  public void fc() { //...}
}

public class Adaptor extends Adaptee implements ITarget {public void f1() {super.fa();
  }
  
  public void f2() {//... 从新实现 f2()...
  }
  
  // 这里 fc()不须要实现,间接继承自 Adaptee,这是跟对象适配器最大的不同点}

对象适配器

// 对象适配器:基于组合
public interface ITarget {void f1();
  void f2();
  void fc();}

public class Adaptee {public void fa() {//...}
  public void fb() { //...}
  public void fc() { //...}
}

public class Adaptor implements ITarget {
  private Adaptee adaptee;
  
  public Adaptor(Adaptee adaptee) {this.adaptee = adaptee;}
  
  public void f1() {adaptee.fa(); // 委托给 Adaptee
  }
  
  public void f2() {//... 从新实现 f2()...
  }
  
  public void fc() {adaptee.fc();
  }
}

具体应用哪种有两者判断规范

  • Adaptee 接口的个数
  • Adaptee 和 ITarget 的符合水平

若接口个数不多则两种实现形式都能够;否则则判断适配的两者之间的接口定义是否大部分都雷同,若雷同,则能够应用类适配器(基于继承)这样能够复用代码,缩小代码的冗余性;若少部分雷同,则能够应用对象适配器(基于组合)这样能够让代码更灵便。

Tips:代理模式在不扭转原始类接口的条件下,为原始类定义一个代理类,次要目标是管制拜访,而非增强性能,这是它跟装璜器模式最大的不同。

Slf4j 中的实现计划举例

// slf4j 对立的接口定义
package org.slf4j;
public interface Logger {public boolean isTraceEnabled();
  public void trace(String msg);
  public void trace(String format, Object arg);
  public void trace(String format, Object arg1, Object arg2);
  public void trace(String format, Object[] argArray);
  public void trace(String msg, Throwable t);
 
  public boolean isDebugEnabled();
  public void debug(String msg);
  public void debug(String format, Object arg);
  public void debug(String format, Object arg1, Object arg2)
  public void debug(String format, Object[] argArray)
  public void debug(String msg, Throwable t);

  //... 省略 info、warn、error 等一堆接口
}

// log4j 日志框架的适配器
// Log4jLoggerAdapter 实现了 LocationAwareLogger 接口,// 其中 LocationAwareLogger 继承自 Logger 接口,// 也就相当于 Log4jLoggerAdapter 实现了 Logger 接口。package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase
  implements LocationAwareLogger, Serializable {
  final transient org.apache.log4j.Logger logger; // log4j
 
  public boolean isDebugEnabled() {return logger.isDebugEnabled();
  }
 
  public void debug(String msg) {logger.log(FQCN, Level.DEBUG, msg, null);
  }
 
  public void debug(String format, Object arg) {if (logger.isDebugEnabled()) {FormattingTuple ft = MessageFormatter.format(format, arg);
      logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
  }
 
  public void debug(String format, Object arg1, Object arg2) {if (logger.isDebugEnabled()) {FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
      logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
  }
 
  public void debug(String format, Object[] argArray) {if (logger.isDebugEnabled()) {FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
      logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
    }
  }
 
  public void debug(String msg, Throwable t) {logger.log(FQCN, Level.DEBUG, msg, t);
  }
  //... 省略一堆接口的实现...
}

桥接模式

然而,还有一种状况,比方援用的是第三方框架用的是 JCL,并且最终应用 JUL 打印日志,然而你的零碎应用的是 Slf4j,最终应用 Log4j 打印,那将会有两种输入,两种日志的输入环境不对立,但时候看日志的时候很麻烦,而后开发 Slf4j 的开发者为了让大家对立应用 Slf4j,就又撸出了对应的桥接包(jul-to-slf4j/log4j-over-slf4j/jcl-over-slf4j)。

定义

将形象和实现解耦,让它们能够独立变动。定义中的“形象”,指的并非“抽象类”或“接口”,而是被形象进去的一套“类库”,它只蕴含骨架代码,真正的业务逻辑须要委派给定义中的“实现”来实现。而定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。“形象”和“实现”独立开发,通过对象之间的组合关系,组装在一起。

最佳实际

日志级别

  • TRACE【跟踪函数调用,不存在变量参数

    • 个别跟踪的是函数的调用,并且 TRACE 不应该含有变量参数,而仅能提醒函数的调用关系。
  • DEBUG【调试应用程序,存在变量参数

    • 个别用于细粒度级别上,对调试应用程序十分有帮忙,次要用于开发过程中打印一些运行信息。
  • INFO【利用程序运行过程,防止过多

    • INFO 音讯在粗粒度级别上突出强调应用程序的运行过程。打印一些你感兴趣的或者重要的信息,这个能够用于生产环境中输入程序运行的一些重要信息,然而不能滥用,防止打印过多的日志。
  • WARN【合乎预期潜在的谬误或提醒

    • WARN 示意会呈现潜在谬误的情景,有些信息不是错误信息,然而也要给程序员一些提醒。该级别示意程序会主动调整到失常的状态,相似参数未传入,应用了默认的参数,仍合乎程序员预期之内的状况。
  • ERROR【呈现谬误,仍能运行

    • ERROR 指出尽管产生谬误事件,但依然不影响零碎的持续运行。打印谬误和异样信息,如果不想输入太多的日志,能够应用这个级别。个别在 WARN 之后的级别在打印谬误时,应该同时打印错误码。
  • FATEL【呈现谬误,不能运行

    • FATAL 指出每个重大的谬误事件将会导致应用程序的退出,这个级别比拟高,重大谬误,程序无奈复原,必须通过重启程序来解决。

    在 Log4j 中,日志级别的关系如下所示:
    ALL<TRACE<DEBUG<INFO<WARN<ERROR<FATAL<OFF
    设置了对应的级别之后,日志框架就只调用大于等于这个级别的办法。Log4j 倡议只应用如下的四个界别:
    DEBUG<INFO<WARN<ERROR

日志性能

//  记录 DEBUG 日志,并设置只记录 >=INFO 级别的日志

private String slowString(String s) {System.out.println("slowString called via" + s);
    try {TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) { }
    return "OK";
}


StopWatch stopWatch = new StopWatch();
stopWatch.start("debug1");
log.debug("debug1:" + slowString("debug1"));
stopWatch.stop();

stopWatch.start("debug2");
log.debug("debug2:{}", slowString("debug2"));
stopWatch.stop();

stopWatch.start("debug3");
if (log.isDebugEnabled())
    log.debug("debug3:{}", slowString("debug3"));
stopWatch.stop();



@Log4j2
public class LoggingController {
...
log.debug("debug4:{}", ()->slowString("debug4"));
}

测试如图所示:

间接拼接字符串,占位符都会输入日志,当时判断日志级别或通过 lambda 表达式(Log4j2 日志 API)不会输入日志,缩小性能影响。

日志配置

  • 日志须要写入磁盘上的日志文件中,所以适当的应用滚动日志并且定时革除旧文件
  • 有的时候日志须要保留一点工夫,不便排查问题时追溯。
<configuration debug="false" scan="false">
  <springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue=""/>
  <property name="log.path" value="logs/${spring.application.name}"/>
  <!-- 黑白日志格局 -->
  <property name="CONSOLE_LOG_PATTERN"
    value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:-}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
  <!-- 黑白日志依赖的渲染类 -->
  <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
  <conversionRule conversionWord="wex"
    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
  <conversionRule conversionWord="wEx"
    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
  <!-- Console log output -->
  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>${CONSOLE_LOG_PATTERN}</pattern>
    </encoder>
  </appender>

  <!-- Log file debug output -->
  <appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${log.path}/debug.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
      <maxFileSize>50MB</maxFileSize>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- Log file error output -->
  <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${log.path}/error.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
      <maxFileSize>50MB</maxFileSize>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>ERROR</level>
    </filter>
  </appender>

  <!-- Level: FATAL 0  ERROR 3  WARN 4  INFO 6  DEBUG 7 -->
  <root level="INFO">
    <appender-ref ref="console"/>
    <appender-ref ref="debug"/>
    <appender-ref ref="error"/>
  </root>
</configuration>

总结

参考链接

13 | 日志:日志记录真没你设想的那么简略
51 | 适配器模式:代理、适配器、桥接、装璜,这四个模式有何区别?
Java 日志零碎历史从入门到解体
这份 Java 日志格局标准,拿走不谢!

退出移动版