分析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); } } }
这里会按顺序去尝试加载不同的日志框架,若当前系统中存在对应的日志框架,才可以加载成功,这样logConstructor就有值了,下面则不会再尝试加载,在getLog里面则是实例化具体的日志对象。
这样就完成了mybatis如何打印sql语句的整体流程解析,主要有两点,创建Log对象和通过动态代理给jdbc增加日志打印能力。