Mybatis源码分析之binding 模块分析
为什么使用mapper接口就能对数据库进行访问?
在回答这个问题之前我们先看一下Mybatis编程方式与传统的Ibatis的区别 :
@Test
// ibatis编程模型 本质分析
public void originalOperation() throws IOException {
// 2.获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.执行查询语句并返回结果 通过nameSpace 加id来确认接口
TUser user = sqlSession.selectOne("com.enjoylearning.mybatis.mapper.TUserMapper.selectByPrimaryKey", 2);
System.out.println(user.toString());
}
//mybatis编程方式
@Test
public void testAutoMapping() throws IOException {
// 2.获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.获取对应mapper
//sqlSession到底把请求转发给谁?
//怎么找到相对应的id ?
//怎么传参数的 ?
//给你一个接口,返回一个实现类
TUserTestMapper mapper = sqlSession.getMapper(TUserTestMapper.class);
// 4.执行查询语句并返回多条数据
List<TUser> users = mapper.selectAll();
for (TUser tUser : users) {
System.out.println(tUser);
}
}
一、与Ibatis编程模型的对比?
我们可以看到原生的Ibatis的模型 需要通过namespane+id 然后传入参数才可以让sqlSession去执行sql的 那我们日常使用的时候而是直接通过接口直接调用方法的 那么问题就出来了我们又没有写mapper的实现类,mybatis到底是怎么实现它的实现类并且调用Ibatis的模式的呢?
SqlSession 是 MyBatis 对外提供数据库访问最主要的 API,但是因为直接使用SqlSession 进行数据库开发存在代码可读性差、可维护性差的问题,所以我们很少使用,而是使用 Mapper 接口的方式进行数据库的开发。表面上我们在使用 Mapper 接口编程,实际上MyBatis 的内部,将对Mapper 接口的调用转发给了 SqlSession,这个请求的转发是建立在配置文件解读、动态代理增强的基础之上实现的,实现的过程有三个关键要素:
- 找到SqlSession 中对应的方法执行;
- 找到命名空间和方法名(两维坐标)
- 传递参数
要实现上述的步骤,必须对 bindling 模块有深入的分析;
二、对Ibatis进行怎么样封装的?以及核心类
1.哪些类是干什么的?在什么地方被加载的 ?
- MapperRegistry:mapper 接口和对应的代理对象工厂的注册中心;
- MapperProxyFactory:用于生成mapper 接口动态代理的实例对象;保证Mapper 实例对象是局部变量;
- MapperProxy:实现了 InvocationHandler 接口,它是增强 mapper 接口的实现;
- MapperMethod:封装了 Mapper 接口中对应方法的信息,以及对应的 sql 语句的信息;它是 mapper 接口与映射配置文件中 sql 语句的桥梁; MapperMethod 对象不记录任何
- 状态信息,所以它可以在多个代理对象之间共享;MapperMethod 内几个关键数据结构:
- SqlCommand : 从configuration 中获取方法的命名空间.方法名以及 SQL 语句的类型;
- MethodSignature:封装 mapper 接口方法的相关信息(入参,返回类型);
- ParamNameResolver: 解析mapper 接口方法中的入参,将多个参数转成 Map;
代码入口类:
org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
我们可以看到它是从configuration里面的mapperRegistry 获取的mapper接口的
那么问题来了,这个mapper接口到底是什么时候注册进来的?
答案是这个类其实是在加载的时候就已经配置进来的
//org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
public void parse() {
//判断是否已经加载该配置文件
if (!configuration.isResourceLoaded(resource)) {
//处理mapper节点
configurationElement(parser.evalNode("/mapper"));
/*将mapper文件添加到configuration.loadedResources中*/
configuration.addLoadedResource(resource);
/*注册mapper接口 找到Class类与 xml文件所处的位置 重点分析 */
bindMapperForNamespace();
}
//处理解析失败的ResultMap节点
parsePendingResultMaps();
//处理解析失败的CacheRef节点
parsePendingCacheRefs();
//处理解析失败的Sql语句节点
parsePendingStatements();
}
// org.apache.ibatis.builder.xml.XMLMapperBuilder#bindMapperForNamespace
//通过这个方法来进行注册mapper的
我们可以看到在config中有一个类记录了mapper接口与对应MapperProxyFactory之间的关系
/*mapper接口的动态代理注册中心*/
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
private final Configuration config;//config对象,mybatis全局唯一的
//记录了mapper接口与对应MapperProxyFactory之间的关系
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
但是这个MapperProxyFactory是什么呢?用于生成mapper 接口动态代理的实例对象;保证Mapper 实例对象是局部变量
//mapper接口的class对象
private final Class<T> mapperInterface;
//key是mapper接口中的某个方法的method对象,value是对应的MapperMethod,MapperMethod对象不记录任何状态信息,所以它可以在多个代理对象之间共享
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
....
我们可以看到 有一个MapperMethod类 还有MethodSignature 两个内部类 ,可以大概看看这个类是干什么的
//org.apache.ibatis.binding.MapperMethod.SqlCommand
//org.apache.ibatis.binding.MapperMethod.MethodSignature
//从configuration中获取方法的命名空间.方法名以及SQL语句的类型
private final SqlCommand command;
//封装mapper接口方法的相关信息(入参,返回类型);
private final MethodSignature method;
//是从哪儿实例化的?也就是实例化过程,也是在加载阶段来存到config里面对象的 ,是通过这个构造方法来实例化的
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
public static class MethodSignature {
/**
* 返回类型 接口
* List<TUser> selectAll();
*/
private final boolean returnsMany;//返回参数是否为集合类型或数组
private final boolean returnsMap;//返回参数是否为map
private final boolean returnsVoid;//返回值为空
private final boolean returnsCursor;//返回值是否为游标类型
private final boolean returnsOptional;//返回值是否为Optional
private final Class<?> returnType;//返回值类型 (也就是我们返回具体参数)
.....
}
好到现在我们 已经了解了它3大步骤的来源 那么久可以从源码层面来了解它是怎么获取的了
org.apache.ibatis.binding.MapperRegistry#getMapper 调用了
->org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
public T newInstance(SqlSession sqlSession) {
//每次调用都会创建新的MapperProxy对象
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
->
protected T newInstance(MapperProxy<T> mapperProxy) {
//创建实现了mapper接口的动态代理对象 与 通过动态代理来生成一个对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
-->>org.apache.ibatis.binding.MapperProxy#invoke
-->org.apache.ibatis.binding.MapperMethod#execute
动态代理传入了MapperProxy对象 里面的invok方法就是对代理进行增强的
在invok里面最终进行了方法的调用: 重要的代码入下 : 用来区分调用Ibatis的哪一些方法
Object result;
//对三部翻译进行执行
//根据sql语句类型以及接口返回的参数选择调用不同的方法 翻译完之后在判断去执行哪一个方法
//应该把接口给返回给谁呢?
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()) {//返回值为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 {
//处理返回为单一对象的情况
//通过参数解析器解析解析参数
//第三部翻译 入参转化成mapo
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = OptionalUtil.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
从SqlSession.getMapper(Class)方法开始跟踪,画出 binding 模块的时序图如下所示:
总结
在 binding 模块的运行流程中实现三步翻译的核心方法是 MapperMethod.execute(SqlSession, Object[]),翻译的过程描述如下:
- 通过Sql 语句的类型(MapperMethod.SqlCommand.type)和 mapper 接口方法的返回参(MapperMethod.MethodSignature.returnType)确定调用 SqlSession 中的某个方法;
- 通过MapperMethod.SqlCommand.name 生成两维坐标;
- 通过 MapperMethod.MethodSignature.paramNameResolve 将传入的多个参数转成Map 进行参数传递;