背景
上一篇中讲了代理对象的方法调用,实际是通过mapperProxy.invoke(proxyObj, method, args)来完成的。在执行过程中,需要先判断方法是不是Object类定义的。
- 若方法是Object类定义的方法,则直接执行。
- 若不是,则需要创建对应的invoker来处理,创建逻辑由lambda表达式mapperFunction来定义:对于大多数接口方法而言,他们是非default的,为其创建PlainMethodInvoker,并存入到cachedMethod中。
在创建PlainMethodInvoker的时候,先要创建其核心对象MapperMethod,MapperMethod的包含两个重要属性:
- sqlCommand: 用于指定方法名称和操作类型(select/update/insert...),
- 方法签名methodSignature: 为MapperMethod指定resultHandlerIndex(用于找到resultHandler)和paramNameResolver(用于参数名解析)。
最后调用方法执行的时候,PlainMethodInvoker.invoke(proxy, method, args, sqlSession),实际上就是调用了mapperMethod.execute(sqlSession, args)来获取到结果。
mapperMethod.execute(sqlSession, args)
在execute的时候,有一些问题需要考虑:execute方法做了哪些事?参数是如何被解析到sql中的?这个execute()过程中mybatis是怎样与数据库交互的获取到结果集的?Object类型的结果集又是怎么被封装成了指定的类型的?ResultHandler是个什么东西,它又是怎样处理返回结果的?
在本文中,以查询一个List<Entity>来进行分析。创建sqlConmmand之后,就知道该sql是SELECT类型的。mapperMethod解析方法签名的时候,就已经将方法返回值类型、结果处理器和参数名解析器准备好了。所以现在可以轻松知道该方法返回结果为many,进入result = this.executeForMany(sqlSession, args)分支。
public class MapperMethod {
private final MapperMethod.SqlCommand command;
private final MapperMethod.MethodSignature method;
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
case INSERT:
//将方法的参数解析为sql语句的参数
param = this.method.convertArgsToSqlCommandParam(args);
//解析返回结果
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
//对于增删改而言,方法的返回结果比较固定,就是影响的行数。
//但对于查找来说,返回结果则复杂一点
case SELECT:
//如果返回void类型且定义了ResultHandler,那么需要resultHandler去处理查找结果并返回void。
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
//返回结果为多条记录
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
//返回结果为Map
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
//返回游标位置
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
//常规查找, 先解析参数, 若结果为null或返回结果类型与方法的返回类型不一致,则返回Optional.ofNullable.(看似非空,实则为空,避免空指针异常)
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
......
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
Object param = this.method.convertArgsToSqlCommandParam(args);
List result;
if (this.method.hasRowBounds()) {
RowBounds rowBounds = this.method.extractRowBounds(args);
result = sqlSession.selectList(this.command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(this.command.getName(), param);
}
if (!this.method.getReturnType().isAssignableFrom(result.getClass())) {
return this.method.getReturnType().isArray() ? this.convertToArray(result) : this.convertToDeclaredCollection(sqlSession.getConfiguration(), result);
} else {
return result;
}
}
先说一下executeForMany大致的执行流程:
-
利用方法签名来解析参数
-
判断是否有RowBounds, RowBounds的主要作用是进行分页。
- 如果有,先把分页参数提取出来,提取完进行selectList
- 没有则直接selectList
-
selectList完之后就已经产生结果集了,现在要判断是否可以将返回结果集赋值给方法的返回值类型。
- 可以则判断一下返回值类型:如果是返回数组类型则将结果转数组,是Collection类型则转为对于的Collection
- 不可以的话,直接将result返回。
每一步具体过程如下:
1. 解析参数
mapperMethod ——>方法签名 ——>paramNameResolver,一层层委派。
public Object convertArgsToSqlCommandParam(Object[] args) {
return this.paramNameResolver.getNamedParams(args);
}
具体承担参数解析责任的是paramNameResolver,其解析方法如下。
public class ParamNameResolver {
public static final String GENERIC_NAME_PREFIX = "param";
// 以sortedMap存放参数名
private final SortedMap<Integer, String> names;
private boolean hasParamAnnotation;
public Object getNamedParams(Object[] args) {
int paramCount = this.names.size();
if (args != null && paramCount != 0) {
//没有@Param注解且参数名只有1个,那么直接返回参数args[map.firstkey()]
if (!this.hasParamAnnotation && paramCount == 1) {
return args[(Integer)this.names.firstKey()];
} else {
// 将参数名与参数值对应起来,存放到paramMap中。
// 需要遍历names中的每一个节点,获取参数名paramName和该参数名在参数数组中的位置index,
// 然后以paramName: args[index]存放到paramMap内。
Map<String, Object> param = new ParamMap();
int i = 0;
for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
Entry<Integer, String> entry = (Entry)var5.next();
param.put((String)entry.getValue(), args[(Integer)entry.getKey()]);
String genericParamName = "param" + (i + 1);
if (!this.names.containsValue(genericParamName)) {
param.put(genericParamName, args[(Integer)entry.getKey()]);
}
}
return param;
}
} else {
return null;
}
该方法就是将参数名和参数值对应起来,举例如下。
举个例子来说:在candidates表中有一个方法来筛选候选人
public List<Candidate> getCandidatesByCondition(@Param("age") int age,
@Param("workYears") int workYears,
@Param("educationLevel") int educationLevel);
其names假设为{0: "age", 1: "workYears", 2: "educationLevel"}
调用传入参数为getCandidatesByCondition(30, 3, 2);
那么得到的param = {"age" : 30, "workYears" : 3, "educationLevel" : 2}
2. 是否分页
// MethodSignature.hasRowBounds()
public boolean hasRowBounds() {
return this.rowBoundsIndex != null;
}
// 若分页,则根据rowBoundsIndex从参数数组中提取出分页的参数。
3. sqlSession.selectList()查询结果集。
绕了一大圈,又回到了sqlSession中。(想想绕了一圈框架做了什么?我们得到了什么?搞了个代理对象,创建了InvocationHandler(即mapperProxy对象),然后又创建了MapperMethod对象,最后就将sql语句和接口方法名对应起来了(sqlCommand.name<-->MappedStatements {name: MappedStatement}),方法参数也解析了,返回结果类型也已经很明确了。)
但是呢,有些事情还没有做,比如sql语句中还有些占位符没搞定(现在的sql还是 select ... from table where xx = ? and zz = ?, 存放在sqlSource中,如下面的代码块),而我们想要的是先创建preparedStatement,然后再由jdbc去完成数据的查询。现在还差"?"对应的实际参数呢。数据库连接也没创建,语句还没执行,那事务也还没开启。前面准备工作都做好了,现在就要开始着手处理这些问题了。
还是以筛选候选人方法对应的Mappedstatement来说:
它在Mappedstatement中的key是namespace + 标签的id : "org.orange.dao.CandidateDao.getCandidatesByCondition"
其sqlSource的属性是:
{
sql: "select * from candidates where work_years >= ? and education_level >= ? and age <= ?"
parameterMappings:[
{property: "workYears", JavaType:{...}, ...},
{property: "educationLevel", JavaType:{...}, ...},
{property: "age", JavaType:{...}, ...}
],
...
}
1. 通过sqlCommand的name就可以查到这个Mappedstatement了
2. 这条sql语句后面应该再添上参数列表才成了preparedStatement的样子,根据parameterMappings和pareameterMap
可以将参数对应起来,从而产生参数列表。
具体做法请看 3.1
selectList流程如下:
3.1. 根据sqlCommand的name获取mappedStatement.
之前sql语句的信息都存放到了configuration对象的MappedStatements中,其key为xxMapper.xml中sql语句的namespace+id。现在我们不是在sqlCommand中存放了方法名(类名+方法名)吗,根据它和sql语句的对应关系就可以取出MappedStatement。
从这里就可以理解为什么mapper接口的方法不可以重载也不可以有不同返回值了:因为要确定一个方法的两个属性是方法名和方法签名。而mybatis中方法与sql语句的映射仅仅是通过方法名和sql的namespace+id一一对应的。
同时,也可以理解sql语句中的占位符应该怎样被替换成方法参数:在查询的时候还需要将原来的占位符替换成参数列表中的值。参数列表在参数解析之后就存到了paramMap中,key是参数名,它和xml中sql语句里#{}中的字符串是对应的。现在#{}中的字符串存放在了mappedStatement中的parameterMappings数组内。只需要在遇到第i个"?"的时候去statementparamMap中查它的property, 将其作为key,然后查paramMap中该key对应的value,最后将value放到字符串最后参数对相应位置就完成了参数的替换,即生成了preparedStatement对象。
如图:mapper接口方法与mapper.xml文件终于走到了一起(撒花)

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
//根据sqlCommand的name属性取出sql语句的信息MappedStatement
MappedStatement ms = this.configuration.getMappedStatement(statement);
//executor完成数据库操作
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
3.2.激动人心的时刻来了,是的,我们开始查数据了。
executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
不过先得搞明白两件事情:
- 在调用前先对参数parameter做了一次包装,这次包装做了什么事
- executor是哪个对象
第一个问题: wrapCollection的逻辑很简单,创建一个map,判断参数是什么类型的:容器、List、数组还是Object?
如果是前三个,则以类型为key,当前object为value存到map中并返回该map,如果是Object类的,就直接返回该object。
private Object wrapCollection(Object object) {
DefaultSqlSession.StrictMap map;
if (object instanceof Collection) {
map = new DefaultSqlSession.StrictMap();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
map = new DefaultSqlSession.StrictMap();
map.put("array", object);
return map;
} else {
return object;
}
}
第二个问题:在看query方法的执行逻辑之前,先了解一下Executor接口,毕竟人家是核心接口。它的UML结构如下

一开始我们在创建sqlSession的时候,使用的是SimpleExecutor, 由于开启了缓存,因此在executor创建完成之后,为其包装了一个CachingExecutor.因此sqlSession中的executor是CachingExecutor,它具有原来executor的全部功能。
// newExecutor(tx, exeType), exeType=Simple
//...
if (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}
//CachingExecutor构造方法
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
//CachingExecutor的delegate 就是传过来的SimpleExecutor
public void setExecutorWrapper(Executor wrapper) {
this.wrapper = wrapper;
}
现在我们内存中有两个Executor,分别是cachingExecutor和baseExecutor,他们的关系是你中有我,我中有你。

再来看query方法,当前query方法的实际调用者是CachingExecutor。

cachingExecutor.query()方法:
- 首先完成了参数绑定,将sql语句信息存放到boundSql中,
- 然后创建缓存key
- 最后执行query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
3.2.1. getBoundSql方法
在前面我们通过sqlCommand.name取出了mappedStatements中的mappedStatement对象,关于sql语句的信息存放在了该对象中,例如sql语句和参数映射存放在了sqlSource属性内。还是以前面的图来说明。

现在要做的事情就是将sql和参数进行绑定,也就是将参数放到sql语句后面,创建一个列表,第i个"?"对应的参数就是在paramMap中以第i个property为key查出来的参数值。
所以boundSql对象相比较sqlSource而言,就是parameterObject不为null, 它内部就是paramMap。根据sql, paramMap和paramMappings三个对象就可以构建出preparedStatement了。
public BoundSql getBoundSql(Object parameterObject) {
//sqlSource存储的是原始的sql语句,其sql属性就是<select>标签下的字符串。
//首先sqlSource来完成boundSql 创建,将parameterMappings传入boundSql.
BoundSql boundSql = this.sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//根据参数映射和参数对象,完成参数的绑定
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(this.configuration, boundSql.getSql(), this.parameterMap.getParameterMappings(), parameterObject);
}
Iterator var4 = boundSql.getParameterMappings().iterator();
// 判断参数映射是否有嵌套的resultMap并将结果写回到mappedStatement对象中
while(var4.hasNext()) {
ParameterMapping pm = (ParameterMapping)var4.next();
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = this.configuration.getResultMap(rmId);
if (rm != null) {
this.hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
3.2.2. 创建cacheKey
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
找到缓存key,如果使用缓存,先查缓存。这点暂时先放着。
3.2.3. query(..., boundSql)
如果使用缓存,先查缓存,查不到的话查数据库,然后将结果写缓存。
缓存查不到或者不允许使用缓存,使用simpleExecutor的query查数据库(实际就是调用其父类BaseExecutor的query方法)。
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) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
有点累了,BaseExecutor.query(..., boundSql)下一篇再发吧
做一下总结,mapperMethod.execute()方法中做了这样一些事情:
- 根据mapperMethod的sqlCommand和methodSignature两个属性,确定执行sql的类型增删改查,查询又分返回多个结果、单个结果、返回map和返回cursor.
- 解析方法参数,根据是否有@param注解和参数个数决定怎么解析
- 生成paramMap,将参数名: 参数值存入到paramMap。 或
- 没有@Param注解且参数名只有1个,那么直接返回第一个参数
- 判断是否分页, 分页参数处理
- 执行sqlSession.select/update
- 根据sqlCommand的name获取mappedStatement
- query()
- 参数绑定
- cacheKey
- 继续调query(..., boundSql):先尝试从缓存获取,没有缓存、获取不到、不允许使用缓存时,从数据库中查
网友评论