MyBatis源码学习(三)(上)

简介: 前言文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在

絮叨


经过前面复杂的解析过程后,现在, MyBatis 已经进入了就绪状态,等待使用者发号施令,sql执行还是有下面的几个点

  1. 为 mapper 接口生成实现类
  2. 根据配置信息生成 SQL,并将运行时参数设置到 SQL 中
  3. 一二级缓存的实现
  4. 插件机制
  5. 数据库连接的获取与管理
  6. 查询结果的处理,以及延迟加载等


SQL 执⾏流程


首先呢?我还是把前面最简单的流程代码来出来,我们的源码走读也是基于那个代码的

public void selectUser() throws IOException {
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("configuration.xml"));
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserDao mapper = sqlSession.getMapper(UserDao.class);
        List<User> users = mapper.select();
        System.out.println(users);
    }
复制代码

其实前面已经把sqlSessionFactory准备好了,那么接下来呢?我们是不是要通过他拿到sqlsession.getMapper这些


SQL 执⾏⼊⼜口

在单独使用 MyBatis 进行数据库操作时,我们通常都会先调用 SqlSession 接口的 getMapper方法为我们的Mapper接口生成实现类。然后就可以通过Mapper进行数据库操作。 比如像下面这样:

SqlSession sqlSession = sqlSessionFactory.openSession();
        UserDao mapper = sqlSession.getMapper(UserDao.class);
复制代码


如果大家对 MyBatis 较为了解,会知道 SqlSession 是通过 JDK 动态代理的方式为接口 生成代理对象的。在调用接口方法时,相关调用会被代理逻辑拦截。在代理逻辑中可根据方 法名及方法归属接口获取到当前方法对应的 SQL 以及其他一些信息,拿到这些信息即可进 行数据库操作


为 Mapper 接⼜创建代理对象

我们从 DefaultSqlSession 的 getMapper 方法开始看起,如下

// -☆- DefaultSqlSession
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
// -☆- Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
// -☆- MapperRegistry
复制代码


public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 从 knownMappers 中获取与 type 对应的 MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("……");
 }
try {
// 创建代理对象
return mapperProxyFactory.newInstance(sqlSession);
 } catch (Exception e) {
throw new BindingException("……");
 } }
复制代码


经过连续的调用,Mapper 接口代理对象的创建逻辑初现端倪。如果大家没分析过 MyBatis配置文件的解析过程,那么可能不知道knownMappers集合中的元素是何时存入的, 这 里简 单说 明一 下。MyBatis 在解析配置文件的节点的过程中,会调用 MapperRegistry 的 addMapper 方法将 Class 到 MapperProxyFactory 对象的映射关系存入到 knownMappers。具体的代码就不分析了,大家可以阅读我之前写的文章,或者自行分析相关 的代码。

在获取到 MapperProxyFactory 对象后,即可调用工厂方法为 Mapper 接口生成代理对象 了。相关逻辑如下


// -☆- MapperProxyFactory
public T newInstance(SqlSession sqlSession) {
// 创建 MapperProxy 对象,MapperProxy 实现了 InvocationHandler 接口,
// 代理逻辑封装在此类中
final MapperProxy<T> mapperProxy =
new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
复制代码
protected T newInstance(MapperProxy<T> mapperProxy) {
// 通过 JDK 动态代理创建代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), 
new Class[]{mapperInterface}, mapperProxy);
}
复制代码


上面的代码首先创建了一个 MapperProxy 对象,该对象实现了 InvocationHandler 接口。 然后将对象作为参数传给重载方法,并在重载方法中调用 JDK 动态代理接口为 Mapper 生成 代理对象。代理对象已经创建完毕,下面就可以调用接口方法进行数据库操作了。由于接口 方法会被代理逻辑拦截,所以下面我们把目光聚焦在代理逻辑上面,看看代理逻辑会做哪些 事情。


执⾏代理逻辑


我们不是用了动态代理,那么我们就要实现那个如下图的方法

Mapper 接口方法的代理逻辑首先会对拦截的方法进行一些检测,以决定是否执行后续 的数据库操作。对应的代码如下

public Object invoke(Object proxy, 
Method method, Object[] args) throws Throwable {
try {
// 如果方法是定义在 Object 类中的,则直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
/*
* 下面的代码最早出现在 mybatis-3.4.2 版本中,用于支持 JDK 1.8 中的
* 新特性 - 默认方法。这段代码的逻辑就不分析了,有兴趣的同学可以
* 去 Github 上看一下相关的相关的讨论(issue #709),链接如下:
* 
* https://github.com/mybatis/mybatis-3/issues/709
*/
 } else if (isDefaultMethod(method)) {
protected T newInstance(MapperProxy<T> mapperProxy) {
// 通过 JDK 动态代理创建代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), 
new Class[]{mapperInterface}, mapperProxy);
return invokeDefaultMethod(proxy, method, args);
 }
 } catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
 }
// 从缓存中获取 MapperMethod 对象,若缓存未命中,则创建 MapperMethod 对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用 execute 方法执行 SQL
return mapperMethod.execute(sqlSession, args);
}
复制代码


如上,代理逻辑会首先检测被拦截的方法是不是定义在 Object 中的,比如 equals、 hashCode 方法等。对于这类方法,直接执行即可。除此之外,MyBatis 从 3.4.2 版本开始, 对 JDK1.8 接口的默认方法提供了支持,具体就不分析了。完成相关检测后,紧接着从缓存 中获取或者创建 MapperMethod 对象,然后通过该对象中的 execute 方法执行 SQL。在分析 execute 方法之前,我们先来看一下 MapperMethod 对象的创建过程。MapperMethod 的创建 过程看似普通,但却包含了一些重要的逻辑,所以不能忽视。


创建 MapperMethod 对象

来分析一下 MapperMethod 的构造方法,看看它的构造方法中都包含了哪些逻 辑。如下

public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface,
Method method, Configuration config){
// 创建 SqlCommand 对象,该对象包含一些和 SQL 相关的信息
this.command = new SqlCommand(config, mapperInterface, method);
// 创建 MethodSignature 对象,由类名可知,该对象包含了被拦截方法的一些信息
this.method = new MethodSignature(config, mapperInterface, method);
 } 
}
复制代码


MapperMethod 构造方法的逻辑很简单,主要是创建 SqlCommand 和 MethodSignature 对 象。这两个对象分别记录了不同的信息,这2个过程 我这边就不讲了,到此,关于 MapperMethod 的初始化逻辑就分析完了

执⾏ execute ⽅法

如上,execute 方法主要由一个 switch 语句组成,用于根据 SQL 类型执行相应的数据库 操作。该方法的逻辑清晰,不需 要太多的分析。不过在上面 代 码 中 convertArgsToSqlCommandParam 方法出现次数比较频繁,这里分析一下:

// -☆- MapperMethod
public Object convertArgsToSqlCommandParam(Object[] args) {
return paramNameResolver.getNamedParams(args);
}
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
 } else if (!hasParamAnnotation && paramCount == 1) {
/*
* 如果方法参数列表无 @Param 注解,且仅有一个非特别参数,则返回该
* 参数的值。比如如下方法:
* List findList(RowBounds rb, String name)
* names 如下:
* names = {1 : "0"}
* 此种情况下,返回 args[names.firstKey()],即 args[1] -> name
*/
return args[names.firstKey()];
 } else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 添加 <参数名, 参数值> 键值对到 param 中
param.put(entry.getValue(), args[entry.getKey()]);
// genericParamName = param + index。比如 param1, param2,... paramN
final String genericParamName =
GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// 检测 names 中是否包含 genericParamName,什么情况下会包含?
// 答案如下:
// 使用者显式将参数名称配置为 param1,即 @Param("param1")
if (!names.containsValue(genericParamName)) {
// 添加 <param*, value> 到 param 中
param.put(genericParamName, args[entry.getKey()]);
 }i++;
 }
 return param;
 } }
复制代码


convertArgsToSqlCommandParam 是一个空壳方法,该方法最终调用了 ParamNameResolver 的 getNamedParams 方法。getNamedParams 方法的主要逻辑是根据条件 返回不同的结果,该方法的代码不是很难理解

相关文章
|
2月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——实体层(User.java)
mybatis简单案例源码详细【注释全面】——实体层(User.java)
14 0
|
10天前
|
SQL Java 数据库连接
一文细说Mybatis八大核心源码
以上 是V哥给大家整理的8大核心组件的全部内容,为什么说选择 Java 就是选择未来,真正爱 Java 的人,一定喜欢深入研究,学习源码只是第一步,要有一杆子捅到操作系统才够刺激。
|
13天前
|
SQL Java 数据库连接
【Mybatis】深入学习MyBatis:概述、主要特性以及配置与映射
【Mybatis】深入学习MyBatis:概述、主要特性以及配置与映射
【Mybatis】深入学习MyBatis:概述、主要特性以及配置与映射
|
14天前
|
SQL 缓存 Java
|
27天前
|
XML Java 数据库连接
探秘MyBatis:手写Mapper代理的源码解析与实现
探秘MyBatis:手写Mapper代理的源码解析与实现
21 1
|
27天前
|
SQL Java 数据库连接
深入源码:解密MyBatis数据源设计的精妙机制
深入源码:解密MyBatis数据源设计的精妙机制
33 1
深入源码:解密MyBatis数据源设计的精妙机制
|
2月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Utils层(MybatisUtils.java)
mybatis简单案例源码详细【注释全面】——Utils层(MybatisUtils.java)
14 0
|
2月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——测试层(UserMapperTest.java)
mybatis简单案例源码详细【注释全面】——测试层(UserMapperTest.java)
10 0
|
2月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Dao层映射文件(UserMapper.xml)【重要】
mybatis简单案例源码详细【注释全面】——Dao层映射文件(UserMapper.xml)【重要】
11 0
|
2月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Dao层接口(UserMapper.java)
mybatis简单案例源码详细【注释全面】——Dao层接口(UserMapper.java)
10 0