Mybatis运行流程源码解析

一、debug源码环境准备

1、下载mybatis源码

调试mybatis源码理解mybatis工作原理,首先需要下载mybatis源码!

下载地址:https://github.com/mybatis/mybatis-3

带中文注释的源码: https://github.com/tuguangquan/mybatis

在这里插入图片描述

2、下载mybatis-parent源码并编译

**注意:**下载的mybatis-parent版本要和mybatis源文件pom.xml 版本一致。

下载地址:https://github.com/mybatis/parent

在这里插入图片描述

切换到下载的mybatis-parent目录,执行:

 mvn clean install

切换到下载的mybatis源码目录,执行:

mvn clean  

mvn install -Dmaven.test.skip=true 

上面的步骤进行完毕后,就可以使用idea导入mybatis源码了!

3、调整配置(选做)

在pom.xml的节点下增加如下配置,读取classpath下的配置文件!

<resource>
    <directory>src/main/java</directory>
        <includes>
          <!--properties的配置文件会和编译后的class文件放在一起-->
          <include>**/*.properties</include>
    </includes>
</resource>
<resource>
 	<!--加载配置的资源-->
    <directory>src/main/resources</directory>
</resource>

注释掉pom.xml中的pdf插件()

  <!--
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-pdf-plugin</artifactId>
      </plugin>
  -->

如果执行过程中有报错,可以根据报错信息注释掉相应的依赖即可!

4、编写测试代码

public static void main(String[] args) throws IOException {
    // 通过字节流读取配置文件
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 创建SqlSessionFacory
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
    // 获取sqlSession
    SqlSession sqlSession = factory.openSession();
    // 获取动态代理产生的mapper接口对象
    MyBlogMapper mapper = sqlSession.getMapper(MyBlogMapper.class);
    // 调用接口方法
    Blog blog = mapper.selectBlog(1);
    System.out.println("blog:"+blog);
  }

二、整体流程说明

mybatis运行流程大致分为以下两步:

第一,构建,也就是解析我们写的xml配置,将其变成它所需要的对象(configuration)。

​ ①、解析mybatis-config.xml信息

​ ②、解析xxxMapper.xml信息

​ ③、解析xxxMapper.xml配置文件中的每个标签信息(比如)

第二,就是执行,在构建完成的基础上,去执行我们的SQL,完成与Jdbc的交互。

​ ①、先获取mapper接口的代理对象

​ ②、通过代理对象调用方法,执行操作。

所以下面就按照这样的步骤来分析!见下面的 【三】和【四】

三、解析xml配置文件

解析xml配置文件也分为两步:

** 第一:**解析全局配置文件mybatis-config.xml

** 第二:**解析与我们的mapper接口对应的xxxxMapper.xml文件,解析完毕后会将每一个xxxMapper.xml中的解析结果保存到Configuration对象的mappedStatements(Map结构)中,并且将每一个xxxMapper.xml信息都保存到 knownMappers(Map结构)中。

最后将解析的配置文件信息整合到configuration类中!所有解析xml配置文件的最终结果就是得到一个configuration对象。

public class Configuration {
  // 封装了所有的xxXMapper.xml配置文件中解析出的sql信息
  protected final Map<String, MappedStatement> mappedStatements;
}

Ⅰ、前期准备

为了更好的理解这个解析流程,这里先贴出探究源码的过程中会用到的内容(测试代码、配置文件、解析流程图等)。

1)、测试代码

public static void main(String[] args) throws IOException {
    // 通过字节流读取配置文件
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 创建SqlSessionFacory
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
    // 获取sqlSession
    SqlSession sqlSession = factory.openSession();
    // 获取动态代理产生的mapper接口对象
    MyBlogMapper mapper = sqlSession.getMapper(MyBlogMapper.class);
    // 调用接口方法
    Blog blog = mapper.selectBlog(1);
    System.out.println("blog:"+blog);
  }

2)、mybatis-config.xml和mapper.mxl配置文件

①、mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <properties resource="jdbc.properties">
      <property name="password" value="root"></property>
  </properties>
<!--  指定默认执行器类型
  <settings>
    <setting name="defaultExecutorType" value="BATCH"/>
  </settings>
-->
    <settings>
      <!--开启二级缓存-->
      <setting name="cacheEnabled" value="true"/>
    </settings>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
   <mapper resource="mapper/MyBlogMapper.xml"/>
    <!--<mapper class="bat.ke.qq.com.mapper.BlogMapper"></mapper>-->
  </mappers>
</configuration>

②、MyBlogMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.demo.mapper.MyBlogMapper">
<!--  <cache eviction="LRU" type="bat.ke.qq.com.cache.MybatisRedisCache"/>-->

  <select  id="selectBlog" resultType="org.apache.ibatis.demo.entity.Blog">
    select * from Blog where id = ${id}
  </select>

  <insert id="insertBlog" parameterType="org.apache.ibatis.demo.entity.Blog">
    insert into Blog (id,username,context) values (#{id}, #{username}, #{context})
  </insert>

</mapper>

3)、解析配置文件流程图

这里为了下面看代码容易理解,先贴上解析配置文件的流程图!当把整个流程过完以后再回头来看这个流程图就非常清晰了!

在这里插入图片描述

Ⅱ、解析过程探究

1)、解析mybatis-config.xml文件

由SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);代码debug进去

①、build()

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //解析mybatis-config.xml
      // 委托XMLConfigBuilder来解析xml文件,并构建
      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.
      }
    }
  }

②、build(parser.parse())

  // 解析配置文件信息
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 解析mybatis-config.xml配置文件顶层节点 <configuration>
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

③、解析mybatis-config.xml配置文件核心代码

/**
   * 解析配置mybatis-config.xml的顶层节点(即:<configuration>),
   * 这里会解析出<configuration>标签下的所有一级子标签
   * @param root 顶层节点
   */
  private void parseConfiguration(XNode root) {
    try {
      //分步骤解析
      //1.properties
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      //2.类型别名
      typeAliasesElement(root.evalNode("typeAliases"));
      //3.插件
      pluginElement(root.evalNode("plugins"));
      //4.对象工厂
      objectFactoryElement(root.evalNode("objectFactory"));
      //5.对象包装工厂
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      //6.设置
      settingsElement(settings);
      // 环境信息
      environmentsElement(root.evalNode("environments"));
      //8.databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //9.类型处理器
      typeHandlerElement(root.evalNode("typeHandlers"));
      //10.映射器,从mybatis-config.xml配置文件中的<configration>标签中解析<mappers>标签
      // 这里就是解析Mapper.xml文件的地方,下面分析
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

到这里,解析mybatis-config.xml配置文件的流程就完毕了,解析完毕后会将mybatis-config.xml配置文件信息封装为一个Configuration对象返回!

2)、解析xxxMapper.xml文件

说明:

这里解析xxxMapper.xml文件,解析完毕后,会将 xxxMapper.xml自身的信息添加到一个名为 knownMappers的map集合中。

mybatis-config.xml文件中我们一定会写一个叫做的标签,这个标签中的<mapper>节点存放了我们对数据库进行操作的SQL语句。首先在看源码之前,我们先回忆一下我们在mapper标签内通常会怎样进行配置,通常有如下几种配置方式

<!-- 注意 mapper节点中,可以使用resource/url/class三种方式获取mapper-->
<mappers>
    <!-- 通过配置文件路径 -->
  <mapper resource="mapper/DemoMapper.xml" ></mapper>
    <!-- 通过Java全限定类名 -->
  <mapper class="com.mybatistest.TestMapper"/>
   <!-- 通过url 通常是mapper不在本地时用 -->
  <mapper url=""/>
    <!-- 通过包名 -->
  <package name="com.mybatistest"/>
</mappers>

①、解析xxxMapper.xml

/**
   * 扫描配置mybatis-config.xml文件中的<mappers>节点,根据不同的配置方式,将该节点下指定的所有	
   * mapper接口添加到一个Map集合中
   * @param parent
   * @throws Exception
   */
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //10.0自动扫描包下所有映射器
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          //将包下所有类加入到mapper(一个Map集合,Map<Class<?>, MapperProxyFactory<?>>)
          // 这里其实就是循环的调用addMapper()方法
          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) {
            //10.1使用类路径
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //映射器比较复杂,调用XMLMapperBuilder
            //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 【重要】解析mapper.xml配置文件中的标签,比如:<select/>、<update/>等
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //10.2使用绝对url路径
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //映射器比较复杂,调用XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            // 【重要】解析mapper.xml配置文件中的标签,比如:<select/>、<update/>等
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            //10.3使用java类名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            // 【重要】直接把这个映射加入配置类中(一个Map集合,Map<Class<?>, MapperProxyFactory<?>>),源码在下面的 【②】
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

②、将解析到的xxxMapper.xml信息添加到名为knownMappers的Map中

/**
   * 看一下如何添加一个映射
   * @param type
   * @param <T>
   */
public class MapperRegistry {
    
  // 将已经添加的映射都放入HashMap,一个mapper接口就对应着一个key--value键值对
  // key:接口的全限定名,value: mapper接口对应的MapperProxyFactory
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    
  public <T> void addMapper(Class<T> type) {
    //mapper必须是接口!才会添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重复添加了,报错
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 【添加】:将解析到的每一个mapper.xml添加到map集合中,
        // key: mapper接口的全限定名,
        // value:根据mapper接口创建的MapperProxyFactory对象 
        knownMappers.put(type, new MapperProxyFactory<>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
}

上面两步的作用是:举个例子,假如我在mybatis-config.xml中有如下配置:

<mappers> 
 <mapper resource="mapper/MyBlogMapper.xml"/>  
</mappers>

那么在上面两步就是,找到 节点下的所有节点并解析他们,将解析后的mapper的全限定名作为key,new一个mappre相关的MapperProxyFactory作为value,然后put到knownMappers中。

==注意:==上面代码还有一句 mapperParser.parse();非常重要,我们将在下面做解析!

3)、解析xxxMapper.xml中的子标签

接上面的 mapperParser.parse();代码继续debug进去,这里就是解析每一个mapper.xml文件中的标签,比如:、等等。解析完成后,每一个被正确解析的子标签(、 、 、 )都会被放入一个名为 mappedStatements的Map集合中!

1、mapperParser.parse();

public void parse() {
    //如果没有加载过再加载,防止重复加载
    if (!configuration.isResourceLoaded(resource)) {
      //【重要】解析mapper文件节点(主要)
      configurationElement(parser.evalNode("/mapper"));
      //标记一下,已经加载过了
      configuration.addLoadedResource(resource);
      //绑定映射器到namespace
      bindMapperForNamespace();
    }

    // 重新解析之前解析不了的节点
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

2、configurationElement()

private void configurationElement(XNode context) {
    try {
      //1.配置namespace,检查namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      //2.配置cache-ref
      cacheRefElement(context.evalNode("cache-ref"));
      //3.配置cache
      cacheElement(context.evalNode("cache"));
      //4.配置parameterMap(已经废弃,老式风格的参数映射)
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //5.配置resultMap(高级功能)
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //6.配置sql(定义可重用的 SQL 代码段)
      sqlElement(context.evalNodes("/mapper/sql"));
      //7.【重要】配置select|insert|update|delete TODO
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

3、buildStatementFromContext()

// 配置select|insert|update|delete
  private void buildStatementFromContext(List<XNode> list) {
    //调用构建语句
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

4、parseStatementNode()

// 解析Statement
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    //如果databaseId不匹配,退出
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    //============================================================================
    // 这里省略很多代码。。。
    // 这里的代码主要作用就是:假如mapper.xml中有一个select语句如下:
    // <select  id="selectBlog" resultType="org.apache.ibatis.demo.entity.Blog">
    // 		select * from Blog where id = ${id}
    // </select>
    // 这里相当于是解析这条<select>标签中的各种属性,返回结果集,是select类型还是update类型还是   	// delete类型呀、是否要缓存呀,参数类型,超时时间呀 等等。
    //============================================================================

	//【重要】又去调助手类,添加到MappedStatement中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

5、addMappedStatement()

public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {
    
    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    
    //为id加上namespace前缀
    id = applyCurrentNamespace(id, false);
    //是否是select语句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    //又是建造者模式
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
    statementBuilder.resource(resource);
    statementBuilder.fetchSize(fetchSize);
    statementBuilder.statementType(statementType);
    statementBuilder.keyGenerator(keyGenerator);
    statementBuilder.keyProperty(keyProperty);
    statementBuilder.keyColumn(keyColumn);
    statementBuilder.databaseId(databaseId);
    statementBuilder.lang(lang);
    statementBuilder.resultOrdered(resultOrdered);
    statementBuilder.resulSets(resultSets);
    setStatementTimeout(timeout, statementBuilder);

    //1.参数映射
    setStatementParameterMap(parameterMap, parameterType, statementBuilder);
    //2.结果映射
    setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
    setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

    MappedStatement statement = statementBuilder.build();
    //建造好调用configuration.addMappedStatement
    //比如mapper.xml中有一条select 语句,这里会将这条select语句解析后放入到MappedStatement中(MappedStatement是一个Map)
    configuration.addMappedStatement(statement);
    return statement;
  }

4)、总结

上面的解析XML配置文件的代码繁琐但不复杂,里面主要也就是对xml的节点进行解析 。

总结一下大致流程就是:

①、首先是解析mybatis-config.xml配置文件信息。

②、然后是解析mybatis-config.xml配置文件中的节点下的每一个,然后将解析到的信息以接口的全限定名作为 key, mapper接口对应的MapperProxyFactory作为 value,放入到一个Map集合中(knownMappers)。

③、然后解析每一个mapper.xml里面的子节点(比如、、、),解析这些节点里面的每一个属性,做校验等,解析后的信息保存在MappedStatement中。

④、然后将这些节点按照 接口的全限定名 + 方法名作为 key,**节点解析后的信息(MappedStatement)**作为 value 放入到configuration对象的mappedStatements(Map)属性中。

说明: 这里理一下 Configuration 、 knownMappers 、 mappedStatements之间的关系,如下:

public class Configuration {

  // 环境信息
  protected Environment environment;
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  // 存放mapper.xml解析后的一个个sql信息,key: 为mapper接口全限定名 + 方法名
  protected final Map<String, MappedStatement> mappedStatements;  
  
 }
public class MapperRegistry {

    // 配置信息
	private final Configuration config;
    // 存放一个个的mapper.xml信息,key:为mapper接口的全限定名
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
 	// 构造方法
   public MapperRegistry(Configuration config) {
    this.config = config;
  }

}

到这里mybatis解析配置文件的流程就完了!

四、执行SQL

上面第一步解析了我们的mybatis-config.xml和mapper.xml配置文件并封装为configuration对象后,即可通过获取SqlSession来调用接口的方法进行查询了,代码如下!

// 创建SqlSessionFacory,这步会解析xml配置文件
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession
SqlSession sqlSession = factory.openSession();
// 获取mapper的动态代理对象
MyBlogMapper mapper = sqlSession.getMapper(MyBlogMapper.class);
// 执行查询方法
Blog blog = mapper.selectBlog(1);

Ⅰ、动态代理创建mapper接口的代理对象

跟着 MyBlogMapper mapper = sqlSession.getMapper(MyBlogMapper.class);一路debug下去,会发现如下代码:(动态代理,投鞭断流)

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 通过传入的接口的Class对象,去knownMappers中获取Mapper接口相关信息(mapperProxyFactory)
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 通过mapperProxyFactory创建代理对象,并返回
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
public T newInstance(SqlSession sqlSession) {
    // 传入mapper接口信息,创建mapperProxy对象
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    // 调用下面的方法,返回代理对象
    return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
    // 用JDK自带的动态代理生成映射器
    // 这里要注意:mapperProxy中组合了一个mapper接口,这里是为mapperProxy创建代理对象,代理对象里的方法实现,其实就是MapperProxy里的invoke方法,每次调用接口的方法都会调用到 MapperProxy 里的invoke方法
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

上面的代码便是创建mapper接口代理的代码过程,首先会通过传入的mapper的Class信息去 knownMappers中获取到一个 MapperProxyFactory,然后调用MapperProxyFactory的 newInstance方法,在newInstance方法中通过传入mapper接口信息,sqlSession信息,methodCache创建一个mapperProxy。最后使用JDK代理为mapperProxy创建代理对象(Proxy.newProxyInstance())!

Ⅱ、调用mapper接口方法

1)、代理对象的方法调用

当调用mapper接口中的方法时,其实时调用的代理对象中的对应方法!以debug的方式进入方法调用处!会发现进入了MapperProxyinvoke()方法中!

// 代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
// 并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 如果是Object的方法(toString、hashCode等)则直接执行
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        // 从这里进去
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
2)、代理对象核心方法

从上面进去以后会发现最终进入了 MapperMethodexecute()方法,该方法就是mapper接口方法的实现。在这里判断操作类型(增删改查),获取到要执行的sql,获取sql参数,替换占位符,处理返回结果集等

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
    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);
          //如果结果是map
        } 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;
  }
3)、调用SqlSession的查询方法

这里会根据上面的返回结果集条件判断(是否有多条返回值、返回结果集是否是Map等)选择不同的查询方法,但核心的就只有两个方法:一个是selectOne(),一个是selectList()!在selectOne()里面其实也是调用了selectList()方法在做查询。

3.1、sqlSession.selectOne()
//核心selectOne
@Override
public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    //转而去调用selectList,很简单的,如果得到0条则返回null,得到1条则返回1条,得到多条报TooManyResultsException错
    // 特别需要注意的是当没有查询到结果的时候就会返回null。因此一般建议在mapper中编写resultType的时候使用包装类型
    //而不是基本类型,比如推荐使用Integer而不是int。这样就可以避免NPE
    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;
    }
  }
3.2、selectList()
  // 核心selectList
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // 根据statement id找到对应的 MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 转而用执行器来查询结果,注意这里传入的ResultHandler是null
      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.3 Executor简单说明

Executor是一个接口,该接口的实现以及继承 关系图 如下:

在这里插入图片描述

可以看出 BaseExecutor以及 CachingExecutor直接继承了Executor,BaseExecutor类下有三个子类。这里对这几个执行器做一个简单的说明!

**Executor接口:**该接口主要提供了update、query方法及事物相关的方法接口 !

CachingExecutor: CachingExecutor 从内存中获取数据, 在查找数据库前先查找缓存,若没有找到的话调用delegate从数据库查询,并将查询结果存入缓存中 。该执行器主要是针对于mybatis的二级缓存(需要在配置中开启二级缓存才可用)

BaseExecutor: 抽象类BaseExecutor采用模板方法设计模式,具体的实现在其子类:BatchExecutor、ReuseExecutor和SimpleExecutor

**SimpleExecutor:**简单执行器,每次执行SQL需要预编译SQL语句。

BatchExecutor:批处理执行器,只针对修改操作的SQL语句预编译一次,并且需要手动刷新SQL执行才生效

**ReuseExecutor:**可重用执行器,同一SQL语句执行只需要预编译一次SQL语句。

3.4、executor二级缓存查询逻辑

在上面3.2源码中执行了 executor.query()方法,调用执行器的查询方法,我这里开启了二级缓存,所以会走到缓存执行器的query()方法。

在这里插入图片描述

// CachingExecutor里的query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取sql信息
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    // 调用本类的另一个query方法,如下
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
// CachingExecutor里的query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    //默认情况下是没有开启缓存的(二级缓存).要开启二级缓存
    //简单的说,就是先查CacheKey,查不到再委托给实际的执行器去查
    if (cache != null) {
      // 如果需要则刷新缓存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
          // 先通过事务缓存管理器,在二级缓存中取数据
        List<E> list = (List<E>) tcm.getObject(cache, key);
        // 如果从二级缓存中没有取到数据,则调用委托的执行器对象去执行查询,这里的被委托对象可以是:
        // simpleExecutor、ReuseExecutor、BatchExecutor。默认是simpleExecutor执行器
        // 这里使用的是装饰设计模式
        if (list == null) {
          // 被委托的对象执行查询方法(这里被委托的对象是BaseExecutor执行器)
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

如果二级缓存没有值,则调用委托对象(BaseExecutor)的查询方法,查询一级缓存。

3.5、executor一级缓存查询逻辑

在上面的二级缓存没有查询到结果以后,会委托baseExecutor执行器去一级缓存查询。

// baseExecutor的query方法
@Override
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.");
    }
    //先清局部缓存,再查询.但仅查询堆栈为0,才清。为了处理递归调用
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      // 清除局部缓存
      clearLocalCache();
    }
    List<E> list;
    try {
      //加一,这样递归调用到上面的时候就不会再清局部缓存了
      queryStack++;
      // 【查询一级缓存】先根据cachekey从一级缓存localCache去查,一级缓存实质就是从一个Map中拿
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //若查到localCache缓存,处理localOutputParameterCache
        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();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        //如果是STATEMENT,清本地缓存
        clearLocalCache();
      }
    }
    return list;
  }

从一级缓存中get

public class PerpetualCache implements Cache {
    
  private final Map<Object, Object> cache = new HashMap<>();
    
  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }
}
3.6、一级缓存查询不到,从数据库查询

上面的从一级缓存中如果没有查询到结果,则直接调用BaseExecutor的子类执行器去数据库查询!

// BaseExecutor里的 queryFromDatabase
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 {
      // 执行doQuery()方法从数据库查询【这里执行的是BaseExecutor子类的doQuery(),默认是SimpleExecutor】
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //最后删除占位符
      localCache.removeObject(key);
    }
    //加入缓存
    localCache.putObject(key, list);
    //如果是存储过程,OUT参数也加入缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

在这里插入图片描述

3.7、SimpleExecutor.doQuery查询数据库
// SimpleExecutor里的doQuery()
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      // 这里就是基础的构建prepareStatement,执行查询操作了
      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);
    }
  }

执行查询(或其他操作)完毕后,然后再按照一定的条件或逻辑放入缓存或情况缓存!

Ⅲ、总结

上面分析的执行SQL的逻辑用简单的话描述:

①、首先通过 sqlSession.getMapper(MyBlogMapper.class)创建对应mapper接口的代理对象

②、调用mapper代理对象的方法执行查询(或其他操作)【其中最核心的就是调用sqlSession的API】

③、在执行mapper代理对象的方法中,执行查询的时候会首先查找二级缓存(如果开启了二级缓存),如果命中则返回缓存结果。

④、如果二级缓存没有命中,则调用委托执行器(BaseExecutor)的方法去查询一级缓存,如果命中则返回缓存结果

⑤、如果一级缓存没有命中,则调用BaseExecutor执行器的子类执行器(默认是 SimpleExecutor)的方法从数据库查询!

⑥、将查询到的结果按照一定的规则和条件该写缓存的写缓存,然后返回!

在这里插入图片描述

其他:

1、 一级缓存作用域

MyBatis中的一级缓存,是默认开启且无法关闭的一级缓存默认的作用域是一个SqlSession,解释一下,就是当SqlSession被构建了之后,缓存就存在了,只要这个SqlSession不关闭,这个缓存就会一直存在,换言之,只要SqlSession不关闭,那么这个SqlSession处理的同一条SQL就不会被调用两次,只有当会话结束了之后,这个缓存才会一并被释放。

2、 MyBatis 四大核心对象

ParameterHandler:处理SQL的参数对象

ResultSetHandler:处理SQL的返回结果集

StatementHandler:数据库的处理对象,用于执行SQL语句

Executor:MyBatis的执行器,用于执行增删改查操作

常见问题:

1、缓存失效条件?

2、为什么mybatis和spring进行整合的时候一级缓存会失效?

3、一级缓存的作用域?生命周期?

4、sqlSession生命周期?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值