Mybatis源码解读(一)--日志模块(适配器模式、动态代理)

Mybatis是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。 Mybatis的源码可以说写的非常漂亮,模块之前划分的很清晰,里面大量采用了设计模式,其中的一些模块完全可以二次封装作为工具类来使用。本文开始每一篇都会对其中一个模块进行解读,分析其设计模式。

为什么说Mybatis的日志模块优雅

日志模块的代码在org.apache.ibatis.logging。Mybatis的日志功能的实现可以说非常优雅,主要有两大优势:

1、Mybatis的本身不提供日志功能,而是对接了常用的第三方日志如slf4j、log4j等。在实际使用中根据优先级依次加载 slf4J → commonsLoging → Log4J2 → Log4J → JdkLog。

2、在传统的jdbc代码中,我们如果想打印日志,需要入侵业务代码。而在Mybatis的实际使用中,我们却感受不到任何日志的存在。下面是一个传统的jdbc查询的核心代码,如果打印日志则要手动添加:

// 注册驱动...
Class.forName("com.mysql.jdbc.Driver");

// 获得一个连接
Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);

// 创建一个查询
Statement stmt = conn.createStatement();
String userName = "lwtxzwt";

// 拼接SQL
String sql = "SELECT * FROM t_user where user_name = '" + userName + "'";
ResultSet rs = stmt.executeQuery(sql);

// TODO log

// 从resultSet中获取数据并转化成bean
while (rs.next()) {
    System.out.println("------------------------------");
    TUser user = new TUser();
    user.setId(rs.getInt("id"));
    user.setUserName(rs.getString("user_name"));

    // TODO log

}
// 关闭连接...

而在我们实际开发或者配置时,感觉不到这两块的存在,Mybatis又是如何优雅的实现呢?

适配器模式+静态代码块实现优先级加载

这里首先采用了一个适配器模式,定义了一个通过接口Log:

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);

}

第三方日志各自实现这个接口:

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);
  }

}

通过LogFactory里面的静态代码块,传入各个构造方法,通过判断构造方法是否为空,来实现根据优先级加载:

public final class LogFactory {

  ...

  //被选定的第三方日志组件适配器的构造方法
  private static Constructor<? extends Log> logConstructor;

  //自动扫描日志实现,并且第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
  static {
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
  }

  ...
  
  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {//当构造方法不为空才执行方法
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  ...

}

通过动态代理附加日志功能

Mybatis通过动态代理,给Connection、Statement(PreparedStatement)、ResultSet附加了日志功能,其对应的实现是xxxLogger类。

ConnectionLogger的增强:

  @Override
  //对连接的增强
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
    	//如果是从Obeject继承的方法直接忽略
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      //如果是调用prepareStatement、prepareCall、createStatement的方法,打印要执行的sql语句
      //并返回prepareStatement的代理对象,让prepareStatement也具备日志能力,打印参数
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//打印sql语句
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);//创建代理对象
        return stmt;
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//打印sql语句
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);//创建代理对象
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);//创建代理对象
        return stmt;
      } else {
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

如果是调用prepareStatement、prepareCall、createStatement的方法,打印要执行的sql语句,并返回prepareStatement的代理对象,让prepareStatement也具备日志能力,打印参数。 

        if ("executeQuery".equals(method.getName())) {
          ResultSet rs = (ResultSet) method.invoke(statement, params);
          return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
        } else {
          return method.invoke(statement, params);
        }

StatementLogger也类似,当调用executeQuery时,打印日志,并返回resultSet的代理对象。ResultSetLogger也同理。 

 

因此当我们通过Mybatis获取Connection时(Mybatis对外不会有Connection概念,主要是通过SqlSession封装。),获取到的是代理对象,所以后续操作能够自动的打印出日志。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值