在Mybatis的使用过程中,我们会为每个 Mapper.xml 配置文件创建一个对应的 Mapper 接口,例如,订单系统示例中的 CustomerMapper.xml 配置文件与 CustomerMapper 接口,定义完 CustomerMapper 接口之后,我们无须提供 CustomerMapper 接口实现,就可以直接调用 CustomerMapper 对象的方法执行 CustomerMapper.xml 配置文件中的 SQL 语句。
MyBatis 的前身是 iBatis,我们在使用 iBatis 的时候,如果想查询一个 Customer 对象的话,可以调用 SqlSession.queryForObject (“find”, customerId) 方法,queryForObject() 方法的这两个参数分别是要执行的 SQL 语句唯一标识(示例中就是定义在 CustomerMapper.xml 中的 id 为 find 的 SQL 语句),以及 SQL 语句执行时需要的实参(示例中就是顾客 ID)。
这里 SQL 语句的唯一标识是一个字符串,如果我们在写代码的时候,不小心写错了这个唯一标识,例如将“find”写成了“finb”,在代码编译以及 iBatis 初始化的过程中,根本发现不了这个问题,而是在真正执行到这行代码的时候才会抛出异常,这样其实对流量是有损的。
MyBatis 中的 Mapper 接口就可以很好地解决这个问题。
示例中的 CustomerMapper 接口中定义了 SQL 语句唯一标识同名的 find() 方法,我们在写代码的时候使用的是 CustomerMapper.find() 方法,如果拼写成 CustomerMapper.finb(),编译会失败。这是因为 MyBatis 初始化的时候会尝试将 CustomerMapper 接口中的 find() 方法名与 CustomerMapper.xml 配置文件中的 SQL 唯一标识进行映射,如果 SQL 语句唯一标识写错成“finb”,MyBatis 会发现这个错误,并在初始化过程中就抛出异常,这样编译器以及 MyBatis 就可以帮助我们更早发现异常,避免线上流量的损失。
在 MyBatis 中,实现 CustomerMapper 接口与 CustomerMapper.xml 配置文件映射功能的是 binding 模块,其中涉及的核心类如下图所示:
下面我们就开始详细分析 binding 模块中涉及的这些核心组件。
MapperRegistry 是 MyBatis 初始化过程中构造的一个对象,主要作用就是统一维护 Mapper 接口以及这些 Mapper 的代理对象工厂。
MapperRegistry类是Apache MyBatis应用程序中负责管理注册、加载和检索映射器的类。
MapperRegistry维护了一个已知映射器的映射,并提供了通过类型检索映射器、检查是否已知映射器、添加映射器和获取所有已注册映射器列表的方法。它还支持自动映射器注解加载。
下面我们先来看 MapperRegistry 中的核心字段。
在 MyBatis 初始化时,会读取全部 Mapper.xml 配置文件,还会扫描全部 Mapper 接口中的注解信息,之后会调用 MapperRegistry.addMapper() 方法填充 knownMappers 集合。在 addMapper() 方法填充 knownMappers 集合之前,MapperRegistry 会先保证传入的 type 参数是一个接口且 knownMappers 集合没有加载过 type 类型,然后才会创建相应的 MapperProxyFactory 工厂并记录到 knownMappers 集合中。
在我们使用 CustomerMapper.find() 方法执行数据库查询的时候,MyBatis 会先从MapperRegistry 中获取 CustomerMapper 接口的代理对象,这里就使用到 MapperRegistry.getMapper()方法,它会拿到前面创建的 MapperProxyFactory 工厂对象,并调用其 newInstance() 方法创建 Mapper 接口的代理对象。
MapperProxyFactory类,它用于创建MyBatis的映射器对象。该类接受一个接口类型作为参数,并创建一个实现了该接口的代理对象。该代理对象在调用接口方法时,会将调用转发到对应的Mapper对象进行处理。MapperProxyFactory类还提供了根据SqlSession实例创建映射器对象的方法。在创建映射器对象时,会使用ConcurrentHashMap来缓存方法与方法处理类的映射关系,以提高性能。最后,该类提供了一个私有方法newInstance,用于实际创建代理对象。
在 MapperRegistry 中会依赖 MapperProxyFactory 的 newInstance() 方法创建代理对象,底层则是通过 JDK 动态代理的方式生成代理对象的,如下代码所示,这里使用的 InvocationHandler 实现是 MapperProxy。
protected T newInstance(MapperProxy<T> mapperProxy) {
// 创建实现了mapperInterface接口的动态代理对象,这里使用的InvocationHandler 实现是MapperProxy
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[]{mapperInterface}, mapperProxy);
}
当使用MyBatis时,我们通常定义一个接口来表示数据库的Mapper,然后使用@Mapper注解来指示该接口是一个Mapper。在实际使用中,我们并不需要直接实现这个接口,而是通过使用Mapper代理来生成一个实现了该接口的对象。
Mapper代理的实现就是通过该代码完成的。 MapperProxy,它实现了InvocationHandler接口,该接口定义了一个invoke方法,用于处理方法调用。
在invoke方法中,首先检查方法是否属于Object类,如果是,则直接调用方法。否则,根据方法的不同,调用cachedInvoker方法来获取对应的MapperMethodInvoker对象,并调用其invoke方法来执行实际的数据库操作,并返回结果。
cachedInvoker方法中,首先根据方法来确定是否需要执行默认方法调用。如果方法不是默认方法,则通过mapperMethod.execute方法来执行数据库操作,并返回结果。如果方法是默认方法,则根据不同的Java版本来获取对应的MethodHandle对象,并通过bindTo和invokeWithArguments方法来执行方法调用,并返回结果。
MapperProxy
还定义了一个内部接口MapperMethodInvoker,用于表示不同的方法调用的实现。同时还定义了两个内部类PlainMethodInvoker和DefaultMethodInvoker,分别用于实现普通方法调用和默认方法调用的逻辑。
MapperProxy
通过使用Java的反射和Lambda表达式来动态生成实现了Mapper接口的对象,并根据不同的方法调用实现不同的逻辑,从而实现了一种高效、灵活的Mapper代理机制。
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
下面我们先来介绍一下 MapperProxy 中的核心字段。
这里涉及 MethodHandle 的内容,所以下面我们就来简单介绍一下 MethodHandle 的基础知识点。
当调用Mapper接口中的方法时,MapperMethod类会根据方法的注解类型和参数类型来确定执行的SQL类型,并根据返回值类型进行相应的处理。
对于查询类型的方法,它会将结果映射为Java对象返回;
对于插入、更新、删除类型的方法,它会执行相应的SQL语句并返回影响的行数。
MapperMethod类通过反射机制将参数值设置到SQL语句中的占位符中,并执行SQL语句。
根据返回值类型的不同,MapperMethod类会对结果进行相应的处理,将查询结果映射为Java对象,或将影响的行数作为Integer类型返回。
对于需要事务管理的方法,MapperMethod类会在方法执行前后进行事务的提交或回滚操作。
MapperMethod 的第一个核心字段是 command(SqlCommand 类型),其中维护了关联 SQL 语句的相关信息。在 MapperMethod$SqlCommand 这个内部类中,通过 name 字段记录了关联 SQL 语句的唯一标识,通过 type 字段(SqlCommandType 类型)维护了 SQL 语句的操作类型,这里 SQL 语句的操作类型分为 INSERT、UPDATE、DELETE、SELECT 和 FLUSH 五种。
public static class SqlCommand {
private final String name;
private final SqlCommandType type;
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);
}
}
}
定义了一个公共的静态的SqlCommand类,其中包含一个无参构造函数和两个私有成员变量name和type。构造函数根据传入的参数configuration、mapperInterface和method,解析MappedStatement并设置name和type的值。如果MappedStatement不存在且没有Flush注解,则抛出BindingException异常。如果MappedStatement存在,设置name和type的值。如果type为UNKNOWN,则抛出BindingException异常。
这里调用的 resolveMappedStatement() 方法不仅会尝试根据 SQL 语句的唯一标识从 Configuration 全局配置对象中查找关联的 MappedStatement 对象,还会尝试顺着 Mapper 接口的继承树进行查找,直至查找成功为止。具体实现如下:
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass,
Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
}
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;
}
}
这个函数用于解析MyBatis中的MappedStatement对象。它根据给定的mapper接口类、方法名、声明类和配置对象来确定一个MappedStatement对象。首先根据mapper接口和方法名拼接得到一个唯一标识符,然后检查配置对象中是否存在该MappedStatement对象,如果存在则直接返回。如果mapper接口和声明类相同,则直接返回null。否则,遍历接口的所有超类,如果声明类能够从超类实例化,则递归调用该函数。如果找到对应的MappedStatement对象则返回。如果以上条件都不满足,则返回null。
MapperMethod 的第二个核心字段是 method 字段(MethodSignature 类型),其中维护了 Mapper 接口中方法的相关信息。
public static class MethodSignature {
private final boolean returnsMany;
private final boolean returnsMap;
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;
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对象。该方法接受一个Configuration对象、一个mapper接口和一个方法作为输入参数。它首先解析方法的返回类型,通过使用TypeParameterResolver类的resolveReturnType方法,根据方法和mapper接口的类型参数进行解析。解析结果存储在resolvedReturnType变量中。然后根据解析结果的类型,设置返回类型的类型。如果解析结果是Class类型的,则将结果转换为Class类型,并存储在returnType变量中。如果解析结果是ParameterizedType类型的,则将结果转换为Class类型,并存储在returnType变量中。否则,将方法的返回类型(即方法声明的类型)直接存储在returnType变量中。
接下来,该方法检查返回类型是否为void,如果是,则将returnsVoid变量设置为true。然后,它使用configuration.getObjectFactory().isCollection方法检查返回类型是否是一个集合或数组,如果是,则将returnsMany变量设置为true。接着,它检查返回类型是否为Cursor类,如果是,则将returnsCursor变量设置为true。然后,它检查返回类型是否为Optional类,如果是,则将returnsOptional变量设置为true。
然后,该方法调用getMapKey方法,传递方法作为参数,以获取返回类型是否为Map的关键字。获取到的关键字存储在mapKey变量中。
接下来,它使用getUniqueParamIndex方法,传递方法和RowBounds类作为参数,以获取方法中唯一的RowBounds参数的索引。获取到的索引存储在rowBoundsIndex变量中。然后,它再次使用getUniqueParamIndex方法,传递方法和ResultHandler类作为参数,以获取方法中唯一的ResultHandler参数的索引。获取到的索引存储在resultHandlerIndex变量中。
最后,该方法创建一个ParamNameResolver对象,传递configuration和method作为参数,以解析方法的参数名称。创建的ParamNameResolver对象存储在paramNameResolver变量中。
该函数的目的是解析方法的返回类型和参数类型,并将解析结果存储在相应的变量中,以便后续使用。
首先是 Mapper 接口方法返回值的相关信息,涉及下面七个字段。
接下来是与 Mapper 接口方法的参数列表相关的三个字段。
在 ParamNameResolver 中有一个 names 字段(SortedMap类型)记录了各个参数在参数列表中的位置以及参数名称,其中 key 是参数在参数列表中的位置索引,value 为参数的名称。我们可以通过 @Param 注解指定一个参数名称,如果没有特别指定,则默认使用参数列表中的变量名称作为其名称,这与 ParamNameResolver 的 useActualParamName 字段相关。useActualParamName 是一个全局配置。
如果我们将 useActualParamName 配置为 false,ParamNameResolver 会使用参数的下标索引作为其名称。另外,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” 不一致)。
完成 names 集合的初始化之后,我们再来看如何从 names 集合中查询参数名称,该部分逻辑在 ParamNameResolver.getNamedParams() 方法,其中会将 Mapper 接口方法的实参与 names 集合中记录的参数名称相关联,其核心逻辑如下:
public Object getNamedParams(Object[] args) {
// 获取方法中非特殊类型(RowBounds类型和ResultHandler类型)的参数个数
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null; // 方法没有非特殊类型参数,返回null即可
} else if (!hasParamAnnotation && paramCount == 1) {
// 方法参数列表中没有使用@Param注解,且只有一个非特殊类型参数
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
// 处理存在@Param注解或是存在多个非特殊类型参数的场景
// param集合用于记录了参数名称与实参之间的映射关系
// 这里的ParamMap继承了HashMap,与HashMap的唯一不同是:
// 向ParamMap中添加已经存在的key时,会直接抛出异常,而不是覆盖原有的Key
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 将参数名称与实参的映射保存到param集合中
param.put(entry.getValue(), args[entry.getKey()]);
// 同时,为参数创建"param+索引"格式的默认参数名称,具体格式为:param1, param2等,
// 将"param+索引"的默认参数名称与实参的映射关系也保存到param集合中
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
这个函数目的是从一个Object类型的数组参数中获取命名参数,并返回一个包含这些参数的Map对象。函数的输入参数是一个Object类型的数组args,表示函数的参数。函数首先检查参数数组是否为空或参数数量为0,如果是,则返回null。接下来,函数检查是否设置了@Param注解并且参数数量为1,如果是,则将参数数组中的第一个元素包装为Map对象,并返回。否则,函数创建一个空的ParamMap对象,然后遍历参数数组的名称和索引,并将名称和对应的值放入ParamMap中。同时,函数还会添加一些通用的参数名到ParamMap中。最后,函数返回ParamMap对象作为结果。
ParamNameResolver这个类主要用于处理MyBatis的参数注解,包括@Param注解。它会解析Method的参数注解,生成参数名称的映射关系,并提供方法用于获取参数名称和生成命名参数对象。
在生成命名参数对象时,如果参数数组为null或参数名称映射为空,则返回null。
如果参数数组只有一个参数且没有指定名称,则将参数对象直接放入结果参数对象中。
如果参数数组有多个参数,则会根据名称映射给每个参数对象命名,并添加默认的参数序号名称。
对于参数对象是Collection或数组类型的情况,会将其封装成ParamMap对象,并根据参数对象类型添加相应的属性。
分析完 MapperMethod 中的几个核心内部类,我们回到 MapperMethod 继续介绍。
execute() 方法是 MapperMethod 中最核心的方法之一。execute() 方法会根据要执行的 SQL 语句的具体类型执行 SqlSession 的相应方法完成数据库操作,其核心实现如下:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) { // 判断SQL语句的类型
case INSERT: {
// 通过ParamNameResolver.getNamedParams()方法将方法的实参与
// 参数的名称关联起来
Object param = method.convertArgsToSqlCommandParam(args);
// 通过SqlSession.insert()方法执行INSERT语句,
// 在rowCountResult()方法中,会根据方法的返回值类型对结果进行转换
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
// 通过SqlSession.update()方法执行UPDATE语句
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
// DELETE分支与UPDATE类似,省略
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
// 如果方法返回值为void,且参数中包含了ResultHandler类型的实参,
// 则查询的结果集将会由ResultHandler对象进行处理
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// executeForMany()方法处理返回值为集合或数组的场景
result = executeForMany(sqlSession, args);
} else ...// 省略针对Map、Cursor以及Optional返回值的处理
}
break;
// 省略FLUSH和default分支
}
return result;
}
在 execute() 方法中,对于 INSERT、UPDATE、DELETE 三类 SQL 语句的返回结果,都会通过 rowCountResult() 方法处理。我们知道,上述三种类型的 SQL 语句的执行结果是一个数字,多数场景中代表了 SQL 语句影响的数据行数(注意,这个返回值的具体含义根据 MySQL 的配置有所变化),rowCountResult() 方法会将这个 int 值转换成 Mapper 接口方法的返回值,具体规则如下:
接下来看 execute() 方法针对 SELECT 语句查询到的结果集的处理。
重点介绍了 MyBatis 中的 binding 模块,正是该模块实现了 Mapper 接口与 Mapper.xml 配置文件的映射功能。
到这里,你应该就能回答开篇的那几个疑惑了吧?我这里也总结一下。