参考地址一
参考地址二
参考地址三

Mybatis官网

一、工作流程分析

全局配置文件 mybatis-Config.xml —> Mapper.xml —> Configuration填充配置类 —> SqlSessionFactory(生成session工厂) —> SqlSession(生成session) —> Executor(执行器) —> StatementHandler —> 数据库
[

](https://blog.csdn.net/qq_40111437/article/details/105274919)

1、Demo示例

  1. /**
  2. * 通过 SqlSession.getMapper(XXXMapper.class) 接口方式
  3. * @throws IOException
  4. */
  5. @Test
  6. public void testSelect() throws IOException {
  7. String resource = "mybatis-config.xml";
  8. InputStream inputStream = Resources.getResourceAsStream(resource);
  9. SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  10. SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
  11. try {
  12. BlogMapper mapper = session.getMapper(BlogMapper.class);
  13. Blog blog = mapper.selectBlogById(1);
  14. System.out.println(blog);
  15. } finally {
  16. session.close();
  17. }
  18. }

依赖

  1. <!--mysqlJDBC实现-->
  2. <dependency>
  3. <groupId>mysql</groupId>
  4. <artifactId>mysql-connector-java</artifactId>
  5. <version>5.1.38</version>
  6. </dependency>
  7. <!--mybatis依赖-->
  8. <dependency>
  9. <groupId>org.mybatis</groupId>
  10. <artifactId>mybatis</artifactId>
  11. <version>3.4.1</version>
  12. </dependency>

2、解析配置文件并且生成SqlSessionFactory

首先进入构造这个SqlSessionFactory工场的build方法,我们发现它是生成了一个XMLConfigBuilder对象
image.png

而XMLConfigBuilder对象里面new了一个全局配置对象,也就是我们Mybatis配置文件的Configuration。
image.png

我们再进入这个parser的parse()方法来看看,从if判断我们可以看出这个Mybatis的xml配置文件只能加载一次,否则会丢出异常,然后通过这个XMLConfigBuilder的解析Node方法,把配置文件里面的所有属性以Xnode的形式返回,通过读取根标签configuration开始解析。
image.png
看看parseConfiguration方法,我们发现它是把每个配置文件里的属性解析放在了Configuration的属性中,那下面这些其实也就是把我们的配置文件解析成属性,赋值到Configuration中。
image.png
image.png

我们发现它设置属性的同时,还有几个load操作,这几个操作是给我们的mybatis开启一些其它功能,比如上面的加载客户端的日志。
image.png

好,解析完这些属性的时候,中间突然插了一个settingElement方法,我们来看看这个方法做了什么。
image.png
这个方法里面就是设置mybatis给我们开启的功能了,如果你配置文件里面配置了,它就会读取替换,没有开启的就会用它默认的参数,比如延迟加载默认为false,比如缓存开启默认为true等,如果不知道某些值得默认属性得话,我们就可以来这里看看。

打开mybatis中文网也能看到这些配置,看看是不是与上面对应。
image.png

再看settingElement方法之后得方法,主要看看mapperElement方法,显然这是解析mapper得方法。

image.png
我们常说mybatis查找mapper有几种方式,看到这段代码应该就能记住了,首先代码判断了是否是package标签,这也是我们常用得直接扫描整个包下面得mapper,然后如果不是,就解析下面三种标签,resource(直接指定mapper.xml的相对文件路径),url(直接指定mapper.xml的绝对文件路径),class(直接指定类路径,需要mapper类)。 看看mapper文件的解析方式。
image.png

mybatis解析完我们整个sql的标签以后,最终它会产生一个Mappedstatement对象,并把解析出来的,看看里面的属性是不是对应map,sqlsource放的是sql语句,statementType放的是curd标签等,果然,它把我们整个mapper里面的sql变成了一个一个的对象。

再回到mapperParse.parse方法,解析完mapper文件以后,还调用了一个bindMapperForNamespace()方法。

boundType获得那个mapper.xml对应的接口的全路径,然后调用了一个addMapper()方法,把这个mapper接口的全路径传进去。
[

](https://blog.csdn.net/qq_40111437/article/details/105274919)
image.png

最后,发现它是存了一组这样的键值对,这个mapper的class和这个mapper的代理工厂。
一直走到这里,就发现mybatis已经把我们的配置文件以及mapper.xml都解析完了,并且放在了配置类Configuration里面,最后返回一个DefaultsqlsessionFactory工厂了。
image.png

以上总结的 时序图 如下所示:

image.png

3、SqlSessionFactory生成session会话

通过上一步,我们发现解析完配置文件以后它会返回一个DefaultSqlSessionFactory对象,我们现在看看这个对象是怎么open一个SqlSession的,先跟踪方法。
image.png

通过这个方法,我们看到,它是从我们之前解析的配置文件里面,拿到了environment对象,同时创建了一个事务,然后又从配置中拿到属性,构造了一个执行器。
所以,mybatis里面的事务和执行器都是在openSession里面创建的,我们重点来看看执行器的构造。
image.png

在这里我们可以看到,如果我们没有定义执行器的话,它会默认使用一个simple的执行器,同时为了防止有人手贱,设置一个执行器并且赋值null,所以加了一层null判断。

SimpleExecutor:封装了jdbc的statement,使用完后直接关闭。
ReuseExecutor:封装了jdbc的statement,使用完后不直接关闭,放在一个缓存的map里面等待下次使用。
BatchExecutor:封装了jdbc的statement,有缓存功能,同时支持批量操作。
[

](https://blog.csdn.net/qq_40111437/article/details/105274919)
还记得我们之前说到了mybatis的一级缓存和二级缓存,说过了一级缓存是放在baseExecutor中。
MyBatis系列之Mybatis缓存深入了解

可以看看这个执行器的类图,发现它们都是BaseExcutor的子实现类,里面有几个抽象的增删改查方法,如doUpdate(),doQuery(),留给子类去实现,典型的模板模式。
image.png

再回到这个方法,添加完执行器以后,它用包装类把这个Executor包装了一下,这里是为了二级缓存的实现,之后还调用了一个插件拦截器链的pluginAll方法,里面是把每个插件都关联一下这个执行器,最后返回这个执行器。
image.png
最后又回到一开始那个方法,返回了一个带有Configruation,executor的defaltSqlSession 。

image.png

4、Session获取Mapper

上一步我们已经拿到了一个DefaltSqlSession,现在我们再来看看它是怎么得到一个可以调用curd的mapper的,看看它具体做了哪些操作。
image.png

  1. 看到这里是不是恍然大悟,我们第一次获得一个defaltSqlSessionFactory的时候,最后一步是不是把接口类和对应的代理工厂绑定在了一起,而这里就是通过这个接口类来获取这个对应的工厂,并且把sqlSession作为参数来new一个实例,最后,它又把这个实例传给了另一个newInstance方法,没错,非常明显的jdk动态代理,接口的类加载器,接口和被代理的对象。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22731784/1644977566438-df6ec837-d8b8-45d3-a101-bb2e6addadbd.png#clientId=u1248b6bc-8d03-4&from=paste&height=397&id=uc6a1b2ec&margin=%5Bobject%20Object%5D&name=image.png&originHeight=478&originWidth=966&originalType=binary&ratio=1&size=432067&status=done&style=none&taskId=ue9df0027-230a-4a84-841c-ab89ef276ee&width=803)

5、代理对象MapperProxy执行具体的sql操作

对最后一行代码进行调试发现,怎么直接去了接口方法,😂😂,当然了!代理对象调用的方法不都在invoke里面吗,此时当然是去看mapperProxy的invoke方法了。
image.png
看这个方法,第一步,它首先会去看看它调用的是不是object本身的方法, 如果是就直接调用这个方法;第二步,判断你是不是接口中的defalt方法(jdk1.8中新加了很多接口中得defalut方法),如果是就走下面这个方法,有兴趣的自己可以去看看,否则,它会先拿到一个mapperMethod对象,然后用这个对象调用这个方法,所以重点又来了,这是个啥对象。
image.png

来了来了,就在这里了,判断标签并且调用方法。

image.png
mappedStatement对象,我们第一步封装sql使用的对象,终于在这里拿到了,通过方法的名字拿到了这个方法,也就是这段sql对应的sql对象,然后用执行器执行query方法,因为开启了缓存,我们进入缓存的执行器看看。
image.png

原来,缓存保存一个sql,保存了这个sql的id,分页参数,sql语句以及传参值,这样缓存就能够确保找到这个对应的sql了,好,继续往下走看到这个query方法。
image.png

从这一步可以看出,它是先从我们的二级缓存中取这个值,通过我们之前保存的参数,如果取不到,就走下面的方法。
image.png
如果没有二级缓存,它就会走一级缓存中的方法,也就是baseExecutor中的query方法,其中走数据库的方法就是上面那个queryFromDatabase了,终于要揭开他的真面目了。
image.png

我们看看默认的simpleExecutor执行器是怎么执行的。

image.png

发现它创建preparedstatement的时候,先把这些参数赋值,比如配置文件,执行器,sql对象以及分页参数,同时最后还构造了两个handler,一个是参数处理器,一个是结果处理器。
image.png
到目前为止,我们已经发现了mybatis里面的四个handler了,分别是executor,执行器的拦截,statement,jdbc的拦截,parameter,参数的拦截以及resultset,返回结果集的拦截。
image.png

看到了,下面一个query方法,就是prepareStatement的execute方法了,操作数据库,并且把返回的结果集传给resultSetHandler来处理。
image.png
然后就是返回结果集,转换成java对象的操作了。

好,以上的流程就是mybatis怎么通过配置文件和mapper文件执行sql的整体过程了,有兴趣的小伙伴可以继续去看看最后是怎么操作把结果集转换成java对象的,无非就是反射,然后从resultType里面取到类型,然后赋值等一些列操作了。
[

](https://blog.csdn.net/qq_40111437/article/details/105274919)

二、JDK动态代理

接口无法实例化 所以采用jdk动态代理 实例化接口

Proxy.newProxyInstance 方法生成动态代理类 invoke() 可以调用类的方法
程序执行时使用java.lang.reflect包中Proxy类与InvocationHandler接口动态地生成一个实现代理接口的匿名代理类及其对象,无论调用代理对象哪个方法,最终都会执行invoke方法。
image.png
image.png

JDBC ,mybatis, Hibernate, SpringJDBC区别

三、要点

1、容器Configuration

Configuration 像是Mybatis的总管,Mybatis的所有配置信息都存放在这里,此外,它还提供了设置这些配置信息的方法。Configuration可以从配置文件里获取属性值,也可以通过程序直接设置。
用一句话概述Configuration,他类似Spring中的容器概念,而且是中央容器级别,存储的Mybatis运行所需要的大部分东西。

2、动态SQL模板

使用mybatis,我们大部分时间都在干嘛?在XML写SQL模板,或者在接口里写SQL模板

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4. <!-- mapper:根标签,namespace:命名空间,命名空间唯一 -->
  5. <mapper namespace="UserMapper">
  6. <select id="selectUser" resultType="com.wqd.model.User">
  7. select * from user where id= #{id}
  8. </select>
  9. </mapper>

或者注解

  1. @Mapper
  2. public interface UserMapper {
  3. @Insert("insert into user( name, age) " + "values(#{user.name}, #{user.age})")
  4. void save(@Param("user") User user);
  5. @Select("select * from user where id=#{id}")
  6. User getById(@Param("id")String id);
  7. }

2.1、MappedStatement (映射器)

  • 就像使用Spring,我们写的Controller类对于Spring 框架来说是在定义BeanDefinition一样。
  • 当我们在XML配置,在接口里配置SQL模板,都是在定义Mybatis的域值MappedStatement

一个SQL模板对应MappedStatement
mybatis 在启动时,就是把你定义的SQL模板,解析为统一的MappedStatement对象,放入到容器Configuration中。每个MappedStatement对象有一个ID属性。这个id同我们平时mysql库里的id差不多意思,都是唯一定位一条SQL模板,这个id 的命名规则:命名空间+方法名

Spring的BeanDefinition,Mybatis的MappedStatement


2.2、两种解析方式

同Spring一样,我们可以在xml定义Bean,也可以java类里配置。涉及到两种加载方式。

xml方式的解析

提供了XMLConfigBuilder组件,解析XML文件,这个过程既是Configuration容器创建的过程,也是MappedStatement解析过程。

  1. XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
  2. Configuration config = parser.parse()

与Spring使用时

会注册一个MapperFactoryBean,在MapperFactoryBean在实例化,执行到afterPropertiesSet()时,触发MappedStatement的解析
image.png

最终会调用Mybatis提供的一MapperAnnotationBuilder 组件,从其名字也可以看出,这个是处理注解形式的MappedStatement

3、SqlSession

3.1、基本介绍

有了SQL模板,传入参数,从数据库获取数据,这就是SqlSession干的工作。
SqlSession代表了我们通过Mybatis与数据库进行的一次会话。使用Mybatis,我们就是使用SqlSession与数据库交互的。
我们把SQL模板的id,即MappedStatement 的id 与 参数告诉SqlSession,SqlSession会根据模板id找到对应MappedStatement ,然后与数据交互,返回交互结果

  1. User user = sqlSession.selectOne("com.wqd.dao.UserMapper.selectUser", 1);

3.2、分类

  • DefaultSqlSession:最基础的sqlsession实现,所有的执行最终都会落在这个DefaultSqlSession上,线程不安全
  • SqlSessionManager : 线程安全的Sqlsession,通过ThreadLocal实现线程安全。


    3.3、Executor

    Sqlsession有点像门面模式,SqlSession是一个门面接口,其内部工作是委托Executor 执行器完成的。
  1. public class DefaultSqlSession implements SqlSession {
  2. private Configuration configuration;
  3. private Executor executor;//就是他
  4. }

我们调用SqlSession的方法,都是由Executor完成的。

  1. public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  2. try {
  3. MappedStatement ms = configuration.getMappedStatement(statement);
  4. ----交给Executor
  5. executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  6. } catch (Exception e) {
  7. throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
  8. } finally {
  9. ErrorContext.instance().reset();
  10. }
  11. }

4、Mapper

4.1、存在的意义

  1. UserMapper userMapper = sqlsession.getMapper(UserMapper.class);
  2. User user = userMapper.getById("51");

Mapper的意义在于,让使用者可以像调用方法一样执行SQL。
区别于,需要显示传入SQL模板的id,执行SQL的方式。

  1. User user = sqlSession.selectOne("com.wqd.dao.UserMapper.getById", 1);

4.2、工作原理

Mapper通过代理机制,实现了这个过程。

1、MapperProxyFactory: 为我们的Mapper接口创建代理。

  • 单独使用Mybatis时,Mybatis会调用MapperRegistry.addMapper()方法,为UserDao接口,创建new MapperProxyFactory(type)
  • 当和Spring一起使用时,MapperScannerRegistrar组件触发ClassPathMapperScanner组件的doScan方法将UserDao的BeanDefinition 的BeanClass设置为MapperProxyFactory, 在走SpringBean实例化时,就从MapperProxyFactory里获取UserDao的实例对象(即代理对象)。 ```java public T newInstance(SqlSession sqlSession) {
    final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy); }

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

  1. MapperProxyFactory通过JDK动态代理技术,在内存中帮我们创建一个代理类出来。(虽然你看不到,但他确实存在)
  2. 2MapperProxy:就是上面创建代理时的增强
  3. ```java
  4. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  5. try {
  6. if (Object.class.equals(method.getDeclaringClass())) {
  7. return method.invoke(this, args);
  8. } else if (isDefaultMethod(method)) {
  9. return invokeDefaultMethod(proxy, method, args);
  10. }
  11. } catch (Throwable t) {
  12. throw ExceptionUtil.unwrapThrowable(t);
  13. }
  14. --------------------------
  15. final MapperMethod mapperMethod = cachedMapperMethod(method);
  16. return mapperMethod.execute(sqlSession, args);
  17. }

3、MapperMethod
一个业务方法在执行时,会被封装成MapperMethod, MapperMethod 执行时,又会去调用了Sqlsession

  1. public Object execute(SqlSession sqlSession, Object[] args) {
  2. Object result;
  3. switch (command.getType()) {
  4. case SELECT:
  5. ...
  6. result = sqlSession.selectOne(command.getName(), param);
  7. ...
  8. break;
  9. ....
  10. }

绕了一周,终究回到了最基本的调用方式上。

  1. result = sqlSession.selectOne(command.getName(), param);
  2. User user = sqlSession.selectOne("com.wqd.dao.UserMapper.getById", 1);

总结下:

  • 最基本用法=sqlsession.selectOne(statement.id,参数)
  • Mapper=User代理类getById—-》MapperProxy.invoke方法—-》MapperMethod.execute()—-》sqlsession.selectOne(statement.id,参数)

5、缓存

5.1、一级缓存

原来就是HashMap的封装

  1. public class PerpetualCache implements Cache {
  2. private String id;
  3. private Map<Object, Object> cache = new HashMap<Object, Object>();
  4. public PerpetualCache(String id) {
  5. this.id = id;
  6. }
  7. }

在什么位置?作为BaseExecutor的一个属性存在。

  1. public abstract class BaseExecutor implements Executor {
  2. protected BaseExecutor(Configuration configuration, Transaction transaction) {
  3. this.localCache = new PerpetualCache("LocalCache");
  4. }
  5. }

Executor上面说过,Sqlsession的能力其实是委托Executor完成的.Executor作为Sqlsession的一个属性存在。
所以:MyBatis一级缓存的生命周期和SqlSession一致

5.2、二级缓存

基本信息

二级缓存在设计上相对与一级缓存就比较复杂了。
以xml配置为例,二级缓存需要配置开启,并配置到需要用到的namespace中。

  1. <setting name="cacheEnabled" value="true"/>
  1. <mapper namespace="mapper.StudentMapper">
  2. <cache/>
  3. </mapper>

同一个namespace下的所有MappedStatement共用同一个二级缓存。二级缓存的生命周期跟随整个应用的生命周期,同时二级缓存也实现了同namespace下SqlSession数据的共享。
二级缓存配置开启后,其数据结构默认也是PerpetualCache。这个和一级缓存的一样。
但是在构建二级缓存时,mybatis使用了一个典型的设计模式装饰模式,对PerpetualCache进行了一层层的增强,使得二级缓存成为一个被层层装饰过的PerpetualCache,每装饰一层,就有不同的能力,这样一来,二级缓存就比一级缓存丰富多了。
装饰类有:

  • LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志
  • LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value
  • ScheduledCache: 使其具有定时清除能力
  • BlockingCache: 使其具有阻塞能力
    1. 层层装饰
    2. private Cache setStandardDecorators(Cache cache) {
    3. try {
    4. MetaObject metaCache = SystemMetaObject.forObject(cache);
    5. if (size != null && metaCache.hasSetter("size")) {
    6. metaCache.setValue("size", size);
    7. }
    8. if (clearInterval != null) {
    9. cache = new ScheduledCache(cache);
    10. ((ScheduledCache) cache).setClearInterval(clearInterval);
    11. }
    12. if (readWrite) {
    13. cache = new SerializedCache(cache);
    14. }
    15. cache = new LoggingCache(cache);
    16. cache = new SynchronizedCache(cache);
    17. if (blocking) {
    18. cache = new BlockingCache(cache);
    19. }
    20. return cache;
    21. } catch (Exception e) {
    22. throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
    23. }
    24. }

如何工作

二级缓存的工作原理,还是用到装饰模式,不过这次装饰的Executor。使用CachingExecutor去装饰执行SQL的Executor

  1. public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  2. executorType = executorType == null ? defaultExecutorType : executorType;
  3. executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  4. Executor executor;
  5. if (ExecutorType.BATCH == executorType) {
  6. executor = new BatchExecutor(this, transaction);
  7. } else if (ExecutorType.REUSE == executorType) {
  8. executor = new ReuseExecutor(this, transaction);
  9. } else {
  10. executor = new SimpleExecutor(this, transaction);
  11. }
  12. if (cacheEnabled) {
  13. executor = new CachingExecutor(executor);//装饰
  14. } executor = (Executor) interceptorChain.pluginAll(executor);
  15. return executor;
  16. }

当执行查询时,先从二级缓存中查询,二级缓存没有时才去走Executor的查询

  1. private Executor delegate;
  2. private TransactionalCacheManager tcm = new TransactionalCacheManager();
  3. public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  4. throws SQLException {
  5. Cache cache = ms.getCache();
  6. ....
  7. List<E> list = (List<E>) tcm.getObject(cache, key);
  8. if (list == null) {
  9. list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  10. tcm.putObject(cache, key, list);
  11. // issue #578 and #116
  12. }
  13. return list;
  14. }
  15. }
  16. return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  17. }

其中TransactionalCacheManager 属性为二级缓存提供了事务能力。

  1. public void commit(boolean required) throws SQLException {
  2. delegate.commit(required);
  3. tcm.commit();
  4. 也就是事务提交时才会将数据放入到二级缓存中去}

总结下二级缓存

  • 二级缓存是层层装饰
  • 二级缓存工作原理是装饰普通执行器
  • 装饰执行器使用TransactionalCacheManager为二级缓存提供事务能力

6、插件

一句话总结mybaits插件:代理,代理,代理,还是代理。
Mybatis的插件原理也是动态代理技术。

  1. public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  2. ..
  3. executor = new SimpleExecutor(this, transaction);
  4. ....
  5. if (cacheEnabled) {
  6. executor = new CachingExecutor(executor);
  7. }
  8. 插件的入口
  9. executor = (Executor) interceptorChain.pluginAll(executor);
  10. return executor;
  11. }
  12. InterceptorChain
  13. public Object pluginAll(Object target) {
  14. for (Interceptor interceptor : interceptors) {
  15. target = interceptor.plugin(target);
  16. }
  17. return target;
  18. }

以分页插件为例,
创建完Executor后,会执行插件的plugn方法,插件的plugn会调用Plugin.wrap方法,在此方法中我们看到了我们属性的JDK动态代理技术。创建Executor的代理类,以Plugin为增强。

  1. QueryInterceptor
  2. public Object plugin(Object target) {
  3. return Plugin.wrap(target, this);
  4. }
  5. public class Plugin implements InvocationHandler {
  6. public static Object wrap(Object target, Interceptor interceptor) {
  7. Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  8. Class<?> type = target.getClass();
  9. Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  10. if (interfaces.length > 0) {
  11. return Proxy.newProxyInstance(
  12. type.getClassLoader(),
  13. interfaces,
  14. new Plugin(target, interceptor, signatureMap));
  15. }
  16. return target;
  17. }
  18. }

最终的执行链:Executor代理类方法—》Plugin.invoke方法—》插件.intercept方法—》Executor类方法