三、MyBatis的核心对象
3.1、核心对象
3.1.1、MappedStatement
MyBatis其实是对JDBC的进一步封装,我们都知道JDBC有几个重要的对象:
- Statement
- Prepared Statement
- Callable Statement
- ResultSet
Statement、Prepared Statement、Callable Statement分别与数据与进行交互,最终执行返回的结果由ResultSet进行封装。而SqlSession是对上述步骤进行进一步的封装。
mybatis-config.xml最终被封装成了Configuration对象。我们点啊看iabatis的Configuration类可以发现,environment标签都封装在了Configuration这个类的Environment属性当中,而mapper.xml文件中的一个一个的标签一般会被封装在MappedStatement这个对象中,这就注定了一个Mybatis应用中会有N个MappedStatement对象。

还有一个我们很常见的二级缓存的配置在Configuration类中也有对应的属性。我们可以发现他的默认值为true,所以这个属性我们写不写都可以,因为是默认开启的。

而我们写别名的<typeAlioases>标签被mybatis封装成的对象是TypeAliasRegistry

对于mapper.xml文件的注册,我们可以封装在<mappers>这个标签里面,而被mybatis封装的对象是loadedResources。

而最重要的是写sql语句的mapper.xml文件,在Configuration对象中也做了汇总进行封装。

那么此时问题来了,我们写在标签里面的sql语句最终会被封装到哪里了呢?因为sql语句是写在标签里面的,每一个标签都被封装成了一个个的MappedStatement对象,所以我们需要在MappedStatement对象里面去找。我们往下翻会发现一个getBoundSql方法。

我们会发现他的返回值是叫一个BoundSql的对象,这个对象其实就是MyBatis对sql语句的封装。我们点进去看看这个对象,他有这么几个属性。我们来挨个分析一下。

- private final String sql:用于封装我们写的sql
- 其他的用于封装各种参数。
3.1.2、Executor
Executor是MyBatis中处理功能的核心,对应增删改Executor提供了对应的方法来执行这些操作。
我们打开源码可以发现,Executor是一个接口(一般涉及到操作相关的类型,尽量设计成接口),我们点住alt+7可以打开大纲,看看所有的方法。

我们可以看到他主要有两类方法:
- update:对应的是sql中的增删改,只要是对于数据库有改动的操作都统一归为update
- query:对应的是sql中的查询操作。
- commit、rollback、getTransaction:与事务相关的操作,包括提交、回滚等。
- createCacheKey、isCached:与缓存相关的操作。
Executor接口有3个比较重要的实现:
- BatchExecutor:批处理操作,一次链接,执行多条sql。
- ReuseExecutor:复用Statement,只要你的sql一样,用的Statement就是一样的。这个Executor比较少用,因为我们很少执行同样的sql,同类型的sql哪怕参数不同都不叫一样的sql
- SimpleExecutor:最常用的Executor,也是MyBatis默认的Executor。

3.1.3、StatementHandler
StatementHandler是MyBatis封装的JDBC的Statement,MyBatis访问数据库操作真正的核心。我们来看一下StatementHandler的源码。

可以看到都是一些简单的增删改查操作。
3.1.4、ParamentHandler
ParamentHandlerd的作用是把处理参数,把MyBatis的参数替换成底层JDBC的参数。
3.1.5、ResultSetHandler
ResultSetHandler封装的是JDBC的ResultSet。他的作用是对JDBC中查询结果集ResultSet进行封装。

3.1.6、TypeHandler
用于处理数据库类型与Java类型之间转换的过程。
3.1.7、总结
作为总结,我们就来简单的跟一下insert的源码。
SqlSession的insert方法。

DefaultSqlSessionSession的insert方法,我们可以发现insert方法调用的其实是update方法。

还是在DefaultSqlSessionSession同一个类中的update方法。

接着走到了Executor的update方法,注意这个Executor是默认的SimpleExecutor

3.2、MyBatis动态代理
我们在写MyBatis的时候会想一个问题,为什么xxxDao接口没有实现类却可以实现对应的操作?其实答案很简单,因为在MyBatis的内部采用了动态代理的技术,在JVM运行时那么此时有两个问题:
- 如何创建Dao接口的实现类?
- 实现类是如何进行实现的?
一般来说需要实现动态代理有以下的几种场景:
- 为原始对象(目标)增加一些额外功能
- 远程代理
- 接口实现类,我们看不见的实实在在的类文件,但是在运行中却可以体现出来,典型的无中生有。
我们来看看MyBatis动态代理的源码,他有两个核心的类:
- MapperProxy
- MapperProxyFactory
3.2.1、MapperProxyFactory

我们可以看到MapperProxyFactory中有一个属性是mapperInterface,MapperProxyFactory实际上对应的就是我们自己写的xxxDao接口,他肯定会调用的方法是newInstance方法。
typescript
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<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
接着我们就去看MapperProxy这个类,根据以前学习的动态代理我们可以知道,MapperProxy一定会实现InvocationHandler

我们找到对应的invoke方法去看看。
vbnet
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
看到有一个cachedInvoker方法,我们再追进去看。
php
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (!m.isDefault()) {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
里面有一个很重要的对象,那就是MapperMethod这个对象。我们点进去MapperMethod这个类看看。可以发现他有两个很重要的属性。
arduino
private final SqlCommand command;
private final MethodSignature method;
为了搞清楚这两个属性的作用,我们来看看SqlCommand的构造方法。
ini
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) == null) {
throw new BindingException(
"Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName);
}
name = null;
type = SqlCommandType.FLUSH;
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
我们可以注意到,name这个属性被赋值成了id,而这个id就是namespace命名空间的id。而他的type表明的是这条sql是insert还是delete还是update还是select,以此来对应不同的SqlSession方法。

我们再来看他的第二个属性MethodSignature对象。
arduino
private final boolean returnsMany; // 返回值是否是多个
private final boolean returnsMap; // 你返回的是否是一个map
private final boolean returnsVoid; // 是否没有返回值
private final boolean returnsCursor;
private final boolean returnsOptional; // 返回值类型
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex; // 分页
private final Integer rowBoundsIndex;
private final ParamNameResolver paramNameResolver; // 你的参数是什么
我们可以发现MethodSignature主要针对的是返回值、分页和参数。我们接下来看看参数名的解析器ParamNameResolver,看看他是如何解析参数的。
在看这个代码的源码的时候,我们发现了一个老朋友,那就是@Param
注解,如果有@Param
注解的话,就可以通过注解来获取注解参数的值。

看完这个注解后,我们再回到MapperProxy这个类里面,有一个invoke重载的方法。
typescript
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
我们来看看是如何执行sql语句的,这下就要去点进去execute方法。当你点进去这个方法以后就真的一目了然了。
ini
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());
}
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;
}
在execute方法中,根据不同的case对不同的操作来作区分。我们拿insert来距离,如果操作是insert的话,就可以开始准备参数了。
ini
Object param = method.convertArgsToSqlCommandParam(args);
然后就可以通过调用sqlSession.insert方法来执行insert插入操作。而sqlSession.insert这个方法还需要两个参数,第一个参数是通过command.getName()来获取namespaceId,第二个参数是插入的时候传进来的参数。
ini
result = rowCountResult(sqlSession.insert(command.getName(), param));
最后来看一下比较复杂的查询操作。
ini
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;
他分了好多种情况:
- 如果方法的返回值为空同时他又有返回值的话就执行
executeWithResultHandler(sqlSession, args);
- 如果方法的返回值是一个list即表示她会返回很多个的时候的话,就会执行
result = executeForMany(sqlSession, args);
- 如果返回值是一个map的话,他会执行
result = executeForMap(sqlSession, args);
- 如果都不满足的话,最终会执行
sqlSession.selectOne(command.getName(), param);
我们可以发现,无论是什么操作,最终的落脚点一定都是sqlSession的相关操作。