Mybatis架构分析和工作原理

Mybatis工作流程

工作流程图

1、解析配置文件,包括全局配置文件和映射器配置文件,把它们解析成一个 Configuration 对象。
2、创建会 话 工 厂 SqlSessionFactory
3、操作数据库的接口,它在应用程序和数据库中间,代表我们跟数据库之间的一次连接:创建 SqlSession 对象。
4、SqlSession 持有了一个 Executor 对象,用来封装对数据库的操作。
5、在执行器 Executor 执行 query 或者 update 操作的时候我们创建一系列的对象,来处理参数、执行 SQL、处理结果集。

架构分层和模块划分

github代码

代码架构.png

架构图.png

接口层
接口层是最重要的一层,核心对象是 SqlSession,它是上层应用和 MyBatis打交道的桥梁,SqlSession 上定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。

核心处理层
所有跟数据库操作相关的动作都是在这一层完成的,插件也属于核心层,这是由它的工作方式和拦截的对象决定的。

主要做的事情:

  1. 把接口中传入的参数解析并且映射成 JDBC 类型;
  2. 解析 xml 文件中的 SQL 语句,包括插入参数,和动态 SQL 的生成;
  3. 执行 SQL 语句;
  4. 处理结果集,并映射成 Java 对象。

基础支持层
基础支持层主要是一些抽取出来的通用的功能(实现复用),用来支持核心处理层的功能。比如数据源、缓存、日志、xml 解析、反射、IO、事务等等这些功能。

Mybatis缓存

缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟 Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

缓存体系结构
MyBatis 跟缓存相关的类都在 cache 包里面,其中有一个 Cache 接口,只有一个默认的实现类 PerpetualCache,它是用 HashMap 实现的。

这里用到了装饰器模式,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。

通过查看代码,缓存实现类总体可分为三类:基本缓存、淘汰算法缓存、装饰器缓存

缓存实现类 描述 作用 装饰条件
基本缓存 缓存基本实现类 默认是 PerpetualCache,也可以自定义比如RedisCache、EhCache 等,具备基本功能的缓存类
LruCache LRU 策略的缓存 当缓存到达上限时候,删除最近最少使用的缓存(Least Recently Use) eviction="LRU"(默认)
FifoCache FIFO 策略的缓存 当缓存到达上限时候,删除最先入队的缓存 eviction="FIFO"
SoftCache、WeakCache 带清理策略的缓存 通过 JVM 的软引用和弱引用来实现缓存,当 JVM内存不足时,会自动清理掉这些缓存,基于SoftReference 和 WeakReference eviction="SOFT" eviction="WEAK"
LoggingCache 带日志功能的缓存 比如:输出缓存命中率 基本
SynchronizedCache 同步缓存 基于 synchronized 关键字实现,解决并发问题 基本
BlockingCache 阻塞缓存 通过在 get/put 方式中加锁,保证只有一个线程操作缓存,基于 Java 重入锁实现 blocking=true
SerializedCache 支持序列化的缓存 将对象序列化以后存到缓存中,取出时反序列化 readOnly=false(默认)
ScheduledCache 定时调度的缓存 在进行 get/put/remove/getSize 等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存--即每隔一段时间清空一次缓存 flushInterval 不为空
TransactionalCache 事务缓存 在二级缓存中使用,可一次存入多个缓存,移除多个缓存 在TransactionalCacheManager 中用 Map维护对应关系

一级缓存

一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。

一级缓存.png

如果要在同一个会话里面共享一级缓存,这个对象肯定是在 SqlSession 里面创建的,作为 SqlSession 的一个属性。而DefaultSqlSession实现了SqlSession,对应的要实现他的所有方法。

DefaultSqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在 Executor 里面维护,SimpleExecutor /ReuseExecutor/BatchExecutor 的父类BaseExecutor 的构造函数中持有了 PerpetualCache。

同一个会话里面,多次执行相同的 SQL 语句,会直接从内存取到缓存的结果,不会再发送 SQL 到数据库。但是不同的会话里面,即使执行的 SQL 一模一样(通过一个Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。

一级缓存的缺点
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存

二级缓存

二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。二级缓存是默认关闭的

一级缓存和二级缓存同时存在的时候,那个先执行?

二级缓存作为一个作用范围更广的缓存,他是在SqlSession的外层,否则不可能被多个SqlSession共享,而一级缓存是在SqlSession的内部的,所以在工作在一级缓存之前,也就是说只有取不到二级情况下才到会话中去取一级缓存

二级缓存在哪里维护的?

由于二级缓存是跨会话共享的,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,所以需要用到CachingExecutor这个类,启用了二级缓存的话,CachingExecutor对于查询请求,会判断二级缓存中是否有缓存结果,如果有就直接返回,如果没有则委派交给真正的查询器Executor实现类

二级缓存.png
开启二级缓存

1、在 mybatis-config.xml 中配置了(可以不配置,默认是 true),只要没有显式地设置 cacheEnabled=false,都会用 CachingExecutor 装饰基本的执行器。
<setting name" ="cacheEnabled" value ="true"/>

2、在 Mapper.xml 中配置<cache/>标签

<!-- 声明这个 namespace 使用二级缓存 -->
<cache  type ="org.apache.ibatis.cache.impl.PerpetualCache"
    size ="1024" <!—最多缓存对象个数,默认 1024-->
    eviction ="LRU" <!—回收策略-->
    flushInterval ="120000" <!—自动刷新时间 ms,未配置时只有调用时刷新-->
    readOnly =" false" "/> <!— 默认是  false(安全),改为 true 可读写时,对象必须支持序列化 -->

cache属性介绍:

属性 含义 取值
type 缓存实现类 需要实现 Cache 接口,默认是 PerpetualCache
size 最多缓存对象个数 默认1024
eviction 回收策略(缓存淘汰算法) LRU – 最近最少使用的:移除最长时间不被使用的对象(默认)。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval 定时自动清空缓存间隔 自动刷新时间,单位 ms,未配置时只有调用时刷新
readOnly 是否只读 true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。false:读写缓存;会返回缓存对象的拷贝(通过序列化),不会共享。这会慢一些,但是安全,因此默认是 false。改为 false 可读写时,对象必须支持序列化。
blocking 是否使用可重入锁实现缓存的并发控制 true,会使用 BlockingCache 对 Cache 进行装饰默认 false

Mapper.xml 配置了<cache>之后,select()会被缓存。update()、delete()、insert()会刷新缓存。

在单个 Statement ID 上显式关闭二级缓存(默认是 true)
<select id" ="selectBlog" resultMap" ="BaseResultMap" useCache ="false">

验证二级缓存(需要先开启)

1、事务不提交,二级缓存不存在
因为二级缓存使用 TransactionalCacheManager(TCM)来管理,最后又调用了 TransactionalCache的getObject()、putObject和commit()方法,TransactionalCache里面又持有了真正的 Cache 对象,比如是经过层层装饰的 PerpetualCache。

在 putObject 的时候,只是添加到了 entriesToAddOnCommit 里面,只有它的commit()方法被调用的时候才会调用 flushPendingEntries()真正写入缓存。它就是在DefaultSqlSession 调用 commit()的时候被调用的。

2、使用不同的 session 和 mapper,二级缓存可以跨 session 存在

BlogMapper mapper1 = session1.getMapper(BlogMapper. class);
System. out .println(mapper1.selectBlogById(1));
// 事务不提交的情况下,二级缓存不会写入
session1.commit();
BlogMapper mapper2 = session2.getMapper(BlogMapper. class);
System. out .println(mapper2.selectBlogById(1));

3、在其他的 session 中执行增删改操作,验证缓存会被刷新(清空缓存)
在 CachingExecutor 的 update()方法里面会调用 flushCacheIfRequired(ms),isFlushCacheRequired 就是从标签里面渠道的 flushCache 的值。而增删改操作的flushCache 属性默认为 true。

二级缓存开启的时机

1、因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。

2、如果多个 namespace 中有针对于同一个表的操作,比如 Blog 表,如果在一个namespace 中刷新了缓存,另一个 namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个 Mapper 里面只操作单表的情况使用。

第三方缓存做二级缓存

可以通过实现 Cache 接口来自定义二级缓存。

MyBatis 官方提供了一些第三方缓存集成方式,比如 ehcache 和 redis

操作步骤:

<!-- 1.pom 文件引入依赖:-->
<dependency>
    < groupId>org.mybatis.caches</ groupId>
    < artifactId>mybatis-redis</ artifactId>
    < version>1.0.0-beta2</ version>
</dependency>

<!--2.Mapper.xml 配置,type 使用 RedisCache-->
<cache  type ="org.mybatis.caches.redis.RedisCache" eviction" ="FIFO"  flushInterval" ="60000"  size" ="512"  readOnly ="true"/>

<!--3.redis.properties 配置 -->
host= localhost
port= 6379
connectionTimeout= 5000
soTimeout= 5000
database=0 0

源码解读

分为四个步骤来分析:
1.配置解析
2.会话创建
3.获取Mapper对象
4.执行SQL

配置解析
配置解析的过 程全 部 只 解 析 了 两 种 文 件 。 一个是mybatis-config.xml 全局配置文件,一个是很多的Mapper.xml文件

//解析文件
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

解析配置文件之后new了一个SqlSessionFactoryBuilder(建造者模式),返回了一个SqlSessionFactory对象(单例模式)

从SqlSessionFactoryBuilder#build()方法就是XMLConfigBuilder对象的解析

XMLConfigBuilder

这个类是抽象类BaseBuilder的一个子类,专门用来解析全局配置文件,针对不同的构建目标还有其他的子类,比如:
XMLMapperBuilder:解析 Mapper 映射器
XMLStatementBuilder:解析增删改查标签

解析文件流,创建了一个parser,返回了一个Configuration类,然后调用parser.parse()方法

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

这个方法首先会检查是不是已经解析过,也就是说在应用的生命周期里面,config 配置文件只需要解析一次,生成的 Configuration 对象也会存在应用的整个生命周期中。
parseConfiguration这个方法就是加载 config 文件里面的所有一级标签。

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

一级标签解析

propertiesElement()

第一个是解析<properties>标签,读取我们引入的外部配置文件。

这里面又有两种类型,一种是放在 resource 目录下的,是相对路径,一种是写的绝对路径的。

解析的最终结果就是我们会把所有的配置信息放到名为 defaults 的 Properties 对象里面,最后把XPathParser 和 Configuration 的 Properties 属性都设置成我们填充后的 Properties对象。

settingsAsProperties) ()

把<settings>标签也解析成了一个 Properties 对象,对于<settings>
标签的子标签的处理在后面。

loadCustomVfs(settings)

loadCustomVfs 是获取 Vitual File System 的自定义实现类。
比如我们要读取本地文件,或者 FTP 远程文件的时候,就可以用到自定义的 VFS 类。我们根据<settings>标签里面的<vfsImpl>标签,生成了一个抽象类 VFS 的子类,并且赋值到 Configuration中。

private void loadCustomVfs(Properties props) throws ClassNotFoundException {
    String value = props.getProperty("vfsImpl");
    if (value != null) {
      String[] clazzes = value.split(",");
      for (String clazz : clazzes) {
        if (!clazz.isEmpty()) {
          @SuppressWarnings("unchecked")
          Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
          configuration.setVfsImpl(vfsImpl);
        }
      }
    }
  }
loadCustomLogImpl(settings)

loadCustomLogImpl 是根据<logImpl>标签获取日志的实现类,我们可以用到很多的日志的方案,包括 LOG4J,LOG4J2,SLF4J 等等。这里生成了一个 Log 接口的实现类,并且赋值到 Configuration 中。

typeAliasesElement()

有两种定义方式,一种是直接定义一个类的别名,一种就是指定一个包,那么这个 package 下面所有的类的名字就会成为这个类全路径的别名。

类的别名和类的关系,我们放在一个 TypeAliasRegistry 对象里面。

private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          String alias = child.getStringAttribute("alias");
          String type = child.getStringAttribute("type");
          try {
            Class<?> clazz = Resources.classForName(type);
            if (alias == null) {
              typeAliasRegistry.registerAlias(clazz);
            } else {
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
          }
        }
      }
    }
  }
pluginElement()

插件标签,比如 Pagehelper 的翻页插件,或者我们自定义的插件。<plugins>标签里面只有<plugin>标签,<plugin>标签里面只有<property>标签。

标签解析完以后,会生成一个 Interceptor 对象,并且添加到 Configuration 的InterceptorChain 属性里面,它是一个 List。

objectFactoryElement() 、objectWrapperFactoryElement()

用来实例化对象,分 别 生 成 ObjectFactory 、ObjectWrapperFactory 对象,同样设置到 Configuration 的属性里面。

reflectorFactoryElement()

解析 reflectorFactory 标签,生成 ReflectorFactory 对象

settingsElement(settings)

对<settings>标签里面所有子标签的处理,所有的值,都会赋值到 Configuration 的属性里面去。

private void settingsElement(Properties props) {
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
    configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
    configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
    configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
    configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
    configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
    configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
    configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
    configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
    configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
    configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
    configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
    configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
    configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
    configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
    configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
    configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
    configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
    configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
    configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
    configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
    configuration.setLogPrefix(props.getProperty("logPrefix"));
    configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
  }
environmentsElement()

一个 environment 就是对应一个数据源,所以在这里我们会根据配
置的<transactionManager>创建一个事务工厂,根据<dataSource>标签创建一个数据源,最后把这两个对象设置成 Environment 对象的属性,放到 Configuration 里面。

databaseIdProviderElement()

解析 databaseIdProvider 标签,生成 DatabaseIdProvider 对象(用来支持不同厂商的数据库)。

typeHandlerElement()

TypeAlias 一样,TypeHandler 有两种配置方式,一种是单独配置一个类,一种是指定一个 package。最后我们得到的是 JavaType 和 JdbcType,以及用来做相互映射的 TypeHandler 之间的映射关系。
最后存放在 TypeHandlerRegistry 对象里面。

private void typeHandlerElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          String javaTypeName = child.getStringAttribute("javaType");
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          String handlerTypeName = child.getStringAttribute("handler");
          Class<?> javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }
mapperElement()
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //包
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          //相对路径
          String resource = child.getStringAttribute("resource");
          //绝对路径
          String url = child.getStringAttribute("url");
          //单个接口
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

这个标签的解析主要做了几个事情:
1.判断
首先会判断是不是接口,只有接口才解析;然后判断是不是已经注册了,单个 Mapper重复注册会抛出异常。
2.注册
XMLMapperBuilder.parse()方法,是对 Mapper 映射器的解析。里面有两个方法:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

configurationElement()—— 解析所有的子标签 , 其中
buildStatementFromContext()最终获得 MappedStatement 对象。

bindMapperForNamespace()——把 namespace(接口类型)和工厂类绑定起来。

无论是按 package 扫描,还是按接口扫描,最后都会调用到 MapperRegistry 的addMapper()方法。

MapperRegistry 里面维护的其实是一个 Map 容器,存储接口和代理工厂的映射关系。

3.注释处理
除了解析映射文件之外,还会去解析 Mapper 接口方法上的注解,在 addMapper()方法里面创建了一个 MapperAnnotationBuilder,调用parse()方法

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

parseCache() 和 parseCacheRef() 方 法 其 实 是 对@CacheNamespace 和@CacheNamespaceRef 这两个注解的处理。

parseStatement()方法里面的各种 getAnnotation(),都是对注解的解析,比如@Options,@SelectKey,@ResultMap 等等。

最后同样会解析成 MappedStatement 对象,也就是说在 XML 中配置,和使用注解配置,最后起到一样的效果。

4.处理完成
如果注册没有完成,还要从 Map 里面 remove 掉。

if (!loadCompleted) {
    knownMappers.remove(type);
}

MapperRegistry 也会放到 Configuration 里面去,最后调用另一个 build()方法,返回 DefaultSqlSessionFactory。

配置解析小结

1.主要完成了 config 配置文件、Mapper 文件、Mapper 接口上的注解的解析。

2.得到了一个最重要的对象 Configuration,这里面存放了全部的配置信息,它在属性里面还有各种各样的容器。

3.返回了一个 DefaultSqlSessionFactory,里面持有了 Configuration的实例。

创建会话

数据库的每一次连接,都需要创建一个会话,我们用openSession()方法来创建。

DefaultSqlSessionFactory#openSessionFromDataSource()

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      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();
    }
  }

由源码可以得出:要先从 Configuration 里面拿到 Enviroment,Enviroment 获取事务工厂,最后通过执行器Executor 处理请求(sql)

1.创建Transaction
这里有2种方式获取:JDBC、MANAGED

1.1.JDBC(JdbcTransactionFactory-> JdbcTransaction)
如果配置的是 JDBC,则会使用 Connection 对象的 commit()、rollback()、close()管理事务。

1.2.MANAGED(ManagedTransactionFactory->ManagedTransaction)
如果配置成 MANAGED,会把事务交给容器来管理,比如 JBOSS,Weblogic。因为我们跑的是本地程序,如果配置成 MANAGE 不会有任何事务。

PS:如 果 是 Spring + MyBatis,则 没 有 必 要 配 置 , 因 为 我 们 会 直 接 在applicationContext.xml 里面配置数据源和事务管理器,覆盖 MyBatis 的配置。

2.创建Executor(执行器)
Executor 的基本类型有三种:SIMPLE、BATCH、REUSE,默认是 SIMPLE(settingsElement()读取默认值),他们都继承了抽象类 BaseExecutor。

三种执行器的区别:
SimpleExecutor:每执行一次 update 或 select,就开启一个Statement 对象,用完立刻关闭 Statement 对象。

ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map 内,供下一次使用。简言之,就是**重复使用 **Statement 对象。

BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行executeBatch()批处理。与 JDBC 批处理相同。

小结

创建会话的过程,获得了一个DefaultSqlSession,里面包含了一个Executor,用来执行SQL

获取Mapper对象

获得DefaultSqlSession之后,必须找到 Mapper.xml 里面定义的Statement ID,才能执行对应的 SQL 语句。

找到Statement ID有2中方式:
1.直接调用session的方法,在参数里面传入Statement ID(硬编码),如果写错了很难调试,因为编译的时候不会报错,在用的时候才会说找不到

2.定义一个接口,在调用Mapper接口的方法,由于我们的接口名称跟 Mapper.xml 的 namespace 是对应的,接口的方法跟statement ID 也都是对应的,可以快速定位(Mybatis后面的版本默认用这种方式)

Mapper对象的获取?

在DefaultSqlSession#getMapper()方法,调用了 Configuration#getMapper()方法,Configuration 的 getMapper()方法,又调用了 MapperRegistry 的 getMapper()方法,在最后返回了一个mapperProxyFactory.newInstance(sqlSession)

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

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

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 {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

在解析 mapper 标签和 Mapper.xml 的时候已经把接口类型和类型对应的 MapperProxyFactory 放到了一个 Map Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();中。获取 Mapper 代理对象,实际上是从Map 中获取对应的工厂类后mapperProxyFactory.newInstance(sqlSession)

在这里用到了动态代理,那Mybatis的动态代理和JDK的动态代理有什么区别?(这个就是面试很常问的一个问题)
JDK代理.png

用过JDK动态代理的都知道,在实现了 InvocationHandler 的代理类里面,需要传入一个被代理对象的实现类。

Mybatis动态代理.png

这里 不需要实现是因为:我们只需要根据接口类型+方法的名称,就可以找到Statement ID了,而唯一要做的一件事情也是这件,所以不需要实现类。在MapperProxy里面直接执行逻辑(也就是执行 SQL)就可以。

小结

获得 Mapper 对象的过程,实质上是获取了一个 MapperProxy 的代理对象。MapperProxy 中有 sqlSession、mapperInterface、methodCache。

执行sql

上面提到的Mapper都是MapperProxy代理对象,所以所有的方法都值执行invoker方法

1.Mapper.invoke()

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //首先判断是否需要去执行 SQL,还是直接执行方法。
      //Object 本身的方法和 Java 8 中接口的默认方法不需要去执行SQL
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        if (privateLookupInMethod == null) {
          return invokeDefaultMethodJava8(proxy, method, args);
        } else {
          return invokeDefaultMethodJava9(proxy, method, args);
        }
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //获取缓存,保存了方法签名和接口方法的关系
    //为了提升 MapperMethod 的获取速度
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

2.MapperMethod.execute()

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    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()) {
          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 {
          Object param = method.convertArgsToSqlCommandParam(args);
          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;
  }

MapperMethod里面主要有两个参数 ,一 个是SqlCommand , 一 个 是MethodSignature,这两个都是 MapperMethod 的内部类。

在execute方法中,根据不同的type和返回类型:
调用 convertArgsToSqlCommandParam()将参数转换为 SQL 的参数。
调用 sqlSession 的 insert()、update()、delete()、selectOne ()方法
以查询为例,会走到 selectOne()方法。

3.DefaultSqlSession.selectOne()
sqlSession.selectOne(command.getName(), param)最终调用了selectList()方法

public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

在 SelectList()中,我们先根据 command name(Statement ID)从 Configuration中拿到 MappedStatement,这个 ms 上面有我们在 xml 中配置的所有属性,包括 id、statementType、sqlSource、useCache、入参、出参等等。执行了 Executor 的 query()方法。

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

3.1.执行器的选择
除了SIMPLE/REUSE/BATCH,还有一种包装类型CachingExecutor。

如果启用了二级缓存,就会先调用 CachingExecutor 的 query()方法,里面有缓存相关的操作,然后才是再调用基本类型的执行器。

在没有开启二级缓存的情况下,先会走到 BaseExecutor 的 query()方法(否则会先走到 CachingExecutor)。

4.BaseExecutor.query()
4.1.创建CacheKey
从 Configuration 中获取 MappedStatement, 然后从 BoundSql 中获取 SQL 信息,创建 CacheKey。这个 CacheKey 就是缓存的 Key。
然后再调用另一个 query()方法。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

4.2.清空本地缓存
queryStack 用于记录查询栈,防止递归查询重复处理缓存。

flushCache=true 的时候,会先清理本地缓存(一级缓存):clearLocalCache();

如果没有缓存,会从数据库查询:queryFromDatabase()

如果 LocalCacheScope == STATEMENT,会清理本地缓存。

4.3.从数据库查询
4.3.1.缓存
先在缓存用占位符占位。执行查询后,移除占位符,放入数据。

4.3.2.查询
执行 Executor 的 doQuery();默认是 SimpleExecutor。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //flushCache=true 的时候,会先清理本地缓存(一级缓存)
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      //用于记录查询栈,防止递归查询重复处理缓存
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        //如果没有缓存,会从数据库查询
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      //如果 LocalCacheScope == STATEMENT,会清理本地缓存。
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

5.SimpleExecutor.doQuery()

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

5.1.创建StatementHandler
在 configuration.newStatementHandler()中,new 一个StatementHandler,先得到 RoutingStatementHandler。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

RoutingStatementHandler 里 面 没 有 任 何 的 实 现 , 是 用 来 创 建 基 本 的StatementHandler 的。这里会根据 MappedStatement 里面的 statementType 决定StatementHandler 的 类 型 。 默 认 是 PREPARED ( STATEMENT 、 PREPARED 、CALLABLE)。

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }
  }

StatementHandler 里面包含了处理参数的 ParameterHandler 和处理结果集的ResultSetHandler。

这2个对象是在这里创建的

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;

    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

这三个对象都是可以被插件拦截的四大对象之一,所以在创建之后都要用拦截器进行包装的方法。

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

5.2.创建Statement
用 new 出来的 StatementHandler 创建 Statement 对象的prepareStatement()方法对语句进行预编译,处理参数。
handler.query(stmt, resultHandler);

5.3.执行StatementHandler的query()方法
RoutingStatementHandler 的 query()方法,delegate 委派,最终执行 PreparedStatementHandler 的 query()方法。

5.4.执行PreparedStatement 的 execute() 方法
JDBC 包中的 PreparedStatement 的执行了。

5.5.ResultSetHandler处理结果集
resultSetHandler.handleResultSets(ps);
根据代码可知:我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况,一般只有一个结果集。如果下面的这个 while 循环我们也不用,就是执行一次。然后会调用 handleResultSet()方法。

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

源码核心对象总结

对象 关联对象 作用
Configuration MapperRegistry TypeAliasRegistry TypeHandlerRegistry 包含了 MyBatis 的所有的配置信息
SqlSession SqlSessionFactory DefaultSqlSession 对操作数据库的增删改查的 API 进行了封装,提供给应用层使用
Executor BaseExecutor SimpleExecutor BatchExecutor ReuseExecutor MyBatis 执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护
StatementHandler BaseStatementHandler SimpleStatementHandler PreparedStatementHandler CallableStatementHandler 封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 Statement 结果集转换成 List 集合
ParameterHandler DefaultParameterHandler 把用户传递的参数转换成 JDBC Statement 所需要的参数
ResultSetHandler DefaultResultSetHandler 把 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合
MapperProxy MapperProxyFactory 代理对象,用于代理 Mapper 接口方法
MappedStatement SqlSource BoundSql MappedStatement 维护了一条<select、update、delete、insert>节点的封装,包括了 SQL 信息、入参信息、出参信息
Mybatis创建会话工厂类.png
Mybatis创建会话.png
Mybatis调用代理对象方法,执行SQL.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容