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. 分析即服务(AaaS)是什么?终于有人讲清楚了

    随着世界数据领域的地位和规模的不断扩大,大数据.人工智能和云计算正在结合起来,以分析即服务的形式为企业提供急需的喘息机会 . 让我们简要回顾一下 2010 年以来发生的事情. 这十年来我们见证了许多技 ...

  2. 【Kotlin】List、Set、Map简介

    1 List ​ Java 的 List.Set.Map 介绍见 → Java容器及其常用方法汇总. 1.1 创建 List 1.1.1 emptyList var list = emptyList& ...

  3. HarmonyOS音视频开发概述

      在音视频开发指导中,将介绍各种涉及音频.视频播放或录制功能场景的开发方式,指导开发者如何使用系统提供的音视频API实现对应功能.比如使用TonePlayer实现简单的提示音,当设备接收到新消息时, ...

  4. ArcMap分别求取矢量要素各区域的面积

      本文介绍基于ArcMap软件,自动批量计算矢量图层中各个要素的面积的方法.   一次,遇到一个问题,需要分别计算ArcMap软件中一个图层的所有面要素的面积.如图,这个图层中包括多个省级行政区矢量 ...

  5. sql 语句系列(每个季度的开始日期和结束日期)[八百章之第二十二章]

    前言 基本上统计财务一定会用到. mysql select QUARTER(ADDDATE(y.dy,-1)) QTR, DATE_ADD(y.dy,INTERVAL -3 MONTH) Q_star ...

  6. PTA前三次题目集总结

    以下内容是我对PTA三次习题作业最后一题的思路,源码以及总结 学到的java知识大多都来自写题目集 这些题目对我对java的认知与学习起到了不小的帮助 答题判题程序-1 题目内容 设计实现答题程序,模 ...

  7. 实时数仓入门训练营:基于 Apache Flink + Hologres 的实时推荐系统架构解析

    ​ 简介: <实时数仓入门训练营>由阿里云研究员王峰.阿里云资深技术专家金晓军.阿里云高级产品专家刘一鸣等实时计算 Flink 版和 Hologres 的多名技术/产品一线专家齐上阵,合力 ...

  8. 为什么DevOps的必然趋势是BizDevOps

    简介: 从精益思想出发,我们可以看到DevOps的必然发展方向,那就是向业务侧延伸.业务是产品开发和运维的源头,完整的价值流必须从源头开始.这不是预测,而是正在发生的事. 编者按:本文源自阿里云云效团 ...

  9. 03 Xpath lxml库的安装和使用

    Python lxml库的安装和使用 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 Xpath 表达式提供了良好的支持,因此能够了高效地解析 HTML/XML 文 ...

  10. Git/SourceTree版本管理

    目录 视频课程: 工作区: 文件状态: 回退版本: 合并分支 合并提交 冲突 删除分支 忽略文件 汉英对照表 多端同步 添加远程仓库 推送代码到远程仓库 拉取代码 视频课程: https://www. ...