特点
(1)Mapper文件中有Mapper接口映射关系的唯一标识,比如findById在接口中定义此方法,那么在mapper.xml肯定也有findById标签对应的sql模板,如果写错会在Mybatis启动的时候报错,达到提前校验的目的。
(2)Mapper接口在使用时不用为其实现接口,就可以自动绑定映射其对应的sql模板执行方法。在spring环境中也可以接口注入直接使用。这里注入的是Mapper接口的代理类。
这些功能就在mybatis框架的binding包下。
binging的核心组件及关系如下:
MapperRegistry
MapperRegistry是Mybatis初始化过程中构造的一个对象,主要作用是维护Mapper接口和其对应的MapperProxyFactory。
核心字段:
1 | public class MapperRegistry { |
addMapper和getMapper方法
addMapper是为Mapper接口添加对应的代理工厂到kownsMapper中。
1 | public <T> void addMapper(Class<T> type) { |
getMapper是获取Mapper接口的一个代理对象,也是通过获取到knownMappers map中的MapperFactoryProxy,然后通过newInstance方法来获取新的代理对象
1 | public <T> T getMapper(Class<T> type, SqlSession sqlSession) { |
MapperProxyFactory
MapperProxyFactory逻辑很简单,就是生成代理类的工厂。
其中核心字段为:
- mapperInterface 即要代理的 Mapper接口
- methodCache 用来存放Method和MapperMethod对象的键值对。 这里MapperMethod就是最终在MethodProxy中用于执行sql的地方。
1 | public class MapperProxyFactory<T> { |
MapperProxy
MapperProxy实现了InvocationHandler接口,用于拦截生成代理类。
代理逻辑是利用Method对应的MapperMethod去执行对应execut方法。
1 | public class MapperProxy<T> implements InvocationHandler, Serializable { |
MapperMethod
MapperMethod是最终执行sql的地方,也是存储了当前执行Mapper接口方法的Method对象。其中包含两个核心字段 sqlCommond、methodSignature。这两个都是其中的静态内部类。
SqlCommand
sqlCommand变量维护了关联sql语句的相关信息。
- name 即唯一标识
- type 标识是哪种类型的sql语句
其在构造函数中根据传入的Mapper接口和method方法来初始化SqlCommond。逻辑其实就是从传入接口或其父类中解析出MapperStatement对象,其能标识mapper.xml中的完整的一个sql模板。再从中解析出name和commandType。
1 | public static class SqlCommand { |
MethodSignature
MethodSignature主要维护了当前接口方法的信息,如返回值类型、参数和实际入参的绑定关系(运用了ParamNameResolver工具类)等。
在methodSignature.convertArgsToSqlCommandParam方法中,也是处理了@Param注解与sql模板中的参数绑定关系。
1 | public static class MethodSignature { |
execute方法
最终sql的执行都是通过MapperMethod的execute方法执行,这里依赖了其中的sqlCommond和methodSignature两个变量。
execute核心逻辑就是根据具体的sqlCommondType来选择执行具体的方法。其中也处理了不同的返回值
- 对于Insert、Update、delete类型的sql执行,返回值回采用rowCountResult方法来处理,内部做了对影响行数的处理(可以直接返回boolean类型)
对应Select类型,
- 如果有executeWithResultHandler类型的参数,会按照resultHandler的回调来处理返回值。
- 如果方法返回值为集合类型或是数组类型,则会调用 executeForMany() 方法,底层依赖 SqlSession.selectList() 方法进行查询,并将得到的 List 转换成目标集合类型。
- 如果方法返回值为 Map 类型,则会调用 executeForMap() 方法,底层依赖 SqlSession.selectMap() 方法完成查询,并将结果集映射成 Map 集合。
- 针对 Cursor 以及 Optional返回值的处理,也是依赖的 SqlSession 的相关方法完成查询的,这里不再展开。
- 针对单条数据,也会依赖sqlSession.selectOne方法完成查询。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71/**
* 执行映射接口中的方法 MapperMethod的核心方法
* execute方法会根据要执行的sql语句的具体类型执行sqlsession的具体方法完成数据库操作
* @param sqlSession sqlSession接口的实例,通过它可以进行数据库的操作
* @param args 执行接口方法时传入的参数
* @return 数据库操作结果
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) { // 根据SQL语句类型,执行不同操作
case INSERT: { // 如果是插入语句
// 将参数顺序与实参对应好
Object param = method.convertArgsToSqlCommandParam(args);
// 执行操作并返回结果
// rowCounntResult方法会根据方法的返回值类型对结果进行转换
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: { // 如果是删除语句MappedStatement
// 将参数顺序与实参对应好
Object param = method.convertArgsToSqlCommandParam(args);
// 执行操作并返回结果
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT: // 如果是查询语句
/**
* 如果在方法参数列表中有 ResultHandler 类型的参数存在,则会使用 executeWithResultHandler() 方法完成查询,底层依赖的是 SqlSession.select() 方法,结果集将会交由传入的 ResultHandler 对象进行处理。
* 如果方法返回值为集合类型或是数组类型,则会调用 executeForMany() 方法,底层依赖 SqlSession.selectList() 方法进行查询,并将得到的 List 转换成目标集合类型。
* 如果方法返回值为 Map 类型,则会调用 executeForMap() 方法,底层依赖 SqlSession.selectMap() 方法完成查询,并将结果集映射成 Map 集合。
* 针对 Cursor 以及 Optional返回值的处理,也是依赖的 SqlSession 的相关方法完成查询的,这里不再展开。
*/
if (method.returnsVoid() && method.hasResultHandler()) { // 方法返回值为void,且有结果处理器
// 使用结果处理器执行查询
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()) { // 游标类型结果查询
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()) {
// 查询结果为null,但返回类型为基本类型。因此返回变量无法接收查询结果,抛出异常。
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
总结
重点介绍了 MyBatis 中的 binding 模块,正是该模块实现了 Mapper 接口与 Mapper.xml 配置文件的映射功能。
首先,介绍了 MapperRegistry 这个注册中心,其中维护了 Mapper 接口与代理工厂对象之间的映射关系。
然后,分析了 MapperProxy 和 MapperProxyFactory,其中 MapperProxyFactory 使用 JDK 动态代理方式为相应的 Mapper 接口创建了代理对象,MapperProxy 则封装了核心的代理逻辑,将拦截到的目标方法委托给对应的 MapperMethod 处理。
最后,详细讲解了 MapperMethod,分析了它是如何根据方法签名执行相应的 SQL 语句。