从getMapper方法开始,逐步分析在mybatis中如何创建日志对象以及在jdbc逻辑中插入日志打印代码。
使用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增加日志打印能力。