通常,我们会为每个 Mapper.xml 配置文件创建一个对应的 Mapper 接口,并且无须提供这个接口的实现,在执行 SQL 语句时直接调用这个 Mapper 对象的方法执行即可。那 MyBatis 是如何通过这个 Mapper 接口来执行相应的 SQL 语句的呢?实际上,在 MyBatis 中,实现 Mapper 接口与 Mapper.xml 配置文件映射功能的是 binding 模块,其涉及的核心类如下图所示:
MapperRegistry
MapperRegistry 是 MyBatis 初始化过程中构造的一个对象,主要作用就是统一维护 Mapper 接口以及这些 Mapper 的代理对象工厂。在 MyBatis 初始化时,所有配置信息会被解析成相应的对象记录到 Configuration 对象中。其中,Configuration 的 mapperRegistry 属性就记录了当前使用的 MapperRegistry 对象。
下面我们先来看 MapperRegistry 中的核心字段:
// 指向MyBatis全局唯一的Configuration对象,其中维护了解析之后的全部MyBatis配置信息
private final Configuration config;
// 维护了所有解析的Mapper接口以及MapperProxyFactory工厂对象之间的映射关系
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
在 MyBatis 初始化时,会读取全部 Mapper.xml 配置文件,还会扫描全部 Mapper 接口中的注解信息,之后会调用 MapperRegistry 的 addMapper() 方法填充 knownMappers 集合。在填充 knownMappers 集合之前,MapperRegistry 会先保证传入的 type 参数是一个接口且是 knownMappers 集合没有加载过 type 类型,然后才会创建相应的 MapperProxyFactory 工厂并记录到 knownMappers 集合中。
public <T> void addMapper(Class<T> type) {
// 是否是接口
if (type.isInterface()) {
// 是否已经存在
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 添加到knownMappers集合中
knownMappers.put(type, new MapperProxyFactory<T>(type));
// Mapper对应的XML解析和注解处理
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
MapperProxyFactory
在需要执行某 SQL 语句时,会先调用 MapperRegistry 的 getMapper() 方法获取实现了 Mapper 接口的代理对象,然后通过代理对象执行具体的操作。这里会先拿到之前创建的 MapperProxyFactory 工厂对象,并调用其 newInstance() 方法创建 Mapper 接口的代理对象。底层是通过 JDK 动态代理的方式生成代理对象的,如下代码所示,这里使用的 InvocationHandler 实现是 MapperProxy。
public class MapperProxyFactory<T> {
@SuppressWarnings("unchecked")
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
通过分析 MapperProxyFactory 这个工厂类,我们可以清晰地看到 MapperProxy 是生成 Mapper 接口代理对象的关键,它实现了 InvocationHandler 接口,是执行 SQL 的关键。来看下 MapperProxy 中的核心字段:
// 记录了当前MapperProxy关联的SqlSession对象, 用于访问数据库
private final SqlSession sqlSession;
// Mapper接口类型,也是当前MapperProxy关联的代理对象实现的接口类型
private final Class<T> mapperInterface;
// key是Mapper接口中的方法对应的Method对象,value是该方法对应的MapperMethod对象
// MapperMethod对象会完成参数转换以及SQL语句的执行功能
private final Map<Method, MapperMethod> methodCache;
MapperProxy 中的 invoke() 方法是代理对象执行的主要逻辑,其实现如下:
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)) {
// 针对Java7以上版本对动态类型语言的支持
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 从缓冲中获取MapperMethod对象,如果缓冲中没有,则创建新的MapperMethod对象并添加到缓存中
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行SQL语句
return mapperMethod.execute(sqlSession, args);
}
MapperMethod
MapperMethod 中封装了 Mapper 接口中对应方法的信息,以及对应 SQL 语句的信息。我们可以将 MapperMethod 看作是连接 Mapper 接口与 Mapper.xml 配置文件中定义的 SQL 语句的桥梁。其核心字段如下代码所示:
// 记录了SQL语句的名称和类型
private final SqlCommand command;
// Mapper接口中对应方法的相关信息
private final MethodSignature method;
1. SqlCommand
SqlCommand 是 MapperMethod 中定义的内部类,它使用 name 字段记录了 SQL 语句的名称,使用 type 字段(SqlCommandType 枚举类型)记录了 SQL 语句的类型。SqlCommand 的构造方法会初始化 name 字段和 type 字段,代码如下:
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 获取Mapper接口中对应的方法名称
final String methodName = method.getName();
// 获取Mapper接口的类型
final Class<?> declaringClass = method.getDeclaringClass();
// 将Mapper接口名称和方法名称拼接起来作为SQL语句唯一标识,到Configuration这个全局配置对象中查找SQL语句
// MappedStatement对象就是Mapper.xml配置文件中一条SQL语句解析之后得到的对象
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
// 针对@Flush注解的处理
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 记录SQL语句唯一标识
name = ms.getId();
// 记录SQL语句的操作类型
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
这里调用的 resolveMappedStatement() 方法不仅会尝试根据 SQL 语句的唯一标识从 Configuration 全局配置对象中查找关联的 MappedStatement 对象,还会尝试顺着 Mapper 接口的继承树进行查找,直至找到为止。
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 将Mapper接口名称和方法名称拼接起来作为SQL语句唯一标识
String statementId = mapperInterface.getName() + "." + methodName;
// 检测Configuration中是否包含相应的MappedStatement对象
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
// 如果方法就定义在当前接口中,则证明没有对应的SQL语句,返回null
return null;
}
// 如果当前检查的Mapper接口(mapperInterface)中不是定义该方法的接口(declaringClass),则会从mapperInterface开始,沿着继承关系向上查找递归每个接口,
// 查找该方法对应的MappedStatement对象
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
2. MethodSignature
MethodSignature 也是 MapperMethod 中定义的内部类,其维护了 Mapper 接口中方法的相关信息。
// 表示方法返回值是否为Collection集合或数组
private final boolean returnsMany;
// 表示方法返回值是否为Map集合
private final boolean returnsMap;
// 表示方法返回值是否为void
private final boolean returnsVoid;
// 表示方法返回值是否为Cursor
private final boolean returnsCursor;
// 方法返回值的具体类型
private final Class<?> returnType;
// 如果方法的返回值为Map集合,则通过mapKey字段记录了作为key的列名。mapKey字段的值是通过解析方法上的@MapKey注解得到的
private final String mapKey;
// 记录了Mapper接口方法的参数列表中ResultHandler类型参数的位置
private final Integer resultHandlerIndex;
// 记录了Mapper接口方法的参数列表中RowBounds类型参数的位置
private final Integer rowBoundsIndex;
// 用来解析方法参数列表的工具类
private final ParamNameResolver paramNameResolver;
在上述字段中,需要着重讲解的是 ParamNameResolver 这个解析方法参数列表的工具类。在 ParamNameResolver 中有一个 names 字段(SortedMap
另外,names 集合会跳过 RowBounds 类型以及 ResultHandler 类型的参数,如果使用下标索引作为参数名称,在 names 集合中就会出现 KV 不一致的场景。例如下图就很好地说明了这种不一致的场景,其中 saveCustomer(long id, String name, RowBounds bounds, String address) 方法对应的 names 集合为 {{0, “0”}, {1, “1”}, {2, “3”}}。由于 RowBounds 参数的存在,第四个参数在 names 集合中的 KV 出现了不一致,即 key = 2 与 value = “3” 不一致。
在 ParamNameResolver 的构造方法中,会通过反射的方式读取 Mapper 接口中对应方法的信息,并初始化 names 集合,具体实现代码如下:
public ParamNameResolver(Configuration config, Method method) {
// 获取参数列表中每个参数的类型
final Class<?>[] paramTypes = method.getParameterTypes();
// 获取参数列表上的注解
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
// 该集合用于记录参数索引与参数名称的对应关系
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
// 如果参数是RowBounds或ResultHandler类型,则跳过该参数
if (isSpecialParameter(paramTypes[paramIndex])) {
continue;
}
String name = null;
// 遍历该参数对应的注解集合
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
// 获取@Param注解指定的参数名称
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// 获取原始参数名称,Configuration中的useActualParamName默认为true
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// 获取参数下标索引作为参数名称
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
// 保存到names集合中
names = Collections.unmodifiableSortedMap(map);
}
names 集合主要在 ParamNameResolver 的 getNamedParams() 方法中使用,该方法接收的参数是用户传入的实参列表,并将实参与其对应名称进行关联。
了解了 ParamNameResolver 的核心功能后,我们回到 MethodSignature 继续分析,在构造 MethodSignature 时会解析方法中的返回值、参数列表等信息,并会初始化上面介绍的几个核心字段。
3. 方法执行
介绍完 MapperMethod 中定义的内部类,我们回到 MapperMethod 继续分析。MapperMethod 中最核心的方法是 execute() 方法,它会根据 SQL 语句的类型调用 SqISession 对应的方法完成数据库操作。SqlSession 是 MyBatis 的核心组件之一,其具体实现我们后面会详细介绍。MapperMethod.execute() 方法的具体实现如下:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 根据SQL语句的类型调用SqlSession对应的方法
switch (command.getType()) {
case INSERT: {
// 使用ParamNameResolver处理args[]数组(用户传入的实参列表),将用户传入的实参与指定参数名称关联起来
Object param = method.convertArgsToSqlCommandParam(args);
// 调用SqlSession的insert方法,rowCountResult方法会根据method字段中记录的方法的返回值类型对结果进行转换
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
// UPDATE和DELETE类型的SQL语句的处理与INSERT类型的SQL语句相似
case UPDATE:
case DELETE:
......
case SELECT:
// 处理返回值为void且ResultSet通过ResultHandler处理的方法
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 处理返回值为集合或数组的方法
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 处理返回值为Map的方法
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 处理返回值为Cursor的方法
result = executeForCursor(sqlSession, args);
} else {
// 处理返回值为单一对象的方法
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
......
}
return result;
}
在 execute() 方法中,当执行 INSERT、UPDATE、DELETE 这三种类型的 SQL 语句时,其执行结果都需要经过 rowCountResult() 方法处理。比如:SqISession 中的 insert() 方法返回的是 int 值,rowCountResult() 方法会将该 int 值转换成 Mapper 接口中对应方法的返回值。多数场景中这个 int 值代表了 SQL 语句影响的数据行数,但这个返回值的具体含义会根据 MySQL 的配置有所变化。