Mybatis运行流程源码解析

it2025-04-29  17

一、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的方式进入方法调用处!会发现进入了MapperProxy的 invoke()方法中!

// 代理以后,所有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)、代理对象核心方法

从上面进去以后会发现最终进入了 MapperMethod的 execute()方法,该方法就是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生命周期?

最新回复(0)