分析mybatis如何实现打印sql语句

  使用mybatis查询数据库时,若日志级别为debug时,自动打印sql语句,参数值以及结果集数目,类似这样

==>  Preparing: select id, description from demo where id = ? 
==>  Parameters: 1(Integer)
<==  Total: 1

  若是使用jdbc,打印类似日志,原有的jdbc逻辑里,我们需要插入日志打印逻辑

 1 String sql = "select id, description from demo where id = ?";
 2 System.out.println("==>  Preparing: " + sql);
 3 PreparedStatement psmt = conn.prepareStatement(sql);
 4 System.out.println("==>  Parameters: 1(Integer)");
 5 psmt.setInt(1,1);
 6 ResultSet rs = psmt.executeQuery();
 7 int count = 0;
 8 while(rs.next()) {
 9     count++;
10 }
11 System.out.println("<==  Total:" + count);

  这样做是因为我们无法改变jdbc代码,不能让数据库连接获取PreparedStatement对象时,告诉它你把传给你的sql语句打印出来吧。这时候就在想如果prepareStatement有一个日志打印的功能就好了,还要可以传入日志对象和日志格式参数就更好了,可惜它没有这样的功能。

  我们只能额外在获取PreparedStatement对象时,PreparedStatement对象设置参数时和ResultSet处理返回结果集时这些逻辑之上加上日志打印逻辑,这是很让人沮丧的代码。其实很容易想到,这种受限于原有功能实现,需要增强其能力,正是代理模式适用的场景,我们只需要创建对应的代理类去重写我们想要增强的方法就好。

 

  回到mybatis。mybatis查询数据库时也是使用的jdbc,mybatis作为一种持久层框架,使用了动态代理来增强jdbc原有逻辑,代码在org.apache.ibatis.logging.jdbc包下,下面从getMapper来逐步分析mybatis如何实现打印sql语句。

  mybatis有两种接口调用方式,一种是基于默认方法传入statementID,另一种是基于Mapper接口,其实两种方式是一样的,一会就可以知道,先介绍getMapper。

  getMapper是mybatis顶层API SqlSession的一个方法,默认实现

public class DefaultSqlSession implements SqlSession {
   // mybatis配置信息对象
private final Configuration configuration; @Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); } }
  Configuration 保存解析后的配置信息,继续往下走
public class Configuration { 
  
protected final MapperRegistry mapperRegistry = new MapperRegistry(this); public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } }
  MapperRegistry 保存着Mapper接口与对应代理工厂的映射关系
public class MapperRegistry {
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
          throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
          return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
          throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
}
  MapperProxyFactory是一个生成Mapper代理类的工厂,使用动态代理去生成具体的mapper接口代理类
public class MapperProxyFactory<T> {
   // 构造器中初始化
private final Class<T> mapperInterface; protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }  
  分析MapperProxy定义的代理行为,调用mapper接口里面的方法时,都会走这里,这里完成了mapper接口方法与Mapper.xml中sql语句的绑定,相关参数已在MapperMethod构造器中初始化,这里逻辑较为复杂,简单来说,就是让接口中的方法指向具体的sql语句
public class MapperProxy<T> implements InvocationHandler, Serializable {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
   // 这里关联mapper中接口方法与Mapper.xml中的sql语句
final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } }

  下一步就是MapperMethod具体的执行逻辑了,这里内容较多,主要是对执行sql的类型进行判断,简单截取select部分

case SELECT:
  if (method.returnsVoid() && method.hasResultHandler()) {
    executeWithResultHandler(sqlSession, args);
    result = null;
  } else if (method.returnsMany()) {
    result = executeForMany(sqlSession, args);
  } else if (method.returnsMap()) {
    result = executeForMap(sqlSession, args);
  } else if (method.returnsCursor()) {
    result = executeForCursor(sqlSession, args);
  } else {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
  }
  break;

  看到这里就可以发现原来当我们使用getMapper生成的代理类型时,调用内部自定义方法,仍然是基于mybatis默认方法的。不过这好像和打印sql语句没啥关系,重点在类似 sqlSession.selectOne(command.getName(), param)方法,依旧去查看其默认实现DefaultSqlSession,可以发现是调用内部的执行器去执行查询方法的

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
    // mybatis存在3种执行器,批处理、缓存和标准执行器
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }

  这里的执行器是在构造方法内赋值的,默认情况下使用的是SimpleExecutor,这里省略父类query方法中的缓存相关代码,重点是子类去实现的doQuery方法

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

  这里获取Connection对象时,猜测mybatis一定是对Connection进行了增强,不然无法在获取Statement 之前打印sql语句

protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
}
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
      InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
      ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler); }

  代码很清楚的,如果你的当前系统支持打印debug日志,那么就动态帮你生成一个连接对象,这里传入的代理行为是这个类本身,那么只需要分析其的invoke方法就好了,这里依旧只分析一部分必要的

public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
  if (Object.class.equals(method.getDeclaringClass())) {
    return method.invoke(this, params);
  }    
  if ("prepareStatement".equals(method.getName())) {
    if (isDebugEnabled()) {
      debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
    }        
    PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
    stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
    return stmt;
  }
}
    

  在这里找到了打印的sql信息,还可以发现下面的PreparedStatementLogger继续增强,逻辑都是一样的,就不分析了。有一点需要注意的,这里是mapper接口的动态代理类,所以这里的日志级别是受接口所在包的日志级别控制的,只需要配置mapper接口的日志级别是debug,就可以看见打印的sql语句了。

  

  到这里,已经知道了,sql信息是如何打印出来的了,可是,还有一个问题需要解决,这里的日志对象是怎么来的,mybatis本身是没有日志打印能力的。

  mybatis本身并没有内嵌日志框架,而是考虑了用户本身的日志框架的选择。简而言之,就是用户用啥日志框架,它就用什么框架,当用户存在多种选择时,它也有自己的偏好设计(可以指定)。这样做,就需要mybatis兼容市面上常见的日志框架,同时自己也要有一套日志接口,mybatis定义日志输出级别控制,兼容日志框架提供的具体实现类。这是不是又和一种设计模式很像了,适配器模式,转换不同接口,实现统一调用,具体的代码在org.apache.ibatis.logging包下。

  org.apache.ibatis.logging.Log 是mybatis自己定义的日志接口,org.apache.ibatis.logging.LogFactory 是mybatis用于加载合适的日志实现类,其下的众多包,均是日志框架的适配器类,主要做日志级别的转换和具体log对象的创建,那么这些适配器类怎么加载的,LogFactory有一个静态代码块去尝试加载合适的日志框架,然后创建正确的log对象。

public final class LogFactory {
  private static Constructor<? extends Log> logConstructor;

  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    ...
  }

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

  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }
}
View Code

  这里会按顺序去尝试加载不同的日志框架,若当前系统中存在对应的日志框架,才可以加载成功,这样logConstructor就有值了,下面则不会再尝试加载,在getLog里面则是实例化具体的日志对象。

 

  这样就完成了mybatis如何打印sql语句的整体流程解析,主要有两点,创建Log对象和通过动态代理给jdbc增加日志打印能力。

 

posted @ 2020-03-15 22:56  柠檬水请加冰  阅读(2591)  评论(0编辑  收藏  举报