这一章节分析动态代理执行sql的具体流程。
Mybatis 的动态代理
在第一章的时候我们已经简单介绍过动态代理,现在我们详细来解析一下mybatis是怎么操作的。这是简单Demo.沿用了第一章的例子,用来查询User类。
| 字段名 | 类型 | 主键 |
|---|---|---|
| id | int | true |
| name | varchar | false |
| phone | varchar | false |
//省略get set方法
public class User {
private int id;
private String name;
private String phone;
}
public static void main(String[] args) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println(mapper.findAll());
sqlSession.close();
}
当我们调用sqlSession.getMapper时,返回的就已经是一个代理对象了,所以才能直接调用接口中的方法。我们沿着这个方法深入。
//DefaultSqlSession.getMapper
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
在解析xml的时候,我们会将mapper的存放在Configuration中的mapperRegistry中。所以查询的时候同样从里面取出。
//Configuration.getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
//MapperRegistry.getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//这个knownMappers在xml解析时见过,是MapperRegistry中的容器,用来存放解析成功的mapper
//而解析失败的会被添加到Configuration的incompleteMethods容器中,等待后续解析的机会.
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.newInstance
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
//MapperProxyFactory.newInstance
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
这个就是动态代理了, Proxy.newProxyInstance时jdk自带的方法,传入三个参数,第一个为classLoader,第二个为代理的类,这里我们传入的时UserMapper接口,第三个为代理对象MapperProxy。
MapperProxy这个类实现了InvocationHandler接口.所以能进行代理操作.重写了invoke方法.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//这里判断该方法调用的是不是object的方法,如equals,hashcode.如果是,则直接待用方法本身.
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
//判断是否是默认方法,就是在接口中直接实现的方法.
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//创建一个mapperMethod并添加到缓存中
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
经过代理之后,当物品们定义的userMpper调用方法时,都会进入到这个invoke方法中。
语句执行流程
接下来我们用具体的sql来进行举例,分析执行流程。
<!--如果我们使用包扫描的方式来注册别名,就能直接写类的缩写User,否则要写成com.entity.User-->
<select id="findAll" resultType="User">
select * from user
</select>
这是简单的查询语句,并不需要额外传入参数,这样方便我们说明。后续我们会分析带参数的sql。
在invoke方法中,最后调用了mapperMethod.execute(sqlSession, args)。先来看一下MapperMethod这个类的结构。
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
MapperMethod有两个成员变量,同时这两个类也是MapperMethod的内部类。SqlCommand里面保存了方法名称,方法的类型(增删改查)。MethodSignature保存方法的返回类型。
当我们调用UserMapper.findAll()方法来进行查询时,首先会进入invoke中进行方法类型的判断,该方法既不是Object中的方法,也不是默认方法,所以最后调用 mapperMethod.execute(sqlSession, args);
MapperMethod.execute根据方法的类型和返回值来决定具体的执行流程.findAll使用了Select类型,返回类型为List<User>,所以会进入 result = executeForMany(sqlSession, args)。
其实insert,update,delete最后都会调用DefaultSqlSession.update方法。
//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);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
//isPrimitive 判断是否是java 8个原始类型
//就是需要返回原始数据但是实际返回空的,要抛出异常
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;
}
这个方法非常清晰,根据不同的类型会调用不同的结果,其中select类型中又分成了查询多个,查询MAP,查询游标,和只查询一个(selectOne,查询一个其实和查询多个类似,只不过最后取列表中的第一个,返回列表不止一个的情况会抛出异常)
//MapperMethod.executeForMany
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
//获取方法上的参数,这里由于我们findAll没有传参,所以为空
Object param = method.convertArgsToSqlCommandParam(args);
//是否需要分页,这里是mybatis的分页插件,一般不使用
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
//该方法返回多条记录,command的类是SqlCommand保存了我们语句信息
//getName方法获取mapper.xml语句路径,如com.mapper.UserMapper.findAll
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
//方法定义的返回类是否是实际结果返回值的父类,这里我们的返回类型为List<User>,这里指的就是List本身或者它的子类
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
//判断方法定义的返回值是否是一个数组
if (method.getReturnType().isArray()) {
//将结果转为数组
return convertToArray(result);
} else {
//转为方法定义的集合
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
sqlSession是个接口类,这里会调用默认实现类的方法DefaultSqlSession,selectList
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
sqlSession的selectList方法最后回调用执行器中的executor.query方法。sqlSession大部分的方法都会委托给executor来执行。例如查询就会调用executor.query,增删改调用 自生的update,在调用executor.update,所以增删改其实就是一种类型,都是修改。
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//这个MappedStatement是在解析mapper.xml时候创建的,保存了语句信息,返回类型等,每个sql都会生成一个MappedStatement,例如findAll就会被解析成一个MappedStatement
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();
}
}
对于Mybatis执行器Executor,它采用了嵌套的方式,外层是CachingExecutor,里面是BaseExecutor的子类SimpleExecutor。调用方法也是先调用外层的方法,在调用里层的同名方法。
public class CachingExecutor implements Executor {
private final Executor delegate;
//...
}
这是构造器的构建方法,可以很清楚的看出内外层关系。这里等说到拦截器链的时候再详细说明。
//Configuration.newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
所以这里是外层CachingExecutor.query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//获取绑定的sql语句
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建这个语句的参数构建缓存的key,如果之后查找相同的key,就不用直接查数据库了。
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这里的query还是本地方法CachingExecutor.query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//这个首先获取二级缓存,默认是不开起的,需要在手动在mapper.xml配置
Cache cache = ms.getCache();
//没有配置就是null
if (cache != null) {
//判断缓存是否需要刷新,flushCache是select的一个参数,如果为true,那么每次都会清空缓存
flushCacheIfRequired(ms);
//判断是否使用缓存,如select就能配置useCache属性,而insert,update,delete就没有
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//尝试取缓存中查找,如果没有,调用查找方法
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//没有二级缓存,直接使用查找方法.
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
CachingExecutor是用来进行缓存的查询,如果没有找到,就是用它内部的执行器delegate.query再进行查找。 这里是内层SimpleExecutor,它自己本身没有查询方法,就调用父类的BaseExecutor.query。
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.");
}
//queryStack表示正在进行查询的数量,flushCache是select的一个参数,如果为true,那么每次都会清空缓存
//localCache是本地缓存,也是一级缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//这里取本地缓存的数据,如果没取到,查询数据库。这里的key是上文根据语句和参数生成的
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//这个方法如果StatementType为CALLABLE就执行存储过程,
//还有两种类型,STATEMENT表示直接查询数据库,PREPARED表示语句需要占位符预处理
//默认为PREPARED,可以在mapper语句上用参数statementType进行设置
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//查询数据库
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;
}
由于是第一次进行查询,缓存中肯定是没有的,直接执行查询数据库的方法。这里的queryFromDatabase也是父类BaseExecutor中的方法。
//BaseExecutor.queryFromDatabase
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 {
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是BaseExecutor中的抽象方法,子类SimpleExecutor实现了该方法
//SimpleExecutor.doQuery
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的时候会将其加入拦截器链
//StatementHandler与jdbc中的statement类似,用来执行语句的
//回顾一下MappedStatement用来存放sql信息的,不要混淆
//StatementHandler结构也与Exector类似,也是外层RoutingStatementHandler嵌套一层别的StatementHandler
//根据之前的StatementType,如果是STATEMENT,那么就会嵌套SimpleStatementHandler
//如果是PREPARED,那么就会嵌套PreparedStatementHandler,如果是CALLABLE,嵌套CallableStatementHandler
//由于查询语句中默认值为PREPARED所以嵌套的为PreparedStatementHandler
//他们有个基础实现类BaseStatementHandler,其他StatementHandler为它的子类
//在初始化BaseStatementHandler时,也会同时初始化ParameterHandler用来处理入参,ResultSetHandler用来处理结果集
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//这里做一些预处理工作
stmt = prepareStatement(handler, ms.getStatementLog());
//执行查询方法,并封装结果集。
return handler.query(stmt, resultHandler);
} finally {
//关闭jdbc中的Statement
closeStatement(stmt);
}
}
对数据库进行查询的时候,肯定也要通过jdbc进行处理,这里就顺着之前jdbc的代码思路来看就很好理解。
//SimpleExecutor.prepareStatement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//获取jdbc数据库连接
Connection connection = getConnection(statementLog);
//一些准备工作,初始化Statement连接
stmt = handler.prepare(connection, transaction.getTimeout());
//使用ParameterHandler处理入参
handler.parameterize(stmt);
return stmt;
}
handler.prepare同样先调用RoutingStatementHandler.prepare,接着调用SimpleStatementHandler.prepare,当然SimpleStatementHandler没有重写该方法,所以调用父类BaseStatementHandler中的方法。(RoutingStatementHandler只是作为路由,根据StatementType来选择对应的StatementHandler子类,statementType可以在mapper.xml的select语句标签上进行配置)
BaseStatementHandler.prepare
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
//获取jdbc的Statement
statement = instantiateStatement(connection);
//设置超时时间,如果有配置优先使用select语句中的timeout参数,再查看全局配置seetings中的defaultStatementTimeout参数
setStatementTimeout(statement, transactionTimeout);
//设置抓取条数,如果有配置优先使用select中的fetchSize,在查看全局全局配置seetings中的defaultFetchSize参数
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
在获取了jdbc连接Connection和Statement,接下来就要调用查询方法了。handler.query(stmt, resultHandler);根据上文handler指的是SimpleStatementHandler。
//SimpleStatementHandler.query
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//转型为jdbc中的 PreparedStatement
PreparedStatement ps = (PreparedStatement) statement;
//执行sql语句
ps.execute();
//resultSetHandler处理结果集
return resultSetHandler.handleResultSets(ps);
}
使用Statement查询完数据之后,就是对获取到的数据进行结果集封装了。用到了jdbc的ResultSet来获取对应的数据。
//handleResultSets.handleResultSets
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
//ResultSet rs = stmt.getResultSet();
//将根据ResultSet包装成ResultSetWrapper返回
//ResultSetWrapper里面存放这字段信息,数据库字段类型以及对应的java类型
//这些类型都是在初始化的register的时候预制的
ResultSetWrapper rsw = getFirstResultSet(stmt);
//mappedStatement是根据mapper.xml中的sql语句构建的,ResultMap是里面的结果集映射
//如我们的返回类型resultType,resultMap都会被构建成一个ResultMap
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
//校验
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
//处理行数据,这里会将获取到的数据进行封装,变成我们定义的实体类
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
//类似的,ResulSets是查询语句中,多结果集映射,参数为resultSetType
//这里与上文类似
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
//结果集处理
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
//返回查询的集合
return collapseSingleResultList(multipleResults);
}
handleResultSets.handleResultSet
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
//我们没有使用自定义的resultHandler,所以会生成一个默认的
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
//处理每行的数据
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
//这里关闭jdbc中的ResultSet
closeResultSet(rsw.getResultSet());
}
}
总结
现在我们对之前遇到过的类做一个总结。
| 类 | 作用 |
|---|---|
| Configuration | 用来存储我们的xml文件中的配置信息,以及系统默认的常用别名,数据库类型等,seetings中的配置等等。 |
| SqlSession | 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。 |
| Executor | 用来执行查询方法,查询缓存等。 |
| StatementHandler | 封装了JDBC Statement操作,用来执行语句 |
| ParameterHandler | 负责对用户传递的参数转换成JDBC Statement 所需要的参数 |
| ResultSetHandler | 将JDBC返回的ResultSet结果集对象转换成List类型的集合 |
| TypeHandler | 负责java数据类型和jdbc数据类型之间的映射和转换,存放在TypeHandlerRegistry类型处理器的注册仓库中,初始化了一些常用别名 |
| TypeAliasRegistry | 别名的注册仓库,会初始化常用别名,以及我们注册的别名。 |
| MappedStatement | 解析mapper.xml的增删改查节点时会生成的类,记录了sql的一系列参数 |
| MapperRegistry | 我们编写的dao层接口会被注册到这里 |
回顾一下这次查询的流程,首先我们解析mybatis.xml配置文件,将其中的配置解析到Configuration中,接着开启会话SqlSession,使用动态代理拿到mapper,并执行其中的方法。在执行方法时,我们要要过SqlSession调用执行Executor执行器,分为两类增删改统一调用update,查询调用query。在查询时,不可能时直接查询数据库,要先查二级缓存,在查一级缓存。他会根据MappedStatement查询语句中的参数,parameterObject传入参数,Mybatis分页信息,等生成一个CacheKey 来查询。如果成功查询到结果,那么直接返回,查询不到就要到数据库中进行查询,然后放到缓存中。
查询数据库肯定要进行jdbc的操作了。Mybatis先创建StatementHandler(这个类里面会附带创建两个处理类,一个是parameterHandler用来处理我们传入sql的参数,一个resultSetHandler用来处理从数据库返回的结果集),然后创建数据库连接Connection,初始化数据库Statement,处理入参,Statement执行sql语句,处理返回结果并封装成我们自定义的类,关闭数据库相关连接,返回结果。