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

适配器模式

适配器模式的英文翻译是 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中有很多日志框架,在项目开发中,我们常常用它们来打印日志信息。其中,比较常用的有log4j、logback,以及JDK提供的 JUL(java.util.logging) 和Apache的JCL(Jakarta Commons Logging)等。

大部分日志框架都提供了相似的功能,比如按照不同级别(debug、info、warn、error)打印日志等,但它们却并没有实现统一的接口。这主要可能是历史的原因,它不像JDBC那样,一开始就制定了数据库操作的接口规范。

如果我们只是开发一个自己用的项目,那用什么日志框架都可以,log4j、logback随便选一个就好。但是,如果我们开发的是一个集成到其他系统的组件、框架、类库等,那日志框架的选择就没那么随意了。

当前SpringBoot默认日志是较新的logback日志,但是在以前流行的日志却是log4j。再比如,项目中用到的某个组件使用log4j来打印日志,而我们项目本身使用的是logback。将组件引入到项目之后,我们的项目就相当于有了两套日志打印框架。每种日志框架都有自己特有的配置方式。所以,我们要针对每种日志框架编写不同的配置文件(比如,日志存储的文件地址、打印日志的格式)。如果引入多个组件,每个组件使用的日志框架都不一样,那日志本身的管理工作就变得非常复杂。所以,为了解决这个问题,我们需要统一日志打印框架。

在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。

关于Java日志体系的更详细解析,可参见从一个Logger异常开始梳理Java日志体系这篇博客。

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

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

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值