mybatis解析和基本运行原理
Mybatis 的运行过程分为两大步:
- 第 1 步,读取配置文件缓存到 Configuration 对象,用于创建 SqlSessionFactory;
- 第 2 步,SqlSession 的执行过程。相对而言,SqlSessionFactory 的创建还算比较容易理解,而 SqlSession 的执行过程就不那么简单了,它包括许多复杂的技术,要先掌握反射技术和动态代理,这里主要用到的是 JDK 动态代理.
一个简单使用的例子
public class TestMybatis { public static void main(String[] args) { String resource = "mybatis-config.xml"; Reader reader = null; try { reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); SqlSession session = sqlSessionFactory.openSession(); UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.findById(2); System.out.println("name:" + user.getName()); session.close(); SqlSession session1 = sqlSessionFactory.openSession(); List<User> users = session1.selectList("findAll"); session1.commit(); System.out.println("allSize:" + users.size()); session1.close();} catch (IOException e) {e.printStackTrace(); } } }
MyBatis 的主要构件及其相互关系
从 MyBatis 代码实现的角度来看,MyBatis 的主要的核心部件有以下几个:
- SqlSession 作为 MyBatis 工作的主要顶层 API,表示和数据库交互的会话,完成必要数据库增删改查功能
- Executor MyBatis 执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护
- StatementHandler 封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 Statement 结果集转换成 List 集合。
- ParameterHandler 负责对用户传递的参数转换成 JDBC Statement 所需要的参数,
- ResultSetHandler 负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合;
- TypeHandler 负责 java 数据类型和 jdbc 数据类型之间的映射和转换
- MappedStatement MappedStatement 维护了一条 <select|update|delete|insert> 节点的封装,
- SqlSource 负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回
- BoundSql 表示动态生成的 SQL 语句以及相应的参数信息
- Configuration MyBatis 所有的配置信息都维持在 Configuration 对象之中。
构建 SqlSessionFactory 过程
构建主要分为 2 步:
- 通过 XMLConfigBuilder 解析配置的 XML 文件,读出配置参数,包括基础配置 XML 文件和映射器 XML 文件;
- 使用 Configuration 对象创建 SqlSessionFactory,SqlSessionFactory 是一个接口,提供了一个默认的实现类 DefaultSqlSessionFactory。
说白了,就是将我们的所有配置解析为 Configuration 对象,在整个生命周期内,可以通过该对象获取需要的配置。
SqlSession 运行过程
我们与 mybatis 交互主要是通过配置文件或者配置对象,但是我们最终的目的是要操作数据库的,所以 mybatis 为我们提供了 sqlSession 这个对象来进行所有的操作,也就是说我们真正通过 mybatis 操作数据库只要对接 sqlSession 这个对象就可以了。那么问题来了,我们怎么样通过 sqlSession 来了操作数据库的呢?
问题 1:如何获取 sqlSession?
public interface SqlSessionFactory {SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);Configuration getConfiguration();
}
由上面代码我们可知我们可以通过 SqlSessionFactory 的 openSession 去获取我们的 sqlSession,只要我们实例化了一个 SqlSessionFactory,就能得到一个 DefaultSqlSession 对象。
问题 2:Mapper 对象怎么来的?
@Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this);}
通过调用 DefaultSqlSession 的 getMapper 方法并且传入一个类型对象获取,底层呢调用的是配置对象 configuration 的 getMapper 方法,configuration 对象是我们在加载 DefaultSqlSessionFactory 时传入的。
然后我们再来看下这个配置对象的 getMapper,传入的是类型对象(补充一点这个类型对象就是我们平时写的 DAO 层接口,里面是一些数据库操作的接口方法。),和自身也就是 DefaultSqlSession。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }
我们看到这个 configuration 的 getMapper 方法里调用的是 mapperRegistry 的 getMapper 方法,参数依然是类型对象和 sqlSession。这里呢,我们要先来看下这个 MapperRegistry 即所谓 Mapper 注册器是什么。
public class MapperRegistry { private final Configuration config; private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> MapperRegistry(Configuration config) { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.config =<span style="color: rgba(0, 0, 0, 1)"> config; } ....
}
从这里我们可以知道其实啊这个 MapperRegistry 就是保持了一个 Configuration 对象和一个 HashMap,而这个 HashMap 的 key 是类型对象,value 呢是 MapperProxyFactory。我们这里先不管 MapperProxyFactory 是什么东西,我们现在只需要知道 MapperRegistry 是这么一个东西就可以了。这里有人会问 MapperRegistry 对象是怎么来的,这里呢是在初始化 Configuration 对象时初始化了这个 MapperRegistry 对象的,代码大家可以去看,为了避免混乱,保持贴出来的代码是一条线走下来的,这里就不贴出来了。接下来我们继续看下这个 MapperRegistry 的 getMapper 方法。
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);} }
这里我们可以看到从 knownMappers 中获取 key 为类型对象的 MapperProxyFactory 对象。然后调用 MapperProxyFactory 对象的 newInstance 方法返回,newInstance 方法传入 sqlSession 对象。到这里我们可能看不出什么端倪,那我们就继续往下看这个 newInstance 方法做的什么事情吧。
public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
这里我们可以看到 MapperProxyFactory 直接 new 了一个 MapperProxy 对象,然后调用另外一重载的 newInstance 方法传入 MapperProxy 对象。这里我们可以看出一些东西了,通过调用 Proxy.newProxyInstance 动态代理了我们的 mapperProxy 对象!这里的 mapperInterface 即我们的 dao 层(持久层)接口的类型对象。
所以总结下就是我们通过 sqlSesssion.getMapper(clazz) 得到的 Mapper 对象是一个 mapperProxy 的代理类!
所以也就引出下面的问题。
问题 3:为什么我调用 mapper 对象方法就能发出 sql 操作数据库?
通过上面的讲解,我们知道了这个 mapper 对象其实是一个一个 mapperProxy 的代理类!所以呢这个 mapperProxy 必然实现了 InvocationHandler 接口。所以当我们调用我们的持久层接口的方法时必然就会调用到这个 MapperProxy 对象的 invoke 方法,所以接下来我们进入这个方法看看具体 mybatis 为我们做了什么。
@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); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
从代码中我们可以看到前面做了一个判断,这个判断主要是防止我们调用像 toString 方法或者 equals 方法时也能正常调用。然后我们可以看到它调用 cachedMapperMethod 返回 MapperMethod 对象,接着就执行这个 MapperMethod 对象的 execute 方法。这个 cachedMapperMethod 方法主要是能缓存我们使用过的一些 mapperMethod 对象,方便下次使用。这个 MapperMethod 对象主要是获取方法对应的 sql 命令和执行相应 SQL 操作等的处理,具体细节同学们可以抽空研究。
继续跟踪 MapperMethod.execute
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } 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; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for:" + command.getName());} if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method'" + command.getName() + "attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");} return result; }
我们可以清晰的看到这里针对数据库的增删改查做了对应的操作,这里我们可以看下查询操作。我们可以看到这里针对方法的不同返回值作了不同的处理,我们看下其中一种情况。
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
这里我们可以看到它将方法参数类型转换成数据库层面上的参数类型,最后调用 sqlSession 对象的 selectOne 方法执行。所以我们看到最后还是回到 sqlSession 对象上来,也就是前面所说的 sqlSession 是 mybatis 提供的与数据库交互的唯一对象。
接下来我们看下这个 selectOne 方法做了什么事,这里我们看的是 defaultSqlSession 的 selectOne 方法。
@Override public <T> T selectOne(String statement, Object parameter) { // Popular vote was to return null on 0 results and throw exception on too many. List<T> list = this.<T>selectList(statement, parameter); if (list.size() == 1) { return list.get(0);} else if (list.size() > 1) { throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found:" + list.size());} else { return null; } }
我们看到它调用 selectList 方法,通过去返回值的第一个值作为结果返回。那么我们来看下这个 selectList 方法。
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); 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();} }
我们可以看到这里调用了 executor 的 query 方法,我们再进入到 query 里看看。这里我们看的是 BaseExecutor 的 query 方法
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 1. 根据具体传入的参数,动态地生成需要执行的 SQL 语句,用 BoundSql 对象表示 BoundSql boundSql = ms.getBoundSql(parameter); // 2. 为当前的查询创建一个缓存 Key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 3. 缓存中没有值,直接从数据库中读取数据
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//4. 执行查询,返回 List 结果,然后 将查询的结果放入缓存之中
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
我们看到有个一个方法 doQuery,进入方法看看做了什么。点进去后我们发现是抽象方法,我们选择 simpleExecutor 子类查看实现。
@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(); //5. 根据既有的参数,创建 StatementHandler 对象来执行查询操作 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); //6. 创建 java.Sql.Statement 对象,传递给 StatementHandler 对象 stmt = prepareStatement(handler, ms.getStatementLog()); //7. 调用 StatementHandler.query() 方法,返回 List 结果集 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()); //对创建的 Statement 对象设置参数,即设置 SQL 语句中 ? 设置为指定的参数 handler.parameterize(stmt); return stmt; }
我们可以看到通过 configuration 对象的 newStatementHandler 方法构建了一个 StatementHandler,然后在调用 prepareStatement 方法中获取连接对象,通过 StatementHandler 得到 Statement 对象。另外我们注意到在获取了 Statement 对象后调用了 parameterize 方法。继续跟踪下去(自行跟踪哈)我们可以发现会调用到 ParameterHandler 对象的 setParameters 去处理我们的参数。所以这里的 prepareStatement 方法主要使用了 StatementHandler 和 ParameterHandler 对象帮助我们处理语句集和参数的处理。最后还调用了 StatementHandler 的 query 方法,我们继续跟踪下去。
这里我们进入到 PreparedStatementHandler 这个 handler 查看代码。这里我们进入到 PreparedStatementHandler 这个 handler 查看代码。
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { String sql = boundSql.getSql(); statement.execute(sql); //使用 ResultHandler 来处理 ResultSet return resultSetHandler.<E>handleResultSets(statement); }
看到这里,我们终于找到了操作数据库的地方了,就是 ps.execute() 这句代码。底层我们可以发现就是我们平时写的 JDBC!然后将这个执行后的 PreparedStatement 交给 resultSetHandler 处理结果集,最后返回我们需要的结果集。