日常开发使用mybatis进行CURD操作十分简便,我们只需要在Mapper接口定义好方法,然后在mapper.xml中写好SQL语句,就能在业务代码中使用了。这简单到令人发指步骤,让我们麻痹到以为这个过程是非常简单的,时间久了脑子都笨了。今天,接着上一篇文章中的例子,结合源码学习一下mybatis中一条SQL的执行过程,了解下背后发生的故事。
# 从Demo开始
还是之前的例子,代码如下。
## 示例代码
~~~xml
<!--CompanyDAO.xml-->
<select id="selectById" resultMap="baseResultMap" >
select
<include refid="BaseColumns"></include>
from company
where id= #{id}
</select>
复制代码
~~~
CompanyDAO.xml,按照id查询数据表company的记录,id为主键,selectById为statementId。
~~~java
public interface CompanyDao {
/**
* 根据id查询CompanyDO
*
* @param id 主键id
* @return 查询结果
*/
CompanyDO selectById(@Param("id") Integer id);
}
复制代码
~~~
CompanyDao.java,Mapper接口,仅包含一个方法selectById,按照mybatis要求,与mapper的xml配置文件一一对应。
~~~java
public class Program {
public static void main(String[] args) throws IOException {
// mybatis配置文件
String path = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(path);
// 获取 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建 SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
//获取Mapper:CompanyDao
CompanyDao dao = sqlSession.getMapper(CompanyDao.class);
//调用selectById方法
CompanyDO companyDO = dao.selectById(1);
System.out.println(companyDO);
}
}
}
复制代码
~~~
Program.java业务代码,调用逻辑比较简单,再啰嗦一下:
* 通过mybatis-config.xml创建SqlSessionFactory;
* 使用SqlSessionFactory创建并打开SqlSession;
* 通过sqlSession获取CompanyDao的Mapper实例对象;
* 调用selectById方法获取查询结果;
## 示例分析
由以上示例代码可知,CompanyDao是一个接口,我们并没有编写它的实现类,那为啥可以创建它的对象呢?我们现在得到的到底是哪个类的实例?
打个断点!下面调试界面截图,我们发现dao是org.apache.ibatis.binding.MapperProxy的实例。
![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/63b05a91c0bb4218b0d76bb7be8bdc55~tplv-k3u1fbpfcp-zoom-1.image)
熟悉动态代理的同学,看一眼就知道MapperProxy是一个代理类。我们就从这里入手,顺藤摸瓜。
# 流程分析
## Mapper对象怎么来的?
MapperProxy对象是通过SqlSession#getMapper方法创建的,这里的sqlSession是DefaultSqlSession的实例,跟着源码走一下。
~~~java
//org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper
@Override
public <T> T getMapper(Class<T> type) {
//这里调用了configuration的方法,继续走
return configuration.<T>getMapper(type, this);
}
//org.apache.ibatis.session.Configuration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//所有的Mapper都注册在Configuration#mapperRegistry(不过是存储的MapperProxyFactory),所以是通过它来获取Mapper对象
//下一步进入MapperRegistry#getMapper
return mapperRegistry.getMapper(type, sqlSession);
}
//org.apache.ibatis.binding.MapperRegistry#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//通过type从map中获取MapperProxyFactory实例
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
//如果mapperProxyFactory,则抛出异常
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//通过mapperProxyFactory的newInstance创建实例,继续走
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
//org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
public T newInstance(SqlSession sqlSession) {
//先创建了MapperProxy对象
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
//调用了newInstance方法
return newInstance(mapperProxy);
}
//org.apache.ibatis.binding.MapperProxyFactory#newInstance
protected T newInstance(MapperProxy<T> mapperProxy) {
//到这里就揭开了代理对象创建的面纱
//使用Proxy创建了代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
复制代码
~~~
为了方便梳理,上面把Mapper创建过程的代码放在一起了,我们可以发现整个过程是通过Configuration串在一起的(Configuration是mybatis运行时的基础)。简单描述一下过程:
* 入口在DefaultSqlSession#getMapper,整个方式只是一个中继,需要通过Configuration#getMapper来创建;
* 在解析mybatis配置时,所有的Mapper注册信息由Configuration#mapperRegistry存储,但是存储的其实是一个与Mapper类型相关的MapperProxyFactory,既然是Factory,那就要生产点啥了。
* MapperProxyFactory创建了MapperProxy对象,然后通过Proxy创建了代理对象,由MapperProxy伪装了一个Mapper实例。
所以,Mapper对象(示例中的dao)其实一个“披着Mapper皮的MapperProxy对象”。上述过程虽然跳转的类比较多,一层一层走下来最主要的就是Java动态代理相关的内容。通过下面的时序图再来看下Mapper对象的创建过程吧。
![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/74bb6ee5cec841f6b20e4cdc220cca83~tplv-k3u1fbpfcp-zoom-1.image)
## 代理对象invoke方法执行
创建Mapper对象后,接下来调用Mapper方法,从下面的代码开始:
~~~java
CompanyDO companyDO = dao.selectById(1);
复制代码
~~~
dao为MapperProxy对象,当调用selectById时,会先调用org.apache.ibatis.binding.MapperProxy#invoke方法(这里可以看下Java动态代理相关的文章,保存出代理类的动态实现,有机会写篇文章)。
~~~java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//判断是否为Object类
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);
}
//走到这里说明是被代理接口的方法,获取或设置MapperMethod缓存,得到MapperMethod对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
//方法执行
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
//如果缓存中不存在method,就创建一个对象添加到缓存。
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
复制代码
~~~
到这里,我们可以知道方法invoke最终执行的是org.apache.ibatis.binding.MapperMethod#execute,接着看MapperMethod这个类。
~~~java
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
//sql命令,包含mapper接口、mapper方法和configuration
this.command = new SqlCommand(config, mapperInterface, method);
//方法签名
this.method = new MethodSignature(config, mapperInterface, method);
}
}
复制代码
~~~
MapperMethod构造方法内步创建了SqlCommand和MethodSignature,这两个类都是MapperMethod的内部类。等下看org.apache.ibatis.binding.MapperMethod#execute时会发现,它的执行与这两个类有很大关系,我们先来看下这两个做了什么。
~~~java
public static class SqlCommand {
private final String name;
//sql命令类型枚举:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
private final SqlCommandType type;
//构造方法:主要逻辑
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
//获取方法名称:示例中就是:selectById
final String methodName = method.getName();
//方法所在类的全路径限定名称:com.raysonxin.dao.CompanyDao
final Class<?> declaringClass = method.getDeclaringClass();
//解析获取MappedStatement:看下面方法的注释。
//这样就可以拿到MappedStatement对象了
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if(method.getAnnotation(Flush.class) != null){
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
//获取id
name = ms.getId();
//获取命令类型
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
public String getName() {
return name;
}
public SqlCommandType getType() {
return type;
}
//解析获取MappedStatement
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
//拼接获取statement,示例中是:com.raysonxin.dao.CompanyDao.selectById
//这其实就是Configuration中MappedStatement注册的key
String statementId = mapperInterface.getName() + "." + methodName;
//判断configuration是否存在这个statementId
if (configuration.hasStatement(statementId)) {
//如果存在,获取并返回
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
//这里是从父接口中获取的逻辑
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
复制代码
~~~
SqlCommand就是根据接口和方法的签名信息,从Configuration中获取对应的MappedStatement,MappedStatement是在解析mybatis-config.xml是添加的,大家可以翻阅Configuration解析代码,或者查看上一篇文章《Mybatis源码之Configuration》。SqlCommand创建后,我们就可以清楚知道了其对应的MappedStatement,也就知道了它是查询、更新、删除还是添加这样的命令类型。
~~~java
public static class MethodSignature {
//返回结果是否为多个值,即列表
private final boolean returnsMany;
//返回结果是否为Map类型
private final boolean returnsMap;
//返回结果是否为void
private final boolean returnsVoid;
//返回结果是否为游标
private final boolean returnsCursor;
//返回结果是否为Optioanl类型
private final boolean returnsOptional;
//返回值类型
private final Class<?> returnType;
private final String mapKey;
//
private final Integer resultHandlerIndex;
//查询数据范围参数索引
private final Integer rowBoundsIndex;
private final ParamNameResolver paramNameResolver;
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.returnsOptional = Optional.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
}
复制代码
~~~
MethodSignature是通过反射获取其方法描述信息,也就是签名,代码太多,仅通过其字段和构造方法看下吧。MethodSignature主要的逻辑是获取其返回值类型,比如单个、多个、Map等。
对SqlCommand和MethodSignature两个内部类有一定的认识后,我们继续看下MapperMethod#execute方法。execute方法会根据SqlCommandType处理不同的命令:
* INSERT、UPDATE、DELETE的处理方式应该说是基本相同,参数转换后调用对应的SqlSession方法去执行;
* 对于SELECT命令,需要根据返回值特点,转换参数、处理查询范围等,然后调用SqlSession不同的查询方法;
~~~java
public class MapperMethod {
//...
//方法执行逻辑
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判断命令类型:间接从MappedStatement获取的
switch (command.getType()) {
//insert数据
//INSERT、UPDATE、DELETE的处理方式一毛一样的;
//1、参数处理:把参数列表转为Map结构,同时会复制一份参数按照param0、param1这样put到Map
//2、调用SqlSession对应的方法执行命令,command.getName()就是我们示例中的com.raysonxin.dao.CompanyDao.selectById
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;
}
//查询命令:会根据返回值的类型调用不同的处理方法,进而路由到SqlSession不同的查询方法,在此之前需要做一些准备工作:
//selectOne、selectList、selectCursor等
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());
}
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;
}
//...
}
复制代码
~~~
MapperMethod#execute最终调用了SqlSession接口的方法,在我们的示例中会调用SqlSession#selectOne。
![image.png](data:image/svg+xml;utf8,)
走到这里是不是有点懵了,怎么调来调去,绕了一圈又回到SqlSession了?为啥不直接调用SqlSession接口呢?这里插一句:*直接使用SqlSession确实是可以,而且也是早起Mybatis所支持的方式;但是它不符合面向对象语言的概念和面向接口编程的编程习惯。由于面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增加了第二种使用MyBatis 支持接口(Interface)调用方式,即Mapper接口。*
*
这一段过程的主要工作就是:**采用动态代理方法,根据我们所调用的接口和方法,结合Configuration所加载的配置信息,动态的把请求路由到SqlSession对应的接口上来。**
*![image.png](data:image/svg+xml;utf8,)\_
## Sql命令执行
绕了一圈回到SqlSession后,才算是真正开始sql命令语句的执行。这个过程也是很复杂的,需要依次经过SqlSession、Executor、StatementHandler、ParamterHandler、ResultSetHandler等,流程较长,不能面面俱到,我们随着代码走一遍流程,然后通过后续的文章逐个补齐细节。先看个大体的流程图:
![image.png](data:image/svg+xml;utf8,)
### SqlSession
按照示例,DefaultSqlSession为具体实现,从DefaultSqlSession#selectOne()开始。
~~~java
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
//调用selectList方法,获取返回结果list
List<T> list = this.selectList(statement, parameter);
//如果结果刚好是1个,返回;若为多个,抛出异常;若为空,返回null;
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;
}
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
//中转一下,RowBounds给了默认值,不设限。
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//从configuration获取MappedStatement,
//示例中statement=com.raysonxin.dao.CompanyDao.selectById
MappedStatement ms = configuration.getMappedStatement(statement);
//通过执行器Executor执行查询
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();
}
}
复制代码
~~~
根据DefaultSqlSession代码走下来,从selectOne走到selectList,在selectList中调用了Executor#query方法。根据Executor的原理,当前executor为CachingExecutor实例,并且SimpleExecutor为其代理执行器(如下图),所以接下来进入Executor执行器的流程。
![image.png](data:image/svg+xml;utf8,)
### Executor
CachingExecutor是Executor的缓存设计,若缓存中存在所要查询的内容会直接返回,否则会交给其代理执行器通过数据库进行查询。从CachingExecutor#query(MappedStatement, Object, RowBounds, ResultHandler)开始:
~~~java
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//获取BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
//获取缓存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//调用重载方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//查询缓存
Cache cache = ms.getCache();
//缓存存在
if (cache != null) {
//如果需要刷新缓存
flushCacheIfRequired(ms);
//如果启用缓存
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
//数据为空还是要调用委托执行器的query
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//缓存不存在时,调用委托执行器的query
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
复制代码
~~~
我们看到如果未启用或者未命中缓存的情况,CachingExecutor会通过其委托执行器继续查询,也就是SimpleExecutor#query。SimpleExecutor继承自BaseExecutor,在BaseExecutor也同样具有缓存的处理逻辑,我们暂时跳过,仅贴出主要代码:
~~~java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//省略。。。
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//调用方法从数据库查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
//....
}
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 {
//执行doQuery,这个是抽象方法,具体实现在SimpleExecutor
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;
}
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
//获取configuration
Configuration configuration = ms.getConfiguration();
//创建StatementHandler,默认为PreparedStatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//创建并初始化statement
stmt = prepareStatement(handler, ms.getStatementLog());
//由StatementHandler执行查询,使用ResultSetHandler处理查询结果
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
//创建Statement,BaseStatementHandler#prepare
stmt = handler.prepare(connection, transaction.getTimeout());
//设置参数,使用ParameterHandler
handler.parameterize(stmt);
return stmt;
}
复制代码
~~~
Executor过程的执行就到这里了,CachingExecutor->BaseExecutor->SimpleExecutor,并且完成了StatementHandler执行的准备工作。CachingExecutor充当了缓存一层,当未命中缓存时会执行数据库查询,此时交给BaseExecutor及其子类执行。我们通过doQuery方法看到了StatementHandler创建、参数设置、执行与关闭整个生命周期,可以说Executor是StatementHandler的调度者。
从SimpleExecutor#doQuery、SimpleExecutor#prepareStatement方法看,可以把StatementHandler的工作分为几个过程:创建Statement对象、设置Statement参数、执行Statement查询并返回结果、关闭Statement对象。其中“设置Statement参数”需要使用ParameterHandler进行参数设置、“执行Statement查询并返回结果”需要使用ResultSetHandler处理查询结果。接下来重点看下参数设置、查询、结果处理三个过程。
### ParameterHandler
prepareStatement方法中,调用了StatementHandler#parameterize进行参数设置,我们看下PreparedStatementHandler的实现。
~~~java
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
复制代码
~~~
这里的parameterHandler是ParameterHandler实例。在mybatis中,ParameterHandler仅有一个默认实现,即:DefaultParameterHandler。所以,直接看DefaultParameterHandler#setParameters方法:
~~~java
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//获取参数映射列表,它来自SqlSource的解析过程,目前了解即可。
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//遍历参数映射
for (int i = 0; i < parameterMappings.size(); i++) {
//获取当前参数映射对象
ParameterMapping parameterMapping = parameterMappings.get(i);
//如果参数模式不是OUT,即IN、INOUT时,继续执行
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//获取参数名称:示例中为:id
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
}
//参数值为空,
else if (parameterObject == null) {
value = null;
}
//类型处理器是否存在当前参数类型。如果只有一个参数,这里一般会命中。
//我们编写sql语句时,没有标明入参类型,mybatis会把参数类型解析为Object
else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
}
//多个参数时,会走到这里。
else {
//转为MetaObject类似于一个map
MetaObject metaObject = configuration.newMetaObject(parameterObject);
//获取当前参数对应的值
value = metaObject.getValue(propertyName);
}
//获取类型处理器
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//通过类型处理器设置Statement参数。
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
复制代码
~~~
setParameters设置参数的过程比较简单:获取参数映射列表后,依次遍历参数映射对象,逐个参数类型获取类型处理器,然后使用类型处理器设置Statement参数。当然,这里面会设置到类型处理器的查询中转操作(UnKnown->Integer),大家可以调试看下过程。
### StatementHandler
参数设置完成后,就到了执行操作了。示例中将会走到PreparedStatementHandler#query,如下:
~~~java
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//转为PreparedStatement类型
PreparedStatement ps = (PreparedStatement) statement;
//执行
ps.execute();
//处理查询结果
return resultSetHandler.handleResultSets(ps);
}
复制代码
~~~
呃,这个太简洁了。把statement类型转为PreparedStatement,执行execute,处理查询结果并返回。Statement相关的内容属于传统JDBC范畴了,大家比较熟悉,不再赘述。
### ResultSetHandler
这应该是查询过程中的最后一步了!由PreparedStatementHandler#query方法可知,执行java.sql.PreparedStatement#execute后,mybatis使用ResultSetHandler#handleResultSets处理查询结果。mybatis对ResultSetHandler也仅有一个默认实现DefaultResultSetHandler,直接看代码吧:
~~~java
@Override
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;
//获取第一个结果集
ResultSetWrapper rsw = getFirstResultSet(stmt);
//获取结果映射
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
//如果rsw为空,或者resultMapCount<1,会抛出异常
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
//获取结果集映射对象
ResultMap resultMap = resultMaps.get(resultSetCount);
//获取结果,完成查询结果映射
handleResultSet(rsw, resultMap, multipleResults, null);
//处理下一个,多个结果时有用
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
//获取配置的结果集信息,我们没有使用,不用管了。
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);
}
复制代码
~~~
总结一下这个阶段的处理过程,看图吧,一图胜千言!
![image.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/19e503e00a0e45708a6d576cb40ac4c2~tplv-k3u1fbpfcp-zoom-1.image)
# 总结
本文从一个简答的Mapper查询方法开始,详细分析了其执行过程。按照我的个人理解,把执行过程分为了三个阶段:
* Mapper接口实例的动态实现阶段:Mybatis基于动态代理,动态实现Mapper接口,其实一个“披着Mapper皮的MapperProxy对象”;
* Mapper接口命令解析阶段:这个过程中基于接口、方法签名,结合反射技术、Configuration配置信息,确认了对应的SqlSession方法入口。
* SQL命令执行阶段:从SqlSession入口开始,依次经过Executor、StatementHandler、ParameterHandler、ResultSetHandler四大组件的层层处理,最终完成查询过程。
作者:码路印记
链接:https://juejin.cn/post/6879710920534884360
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 一.JVM
- 1.1 java代码是怎么运行的
- 1.2 JVM的内存区域
- 1.3 JVM运行时内存
- 1.4 JVM内存分配策略
- 1.5 JVM类加载机制与对象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面试相关文章
- 2.1 可能是把Java内存区域讲得最清楚的一篇文章
- 2.0 GC调优参数
- 2.1GC排查系列
- 2.2 内存泄漏和内存溢出
- 2.2.3 深入理解JVM-hotspot虚拟机对象探秘
- 1.10 并发的可达性分析相关问题
- 二.Java集合架构
- 1.ArrayList深入源码分析
- 2.Vector深入源码分析
- 3.LinkedList深入源码分析
- 4.HashMap深入源码分析
- 5.ConcurrentHashMap深入源码分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的设计模式
- 8.集合架构之面试指南
- 9.TreeSet和TreeMap
- 三.Java基础
- 1.基础概念
- 1.1 Java程序初始化的顺序是怎么样的
- 1.2 Java和C++的区别
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字节与字符的区别以及访问修饰符
- 1.7 深拷贝与浅拷贝
- 1.8 字符串常量池
- 2.面向对象
- 3.关键字
- 4.基本数据类型与运算
- 5.字符串与数组
- 6.异常处理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 数据流(Stream)
- 8.3 Java 8 并发教程:线程和执行器
- 8.4 Java 8 并发教程:同步和锁
- 8.5 Java 8 并发教程:原子变量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、数值、算术和文件
- 8.7 在 Java 8 中避免 Null 检查
- 8.8 使用 Intellij IDEA 解决 Java 8 的数据流问题
- 四.Java 并发编程
- 1.线程的实现/创建
- 2.线程生命周期/状态转换
- 3.线程池
- 4.线程中的协作、中断
- 5.Java锁
- 5.1 乐观锁、悲观锁和自旋锁
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平锁和非公平锁
- 5.3.1 说说ReentrantLock的实现原理,以及ReentrantLock的核心源码是如何实现的?
- 5.5 锁优化和升级
- 6.多线程的上下文切换
- 7.死锁的产生和解决
- 8.J.U.C(java.util.concurrent)
- 0.简化版(快速复习用)
- 9.锁优化
- 10.Java 内存模型(JMM)
- 11.ThreadLocal详解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的实现原理
- 1.DelayQueue的实现原理
- 14.Thread.join()实现原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的实际使用场景
- 五.Java I/O NIO
- 1.I/O模型简述
- 2.Java NIO之缓冲区
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之选择器
- 6.基于 Java NIO 实现简单的 HTTP 服务器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面试题
- 六.Java设计模式
- 1.单例模式
- 2.策略模式
- 3.模板方法
- 4.适配器模式
- 5.简单工厂
- 6.门面模式
- 7.代理模式
- 七.数据结构和算法
- 1.什么是红黑树
- 2.二叉树
- 2.1 二叉树的前序、中序、后序遍历
- 3.排序算法汇总
- 4.java实现链表及链表的重用操作
- 4.1算法题-链表反转
- 5.图的概述
- 6.常见的几道字符串算法题
- 7.几道常见的链表算法题
- 8.leetcode常见算法题1
- 9.LRU缓存策略
- 10.二进制及位运算
- 10.1.二进制和十进制转换
- 10.2.位运算
- 11.常见链表算法题
- 12.算法好文推荐
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事务管理
- 4.SpringMVC 运行流程和手动实现
- 0.Spring 核心技术
- 5.spring如何解决循环依赖问题
- 6.springboot自动装配原理
- 7.Spring中的循环依赖解决机制中,为什么要三级缓存,用二级缓存不够吗
- 8.beanFactory和factoryBean有什么区别
- 九.数据库
- 1.mybatis
- 1.1 MyBatis-# 与 $ 区别以及 sql 预编译
- Mybatis系列1-Configuration
- Mybatis系列2-SQL执行过程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-参数设置揭秘(ParameterHandler)
- Mybatis系列8-缓存机制
- 2.浅谈聚簇索引和非聚簇索引的区别
- 3.mysql 证明为什么用limit时,offset很大会影响性能
- 4.MySQL中的索引
- 5.数据库索引2
- 6.面试题收集
- 7.MySQL行锁、表锁、间隙锁详解
- 8.数据库MVCC详解
- 9.一条SQL查询语句是如何执行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能优化神器 Explain 使用分析
- 12.mysql中,一条update语句执行的过程是怎么样的?期间用到了mysql的哪些log,分别有什么作用
- 十.Redis
- 0.快速复习回顾Redis
- 1.通俗易懂的Redis数据结构基础教程
- 2.分布式锁(一)
- 3.分布式锁(二)
- 4.延时队列
- 5.位图Bitmaps
- 6.Bitmaps(位图)的使用
- 7.Scan
- 8.redis缓存雪崩、缓存击穿、缓存穿透
- 9.Redis为什么是单线程、及高并发快的3大原因详解
- 10.布隆过滤器你值得拥有的开发利器
- 11.Redis哨兵、复制、集群的设计原理与区别
- 12.redis的IO多路复用
- 13.相关redis面试题
- 14.redis集群
- 十一.中间件
- 1.RabbitMQ
- 1.1 RabbitMQ实战,hello world
- 1.2 RabbitMQ 实战,工作队列
- 1.3 RabbitMQ 实战, 发布订阅
- 1.4 RabbitMQ 实战,路由
- 1.5 RabbitMQ 实战,主题
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 实战 – 整合 RabbitMQ 发送邮件
- 1.8 RabbitMQ 的消息持久化与 Spring AMQP 的实现剖析
- 1.9 RabbitMQ必备核心知识
- 2.RocketMQ 的几个简单问题与答案
- 2.Kafka
- 2.1 kafka 基础概念和术语
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志机制
- 2.4 kafka是pull还是push的方式传递消息的?
- 2.5 Kafka的数据处理流程
- 2.6 Kafka的脑裂预防和处理机制
- 2.7 Kafka中partition副本的Leader选举机制
- 2.8 如果Leader挂了的时候,follower没来得及同步,是否会出现数据不一致
- 2.9 kafka的partition副本是否会出现脑裂情况
- 十二.Zookeeper
- 0.什么是Zookeeper(漫画)
- 1.使用docker安装Zookeeper伪集群
- 3.ZooKeeper-Plus
- 4.zk实现分布式锁
- 5.ZooKeeper之Watcher机制
- 6.Zookeeper之选举及数据一致性
- 十三.计算机网络
- 1.进制转换:二进制、八进制、十六进制、十进制之间的转换
- 2.位运算
- 3.计算机网络面试题汇总1
- 十四.Docker
- 100.面试题收集合集
- 1.美团面试常见问题总结
- 2.b站部分面试题
- 3.比心面试题
- 4.腾讯面试题
- 5.哈罗部分面试
- 6.笔记
- 十五.Storm
- 1.Storm和流处理简介
- 2.Storm 核心概念详解
- 3.Storm 单机版本环境搭建
- 4.Storm 集群环境搭建
- 5.Storm 编程模型详解
- 6.Storm 项目三种打包方式对比分析
- 7.Storm 集成 Redis 详解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初识ElasticSearch
- 2.文档基本CRUD、集群健康检查
- 3.shard&replica
- 4.document核心元数据解析及ES的并发控制
- 5.document的批量操作及数据路由原理
- 6.倒排索引
- 十七.分布式相关
- 1.分布式事务解决方案一网打尽
- 2.关于xxx怎么保证高可用的问题
- 3.一致性hash原理与实现
- 4.微服务注册中心 Nacos 比 Eureka的优势
- 5.Raft 协议算法
- 6.为什么微服务架构中需要网关
- 0.CAP与BASE理论
- 十八.Dubbo
- 1.快速掌握Dubbo常规应用
- 2.Dubbo应用进阶
- 3.Dubbo调用模块详解
- 4.Dubbo调用模块源码分析
- 6.Dubbo协议模块