MyBatis提供了一种动态代理实现SQL调用的功能,使用者只需要在映射文件中配置SQL语句与映射规则即可完成SQL调用和结果集封装。下面代码展示了动态代理调用的基本步骤:

public void testMyBatisBuild() throws IOException {
InputStream input = Resources.getResourceAsStream("SqlSessionConfig.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
SqlSession sqlSession = sessionFactory.openSession();
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
Student student = mapper.getStudentByIdToResultType("00000b373502481baa1a5f5229507cf8");
System.out.println(student);
}

而我们通过getMapper方法只是传入了一个接口,在整个项目中我们没有一个TestMapper的实现类,那MyBatis是如何帮我们生成实现类的,这一点就需要我们去分析源码了。

一. DefaultSqlSessionFactory

SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);

build方法用于解析配置文件并生成SqlSessionFactory,我们通过跟踪build方法的源码,我们可以发现,build方法实际上返回的是DefaultSqlSessionFactory的实例。DefaultSqlSessionFactory就是SqlSessionFactory接口的唯一实现类。

public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//创建XMLConfigBuilder,用于解析配置文件
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

`

二. DefaultSqlSession

2.1 openSession()

SqlSession sqlSession = sessionFactory.openSession();

上面代码是通过SqlSessionFactory获取SqlSession的代码,我们进入DefaultSqlSessionFactory::openSession方法一探究竟,看看它底层到底做了什么。

可以看到DefaultSqlSessionFactory::openSession方法最终生成了一个DefaultSqlSession实例,它就是Mybatis核心接口SqlSession的实现类。

public SqlSession openSession() {
//从数据源中获取连接,然后创建SqlSessionFactory
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//获取mybatis-config.xml中的enviroment对象
final Environment environment = configuration.getEnvironment();
//从Enviroment获取TranslationFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//从数据源中获取数据库连接,然后创建Transaction对象
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//重点:根据配置创建Executor,该方法内部会根据用户是否配置二级缓存去决定是否创建二级缓存的装饰器去装饰Executor,这也是二级缓存是否生效的关键
final Executor executor = configuration.newExecutor(tx, execType);
//创建DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

2.2 getMapper()获取动态代理实例

TestMapper mapper = sqlSession.getMapper(TestMapper.class);

走到这里我们就来到今天主题的核心,我们现在就来抽丝剥茧般的看看MyBatis是如何帮我们生成动态代理实现类的。

首先我们进入DefaultSqlSession::getMapper方法,可以看到实际上它是调用的Configuration::getMapper方法获取的代理实例:

@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}

Configuration是MyBatis初始化后全局唯一的配置对象,它内部保存着配置文件解析过程中所有的配置信息。进入Configuration::getMapper我们可以发现它实际上调用的是MapperRegistry::getMapper方法:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//调用mapper注册中心的getMapper方法获取Mapper动态代理实现类
return mapperRegistry.getMapper(type, sqlSession);
}

MapperRegistry是Mapper接口动态代理工厂类的注册中心,我们继续进入MapperRegistry::getMapper方法,可以看到它实际上调用的是MapperProxyFactory::newInstance方法。

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//使用Mapper代理工厂创建动态代理实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

MapperProxyFactory是生成动态代理对象的工厂类,走到这里,我有一种预感,我们离真相越来越近了。我们进入MapperProxyFactory::newInstance一探究竟:

public T newInstance(SqlSession sqlSession) {
//MapperProxy实现了InvocationHandler接口。它是Mapper动态代理的核心类
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
//创建动态代理实例
return newInstance(mapperProxy);
}

我们先暂时略过MapperProxy内部的处理流程,我们先看看newInstance方法内部是如何创建动态代理实例的。

protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

看到上面的代码是不是有一种恍然大悟的感觉,它是JDK动态代理的核心API,也就是说Mybatis底层是调用JDK的Proxy类来创建代理实例。对JDK动态代理不熟悉的小伙伴可以看看博主的另一篇文章:JDK动态代理的深入理解

三. 动态实例是如何执行的

代理实例如何创建的过程我们已经清楚了,现在我们需要了解代理类内部是如何实现SQL语句的执行的。我们进入MapperProxy::invoke方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
//如果是Object方法,则调用方法本身
return method.invoke(this, args);
} else {
//根据被调用接口方法的Method对象,从缓存中获取MapperMethodInvoker对象,如果没有则创建一个并放入缓存,然后调用invoke。
//换句话说,Mapper接口中的每一个方法都对应一个MapperMethodInvoker对象,而MapperMethodInvoker对象里面的MapperMethod保存着对应的SQL信息和返回类型以完成SQL调用
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}

看了看invoke方法内部,似乎并没有看出真实的调用逻辑,那我们就先进入cacheInvoker方法中看看吧:

/**
* 获取缓存中MapperMethodInvoker,如果没有则创建一个,而MapperMethodInvoker内部封装这一个MethodHandler
* @param method
* @return
* @throws Throwable
*/
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
//如果调用接口的是默认方法(JDK8新增接口默认方法的概念)
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
//如果调用的普通方法(非default方法),则创建一个PlainMethodInvoker并放入缓存,其中MapperMethod保存对应接口方法的SQL以及入参和出参的数据类型等信息
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}

可以看到进入cacheInvoker方法后首先会判断用户当前调用的是否是接口的default方法,如果不是就会创建一个PlainMethodInvoker对象并返回。

PlainMethodInvoker:类是Mapper接口普通方法的调用类,它实现了MethodInvoker接口。其内部封装了MapperMethod实例。

MapperMethod:封装了Mapper接口中对应方法的信息,以及对应的SQL语句的信息;它是mapper接口与映射配置文件中SQL语句的桥梁。

此时我们跳出cachedInvoker方法回到MapperProxy::invoke方法中。

 return cachedInvoker(method).invoke(proxy, method, args, sqlSession);

我们可以看到当cacheInvoker返回了PalinMethodInvoker实例之后,紧接着调用了这个实例的PlainMethodInvoker::invoke方法。进入PlainMethodInvoker::invoke方法我们发现它底层调用的是MapperMethod::execute方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
//Mybatis如何帮助用户实现动态代理的玄机就在里面
return mapperMethod.execute(sqlSession, args);
}

进入MapperMethod::invoke方法我们会发现眼前一亮,这就是MyBatis底层动态代理的逻辑,可以看到动态代理最后还是使用SqlSession操作数据库的:

  */
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
// 将args进行解析,如果是多个参数则,则根据@Param注解指定名称将参数转换为Map,如果是封装实体则不转换
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()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
//解析参数,因为SqlSession::selectOne方法参数只能传入一个,但是我们Mapper中可能传入多个参数,
//有可能是通过@Param注解指定参数名,所以这里需要将Mapper接口方法中的多个参数转化为一个ParamMap,
//也就是说如果是传入的单个封装实体,那么直接返回出来;如果传入的是多个参数,实际上都转换成了Map
Object param = method.convertArgsToSqlCommandParam(args);
//可以看到动态代理最后还是使用SqlSession操作数据库的
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()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}

四. 动态代理调用过程时序图

由于图片版幅较大,网页显示字体极小,这里博主给出下载链接供大家下载:MyBatis核心调用流程时序图-GitHubMyBatis核心调用流程时序图-Gitee

五. 总结

Mapper动态代理是通过MyBatis生成接口的实现类,然后调用SqlSession中的方法执行数据库操作。上文只是对MyBatis这个过程源码的简单分析,希望读者在读后能够明白Mapper动态代理的几个核心问题:

  • MyBatis如何生成动态代理实例的?
  • MyBatis如何根据不同的情况将Mapper接口“翻译”成SqlSession调用的
    • 如何确定调用SqlSession中的那个方法?
    • 如何确定命名空间和调用的statementId
    • 如何传递参数给SqlSession
  • 从源码的角度看Mapper代理实例和SqlSession是一对一的关系,而SqlSession是线程不安全的,那么在Spring和MyBatis集成的时候,项目的整个生命周期中Mapper接口是单例的(通常情况),那么MyBatis是如何解决SqlSession线程安全问题的?(这个问题在这个部分的源码分析中暂时得不到解决)

最后,博主自己对MyBatis源码进行了详细注释,如有需要,请移步至:GitHubGitee

本文阐述了自己对MyBatis源码的一些理解,如有不足,欢迎大佬指点,感谢感谢!!

从源码的角度弄懂MyBatis动态代理开发原理的更多相关文章

  1. MyBatis 动态代理开发

    MyBatis 动态代理开发 §  Mapper.xml文件中的namespace与mapper接口的类路径相同. §  Mapper接口方法名和Mapper.xml中定义的每个statement的i ...

  2. 解析源码,彻底弄懂HashMap(持续更新中)

    为啥突然想着看HashMap源码了? 无意间看到有人说HashMap能考验Java程序员的基本功,之前我作为面试官帮公司招人的时候偶尔问起HashMap,大部分人回答基本都会用,且多数仅停留在put, ...

  3. mybatis源码分析之04Mapper接口的动态代理

    在工作中,使用mybatis操作数据库,只需要提供一个接口类,定义一些方法,然后调用接口里面的方法就可以CRUD,感觉是牛了一逼! 该篇就是记录一下,mybatis是如何完成这波骚操作的,即分析我们测 ...

  4. MyBatis源码分析(七):动态代理(Mybatis核心机制)

    一.动态代理 动态代理是一种比较高级的代理模式,它的典型应用就是Spring AOP. 在传统的动态代理模式中,客户端通过ProxySubject调用RealSubject类的request( )方法 ...

  5. MyBatis动态代理执行原理

    前言 大家使用MyBatis都知道,不管是单独使用还是和Spring集成,我们都是使用接口定义的方式声明数据库的增删改查方法.那么我们只声明一个接口,MyBatis是如何帮我们来实现SQL呢,对吗,我 ...

  6. 宇宙无敌搞笑轻松弄懂java动态代理

    https://www.cnblogs.com/ferryman/p/13170057.html jdk动态代理和cglib动态代理区别 https://blog.csdn.net/shallynev ...

  7. 从源码的角度解析Mybatis的会话机制

    坐在我旁边的钟同学听说我精通Mybatis源码(我就想不通,是谁透漏了风声),就顺带问了我一个问题:在同一个方法中,Mybatis多次请求数据库,是否要创建多个SqlSession会话? 可能最近撸多 ...

  8. Android AsyncTask完全解析,带你从源码的角度彻底理解

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/11711405 我们都知道,Android UI是线程不安全的,如果想要在子线程里进 ...

  9. [转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

    Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分 ...

  10. 从源码的角度分析ViewGruop的事件分发

    从源码的角度分析ViewGruop的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View ...

随机推荐

  1. openGauss2.1.0新特性-账本数据库实验

    openGauss2.1.0 新特性-账本数据库实验 账本数据库融合了区块链思想,将用户操作记录至两种历史表中:用户历史表和全局区块表.当用户创建防篡改用户表时,系统将自动为该表添加一个 hash 列 ...

  2. 如何在HarmonyOS对数据库进行备份,恢复与加密

    数据库备份与恢复 场景介绍 当应用在处理一项重要的操作,显然是不能被打断的.例如:写入多个表关联的事务.此时,每个表的写入都是单独的,但是表与表之间的事务关联性不能被分割. 如果操作的过程中出现问题, ...

  3. Python根据主播直播时间段判定订单销售额归属

    写在前面:最近在群里看到一个这样的直播电商的场景觉得还是挺有趣的,于是就想用Python来实现. 需求描述:根据主播直播时间段结合销售订单的付款时间判断所属销售的归属 生成主播在线直播时间段数据 fr ...

  4. WPF基础:在Canvas上绘制图形

    Canvas介绍 Canvas是WPF(Windows Presentation Foundation)中的一种面板控件,用于在XAML中布置子元素.它提供了绝对定位的能力,允许元素在自由的二维空间中 ...

  5. Borůvka MST算法

    当我认为最MST(最小生成树)已经没有什么学的了,才发现世界上还有个这个kruskal和prim结合的玩意 Borůvka 运用并查集的思想,先将每一个初始点集初始化为有且只有自己的点集,然后每一次合 ...

  6. 【知识点】如何快速开发、部署 Serverless 应用?

    简介: 本文将详细介绍如何开发和部署 Serverless 应用,并通过阿里云函数计算控制台与开发者工具 Serverless Devs 进行应用的初始化.部署:最后分享应用的调试,通过科学发布.可观 ...

  7. 日处理数据量超10亿:友信金服基于Flink构建实时用户画像系统的实践

    导读:当今生活节奏日益加快,企业面对不断增加的海量信息,其信息筛选和处理效率低下的困扰与日俱增.由于用户营销不够细化,企业 App 中许多不合时宜或不合偏好的消息推送很大程度上影响了用户体验,甚至引发 ...

  8. 开箱即用!Linux 内核首个原生支持,让你的容器体验飞起来!| 龙蜥技术

    简介: 本文将从 Nydus 架构回顾.RAFS v6 镜像格式和 EROFS over Fscache 按需加载技术三个角度来分别介绍这一技术的演变历程. 文/阿里云内核存储团队,龙蜥社区高性能存储 ...

  9. 2021云栖大会开源引力峰会重磅发布的战略合作,Grafana服务到底是什么?

    简介: 这几天关注云栖大会的小伙伴一定会发现阿里巴巴合伙人.阿里云高级研究员蒋江伟(小邪)在云栖大会开源引力峰会的演讲中,特别提到了一个叫 Grafana 服务的产品,并特意花费一页 PPT 介绍了这 ...

  10. 在 VisualStudio 给文件起一个带分号的文件名会怎样

    小伙伴都知道在 Windows 下是支持文件名使用分号的,而写过 Roslyn 的小伙伴都知道,在 csproj 项目里面使用分号分割数组.那么在 VS 里面将一个文件名添加分号会如何?下面让咱写写看 ...