前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2. 日志模块(上)

2. 日志模块(上)

作者头像
张申傲
发布2023-10-12 09:03:32
2050
发布2023-10-12 09:03:32
举报
文章被收录于专栏:漫漫架构路漫漫架构路

日志需求分析

无论对于业务系统还是中间件来说,日志都是必不可少的基础功能。完善、清晰地日志可以帮助我们观测系统运行的状态,并且快速定位问题。现在让我们站在 MyBatis 框架开发者的角度,来简单做一下日志功能的需求分析:

  1. 作为一个成熟的中间件,日志功能是必不可少的。那么,MyBatis 是要自己实现日志功能,还是集成现有的日志呢?MyBatis 没有选择重复造轮子,而是直接集成了第三方日志框架。
  2. 第三方的日志框架种类繁多,常用的如 slf4j、log4j2、logback 等等,而且每种框架的日志级别定义、打印方式、配置格式都不尽相同。MyBatis 作为底层的中间件,每个依赖 MyBatis 的业务系统都可能使用不同的日志组件,那 MyBatis 如何进行兼容呢?如果业务方引入了多个日志框架,MyBatis 按照什么优先级进行选择?
  3. 在 MyBatis 的核心处理流程中,包括 SQL 拼接、SQL 执行、结果集映射等关键步骤,都是需要打印日志的,如果在各处都显式地进行 log.info(“xxx”) 打印肯定不太合适,那么如何将日志打印优雅地织入到核心流程中?

Adapter Pattern 适配器模式

我们要在系统中集成多个第三方组件,每个组件具有相似的功能,但是接口定义各不相同,而我们自己的系统希望以统一地方式对组件进行调用。这么典型的使用场景,第一时间就可以想到 Adapter Pattern 适配器模式。 我们先来复习一下经典适配器模式的 UML 图:

Adapter Pattern
Adapter Pattern

(图片来源:https://refactoring.guru/design-patterns/adapter)

适配器模式的作用:将一个接口转换成满足客户端期望的另一个接口,使得接口不兼容的那些类可以一起工作。

Adapter 模式主要包含了以下角色:

  • Client:客户端,即我们自己的业务系统;
  • Client Interface:目标接口,定义了统一的、所有第三方组件都需要遵循的规范;
  • Service:需要集成的第三方组件,它包含了我们需要的功能,但是因为接口定义不匹配,所以无法直接使用;
  • Adapter:即最核心的适配器,它实现了 Client Interface 接口,并且对于 Service 进行了包装。这样一来,Adapter 就成为了既符合业务接口规范,同时又具备了期望的功能的组件,可以直接在项目中使用

集成第三方日志框架

了解了适配器模式之后,我们来看下 MyBatis 是怎么把它灵活运用于日志模块中的。

首先,MyBatis 定义了 Log 接口,并指定了四种日志级别:

代码语言:javascript
复制
/**
 * MyBatis日志接口定义
 * 指定了trace、debug、warn和error四种日志级别
 */
public interface Log {

  boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);

}

可以看出,这其实是所有主流日志框架所支持的级别的交集。

接下来,MyBatis 为常用的日志框架都进行了 Adapter 的实现。这里以常用的 slf4j 为例:

代码语言:javascript
复制
/**
 * slf4j日志框架的Adapter实现
 * 该Adapter实现了Log接口,并且内部包装了slf4j的Logger对象以完成实际的日志打印功能
 */
class Slf4jLoggerImpl implements Log {

  private final Logger log;

  public Slf4jLoggerImpl(Logger logger) {
    log = logger;
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    log.error(s, e);
  }

  @Override
  public void error(String s) {
    log.error(s);
  }

  @Override
  public void debug(String s) {
    log.debug(s);
  }

  @Override
  public void trace(String s) {
    log.trace(s);
  }

  @Override
  public void warn(String s) {
    log.warn(s);
  }

}

该 Adapter 实现了 Log 接口,并且内部包装了 slf4j 的 org.slf4j.Logger 对象以完成实际的日志打印功能,是一种经典的适配器实现。

这样一来,日志适配器的整体结构就比较清晰了,我简单画一张图类比一下:

日志适配器
日志适配器

这里的对应关系为:

Adapter 模式

MyBatis 实现

Client Interface

Logger 接口

Service

org.slf4j.Logger 组件

Adapter

Slf4jLoggerImpl 适配器

有了日志适配器,就可以在 MyBatis 中实现日志打印的功能了。但是第三方的日志框架众多,如果业务方引入了多个框架,MyBatis 应该如何决策该使用哪一个呢?我们来看下 MyBatis 中 LogFactory 日志工厂的实现:

代码语言:javascript
复制
/**
 * 日志工厂,通过getLog()方法获取日志实现类
 */
public final class LogFactory {

  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;

  //按照顺序依次尝试加载Log实现类
  //优先级为:slf4j -> commons-logging -> log4j2 -> log4j -> jdk-logging -> no-logging
  static {
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
  }

  private LogFactory() {
    // disable construction
  }

  public static Log getLog(Class<?> clazz) {
    return getLog(clazz.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }
  ...省略非必要代码
}

可以看到,在 LogFactory 的静态代码块中,按照指定的顺序尝试加载 Log 实现类,具体的优先级为:slf4j -> commons-logging -> log4j2 -> log4j -> jdk-logging -> no-logging 。如果加载成功,则不再继续加载。这样就实现了主流日志框架的选择。从 MyBatis 的选择中也可以看出,slf4j 确实是日志框架的首选。

最后,可以稍微留意一下,日志适配器中有一个 no-logging,它对应的是 NoLoggingImpl 类,它是一个空的实现,里面什么都没做。这其实是一种 Null Object Pattern(空对象模式),它也实现了目标接口,但是内部实际上是 Do Noting,这样能够以统一的方式使用目标组件,并且省去了很多判空操作。

代码语言:javascript
复制
/**
 * 空日志适配器
 * Null Object模式
 */
public class NoLoggingImpl implements Log {

  public NoLoggingImpl(String clazz) {
    // Do Nothing
  }

  @Override
  public boolean isDebugEnabled() {
    return false;
  }

  @Override
  public boolean isTraceEnabled() {
    return false;
  }

  @Override
  public void error(String s, Throwable e) {
    // Do Nothing
  }

  @Override
  public void error(String s) {
    // Do Nothing
  }

  @Override
  public void debug(String s) {
    // Do Nothing
  }

  @Override
  public void trace(String s) {
    // Do Nothing
  }

  @Override
  public void warn(String s) {
    // Do Nothing
  }

}

好了,到这里 MyBatis 的日志功能已经实现了。但是作为有追求的程序员,我们不能只满足于实现业务需求,还应该考虑提升代码的可扩展性,在面对新需求的时候可以尽可能少地修改现有代码。 那么 MyBatis 是如何实现优雅地打印日志的呢?我们下节再来分析。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-07-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 日志需求分析
  • Adapter Pattern 适配器模式
  • 集成第三方日志框架
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档