了解工程:日志技术


了解工程:日志技术

什么是日志?

在工程中,往往会遇到各种问题,如果在写代码时,蹦出来一个错误可是很难受的,往往一个两个需要debug的或许说直接打印在控制台上就好了。问题是,在上线以前,调试的那些日志,在上线前是要被删除的,如果不删会影响项目的性能效率。

用于说明系统实时运行状态的信息,在很早以前则是使用System.out.println(),该方法是一种最低级的日志方式,之后出现了相关日志框架,以及日志门面(更好对日志框架进行管理)。

日志门面(抽象层)概念

通常对于简单的项目,我们会使用单独的日志门面进行实现日志相关操作,随着开发的进行,会更新迭代使用不同的日志框架,此时若是一开始使用单个日志实现框架,再使用其他日志框架时难以统一与管理,造成日志体系的混乱。此时我们需要借鉴JDBC的思想,为日志系统提供一套门面,通过面向接口规范来进行开发,避免了直接依赖具体的日志框架,可轻松切换不同的日志实现框架并且不需要改动代码,这就是日志门面存在的意义。

日志实现(实现层)概念

单一存在的日志框架。

日志的实现有几种:

  1. 最简单的System.out.println,仅适用于调试程序,不容易维护。

  2. 自己写框架,这要实现很多需求,例如:异步写入,自动归档,日志分级。

  3. 使用一个已经存在的框架。例如JUL、Log4j、Logback、Log4j2(其本身也提供日志门面接口),目前主流使用slf4j+logback,未来趋势会使用slf4j+log4j2

相关的日志门面与日志实现

目前市面上的日志框架

日志门面(抽象层) 日志实现(实现层)
JCLSLF4jJboss-logging Log4jJUL(java.util.logging)Log4j2Logback

日志门面

  • JCL:当时设计时只考虑了主流的几个日志框架,而对于未来新兴框架并没有提供接口,之后随着slf4j出现就慢慢也不再使用了,最后一次版本更新停在2014年,现不再更新维护已被淘汰,不考虑使用。
  • slf4j:能够统一管理所有的日志API,优秀的门面技术,并且其能够支持未来出现的新的日志框架系统并提供日志接口。日志实现中的log4jlogback实现者与slf4j都是同一个人,Spring Boot推荐slf4j+logback,未来主流会是slf4j+log4j2

日志实现:具体的日志功能实现

  • JUL:JDK自带的日志实现依赖。
  • logback:第三方的,Spring Boot默认推荐,搭配slf4j。
  • log4j:Apache推出的,之后出现了Logback(性能更好),就开始慢慢被Logback取代了。
  • log4j2:Apache根据Logback的设计思想推出了Log4j2,号称日志性能最好的实现技术,其本身也有日志门面只不过大多使用slf4j来作为日志门面。

历史

日志框架出现的历史顺序: log4j –>JUL–>JCL–> slf4j–> logback –> log4j2

首先出现的是Log4j(Apache推出),JUL是在jdk1.4的时候出现的,JCL门面日志出现开始统一管理主流的日志框架,之后Log4j的创造者因为与Apache有些矛盾离开了Apache,开发出了Slf4j日志门面(能够管理所有的日志API,让JCL开始逐渐淘汰),之后也是那个创造者设计实现了Logback日志框架(性能优于Log4j,Log4j也开始逐渐淘汰),之后Apache组织根据Logback的设计思想推出了Log4j2,号称为目前性能最好的日志,其自身既包含日志门面也包含日志实现只不过一般不使用其日志门面(即接口)。

日志门面与日志实现的关系

日志门面与日志实现关系图

日志桥接(适配层)

日志框架通常分为日志门面(抽象层)、日志实现(实现层),如果我们需要进行升级更新,则只需要修改实现层的内容,问题来了,如果抽象层也需要更新怎么办?。

这种时候,我们可以写一个适配层,它本身也算一个抽象层,通过适配层实现抽象层的转换。

适配器模式的英文翻译是 Adapter Design Pattern。顾名思义,这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。对于这个模式,有一个经常被拿来解释它的例子,就是USB转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。

适配层,抽象层

原理很简单,适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。两种模式的类图如下:

具体实现的示例代码如下。

// 抽象层
public interface Target {
  void operation1();
  void operation2();
}
// 实现层
public class Adaptee {
  public void fun_a() { //... }
  public void fun_b() { //... }
  public void fun_c() { //... }
}

// 类适配器: 基于继承
public class Adaptor extends Adaptee implements Target {

  @Override
  public void operation1() {
    super.fun_a();
  }
  
  @Override
  public void operation2() {
    //...重新实现fun_b()...
  }
  // 这里fun_c()不重写,直接继承自Adaptee
}

// 对象适配器:基于组合
public class Adaptor implements Target {
  //组合
  private Adaptee adaptee;
  
  public Adaptor(Adaptee adaptee) {
    this.adaptee = adaptee;
  }
  
  @Override
  public void operation1() {
    //委托给Adaptee
    adaptee.fun_a(); 
  }
  
  @Override
  public void operation2() {
    //...重新实现fun_b()...
  }
  
  public void operation3() {
    adaptee.fun_c();
  }
}

类适配器与对象适配器都符合开闭原则。针对这两种实现方式,在实际的开发中,到底该如何选择使用哪一种呢?

如果Adaptee类定义的方法很多,而且Target接口定义的方法大部分都相同,那我们推荐使用类适配器,因为Adaptor复用父类Adaptee的方法,比起对象适配器的实现方式代码量要少一些。如果Adaptee类定义的方法很多,而且Target接口定义的方法大部分都不相同,此时推荐使用对象适配器,因为组合结构相对于继承更加灵活。

适配器模式应用到Java日志体系

在Java的日志体系中,Slf4j这个框架就相当于JDBC规范,它提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback等)来使用。

Slf4j 的出现晚于JUL、JCL、log4j等日志框架,所以,这些日志框架也不可能牺牲掉版本兼容性,将接口改造成符合Slf4j接口规范。Slf4j也事先考虑到了这个问题,所以,它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。对不同日志框架的接口进行二次封装,适配成统一的Slf4j接口定义。

// slf4j统一的Logger接口定义,日志门面(抽象层)
package org.slf4j;
public interface Logger {
    final public String ROOT_LOGGER_NAME = "ROOT";
    public String getName();
    public boolean isInfoEnabled();
    public void info(String msg);
    public void info(String format, Object arg);
    public void info(String format, Object arg1, Object arg2);
    public void info(String format, Object... arguments);
    public void info(String msg, Throwable t);
    public boolean isInfoEnabled(Marker marker);
    public void info(Marker marker, String msg);
    public void info(Marker marker, String format, Object arg);
    public void info(Marker marker, String format, Object arg1, Object arg2);
    public void info(Marker marker, String format, Object... arguments);
    public void info(Marker marker, String msg, Throwable t);

    public boolean isErrorEnabled();
    public void error(String msg);
    public void error(String format, Object arg);
    public void error(String format, Object arg1, Object arg2);
    public void error(String format, Object... arguments);
    public void error(String msg, Throwable t);
    public boolean isErrorEnabled(Marker marker);
    public void error(Marker marker, String msg);
    public void error(Marker marker, String format, Object arg);
    public void error(Marker marker, String format, Object arg1, Object arg2);
    public void error(Marker marker, String format, Object... arguments);
    public void error(Marker marker, String msg, Throwable t);
    //...省略trace、warn等众多方法
}

Slf4j提供的log4j日志框架适配器源码如下,其中Log4jLoggerAdapter实现了LocationAwareLogger接口,而其中LocationAwareLogger继承了Logger接口。

// log4j日志框架的适配器
package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase
  implements LocationAwareLogger, Serializable {
  // log4j
  final transient org.apache.log4j.Logger logger; 
  static final String FQCN = Log4jLoggerAdapter.class.getName();
  final boolean traceCapable;

  Log4jLoggerAdapter(Logger logger) {
      this.logger = logger;
      this.name = logger.getName();
      this.traceCapable = this.isTraceCapable();
  }
 
  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);
  }
  //...省略其他接口的实现...
}

在开发业务系统或者开发框架、组件的时候,我们统一使用Slf4j提供的接口来编写打印日志的代码,具体使用哪种日志框架实现(log4j、logback等),是可以动态地指定的(使用Java的SPI技术),只需要将相应的SDK导入到项目中即可

但是,如果一些老的项目没有使用 Slf4j,而是直接使用比如JCL来打印日志,那如果想要替换成其他日志框架,比如log4j,该怎么办呢?实际上,Slf4j不仅仅提供了从其他日志框架到Slf4j的适配器,还提供了反向适配器,也就是从Slf4j到其他日志框架的适配。我们可以先将JCL切换为Slf4j,然后再将Slf4j切换为log4j。经过两次适配器的转换,我们就能成功将JCL切换成了log4j。

Spring Boot中使用SLF4j + LogBack

日志分级

  • trace
  • debug
  • info
  • warn
  • error

trace < debug < info < warn < error

缺省的log级别是info

yaml设置log级别:

logging:
  level:
    cn:
      silvercorridors: trace

日志的位置

缺省打在console。

yaml设置日志位置:

logging:
  file:
    path: D:\logs\springlog
    name: D:\logs\springlog\a.log   # 如果name设置了,path就失效了,name中给出路径+文件名

日志的格式

在logging.pattern.console设置

%d:日期,可以控制格式
%n:回车
%thread:线程运行时的函数名
%level:log级别

在%后单词加数字、小数点(.)(超位截取)、负号(-)表示制表对齐

LogBack配置

org.springframework.boot.logging.logback中的base.xml

<included>
   <include resource="org/springframework/boot/logging/logback/defaults.xml" />
   <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
   <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
   <include resource="org/springframework/boot/logging/logback/file-appender.xml" />
   <!-- 缺省日志等级 -->
   <root level="INFO">
      <appender-ref ref="CONSOLE" />
      <appender-ref ref="FILE" />
   </root>
</included>

defaults.xml

<included>
   <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的样式 -->
   <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-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}}"/>
   <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
   <!-- file中缺省log的样式 -->
    <property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
   <property name="FILE_LOG_CHARSET" value="${FILE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
    <!--定义一些包的默认日志等级-->
   <logger name="org.apache.catalina.startup.DigesterFactory" level="ERROR"/>
   <logger name="org.apache.catalina.util.LifecycleBase" level="ERROR"/>
   <logger name="org.apache.coyote.http11.Http11NioProtocol" level="WARN"/>
   <logger name="org.apache.sshd.common.util.SecurityUtils" level="WARN"/>
   <logger name="org.apache.tomcat.util.net.NioSelectorPool" level="WARN"/>
   <logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="ERROR"/>
   <logger name="org.hibernate.validator.internal.util.Version" level="WARN"/>
   <logger name="org.springframework.boot.actuate.endpoint.jmx" level="WARN"/>
</included>

SLF4j使用

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(HelloWorld.class);
        logger.info("Hello World");
    }
}

SLF4j框架的实现层

官网图示

每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件;

遗留问题

当一个系统使用了多个框架,而这些框架又使用了不同的日志框架时,这时候我们就需要进行多日志的统一,

spring Boot(slf4j + logback)、 Spring(commons-loggin)、Hibernate(Jboss-logging)、Mybatis…

统一日志记录,即使是别的框架也要和我一起使用slf4j进行输出:

让系统中所有的日志都统一到slf4j:

  1. 将系统中的其他日志框架先排除出去;
  2. 用中间包(适配层)来替换原有的日志框架
  3. 导入slf4j其他的实现

SpringBoot日志底层依赖关系

适配器模式与装饰器模式的区别

装饰器与适配器都有一个别名叫做包装模式(Wrapper),它们看似都是起到包装一个类或对象的作用,但是使用它们的目的很不一一样。

适配器模式是一种事后的补救策略,它提供跟原始类不同的接口,适配器模式的意义是将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的。而装饰器模式不是要改变被装饰对象的接口,而是恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方式而提升性能

参考资料

适配器模式及其在Java日志体系中的应用

日志门面与日志实现框架介绍

SpringBoot之日志配置

从一个Logger异常开始梳理Java日志体系


文章作者: 银色回廊
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 银色回廊 !
评论
  目录