MyBatis 是轻量级的 Java 持久层中间件,彻底基于 JDBC 实现持久化的数据访问,支持以 xml 和注解的形式进行配置,能灵活、简单地进行 SQL 映射,也提供了比 JDBC 更丰富的结果集,应用程序能够从中选择对本身的数据更友好的结果集。本文将从一个简单的快速案例出发,为读者剖析 MyBatis 的总体架构与运行流程。本次分析中涉及到的代码和数据库表能够从 GitHub 上下载:mybatis-demo 。html
话很少说,如今开始🔛🔛🔛!java
MyBatis官网 给出了一个 MyBatis 快速入门案例,简单归纳下来就是以下步骤:node
建立学生表用于测试:git
CREATE TABLE `student` ( `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '学生ID', `name` varchar(20) DEFAULT NULL COMMENT '姓名', `sex` varchar(20) DEFAULT NULL COMMENT '性别', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
该表单对应的实体类以及包括增删改查方法的持久层接口可在 entity 包和 mapper 包查看,数据库链接和 MyBatis 的配置文件以及持久层接口的映射文件能够在 resource 包下查看。github
测试类以下:sql
public class StudentTest { private InputStream in; private SqlSession sqlSession; @Before public void init() throws IOException { // 读取MyBatis的配置文件 in = Resources.getResourceAsStream("mybatis-config.xml"); // 建立SqlSessionFactory的构建者对象 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); // 使用builder建立SqlSessionFactory对象 SqlSessionFactory factory = builder.build(in); // 使用factory建立sqlSession对象并设置自动提交事务 sqlSession = factory.openSession(true); } @Test public void test() { // 使用sqlSession建立StudentMapper接口的代理对象 StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); // 使用代理对象执行相关方法 System.out.println(studentMapper.getStudentById(2)); studentMapper.updateStudentName("托尼·李四", 2); System.out.println(studentMapper.getStudentById(2)); System.out.println(studentMapper.findAll()); } @After public void close() throws IOException { // 关闭资源 sqlSession.close(); in.close(); } }
测试类运行结果以下:数据库
能够看到测试类成功执行了相应方法,这样就完成了 MyBatis 的快速案例实现。要注意的是,在上面的案例中咱们采用的是为持久层接口编写相应 xml 映射文件的方法,其部分配置以下所示:apache
<select id="getStudentById" parameterType="int" resultType="com.chiaki.entity.Student"> SELECT id,name,sex FROM student WHERE id = #{id} </select>
此外,在 MyBatis 中还提供了基于 Java 注解的方式,即在持久层接口的方法前使用对应的注解,以下所示:缓存
@Select("SELECT id,name,sex FROM student WHERE id = #{id}") Student getStudentById(int id);
两种方法各有优劣。基于注解的方法减小了配置文件,使代码更加简洁,可是在面对复杂 SQL 时候会显得力不从心;基于配置文件的方法虽然须要编写配置文件,但其处理复杂 SQL 语句的能力更强,实现了 Java 代码与 SQL 语句的分离,更容易维护。在笔者看来, Mapper.xml 配置文件就像是 MyBatis 的灵魂,少了它就没那味儿了,😄😄😄。不过到底采用哪一种方式来配置映射,读者能够根据实际业务来灵活选择。session
固然上述关于 MyBatis 的使用方式都离不开经过代码手动注入配置,包括建立 SqlSessionFactory、SqlSession等对象的步骤。此外,也能够采用将 MyBatis 与 Spring 等容器集成的方式来进行使用,这也是目前很是受欢迎的方式,因为本文主要是介绍 MyBatis 的偏底层的原理,所以这里不作详细介绍。
在上一小节中咱们进行了 MyBatis 的快速实现,也看到了 Resources 、 SqlSessionFactory 以及 SqlSession 等 MyBatis 框架中的一些类,那么 MyBatis 的系统架构究竟是什么样的呢?咱们经过结合 MyBatis 的源码项目结构获得下面的 MyBatis 总体框架图:
能够看出,在 MyBatis 源码中基本上是每个 package 对应一个模块,模块之间互相配合确保 MyBatis 的成功运行。下面分层介绍 MyBatis 的总体架构。
模块名称 | 关联package | 做用 |
---|---|---|
数据源模块 | datasource | 数据源及数据工厂的代码。 |
事务管理模块 | transaction | 事务支持代码。 |
缓存模块 | cache | 缓存实现代码。 MyBatis 提供以及缓存与二级缓存。 |
Binding模块 | binding | 映射绑定。将用户自定义的 Mapper 接口与映射配置文件关联。 |
反射模块 | reflection | 反射是框架的灵魂。 MyBatis 对原生的反射进行了良好的封装,实现了更简洁的调用。 |
类型转换 | type | 类型处理。包含了类型处理器接口 TypeHandler 、父类 BaseTypeHandler 以及若干子类。 |
日志模块 | logging | 提供日志输出信息,而且可以集成 log4j 等第三方日志框架。 |
资源加载 | io | 对类加载器进行封装,肯定类加载器的使用顺序,并提供加载资源文件的功能。 |
解析器模块 | parsing | 一是对 XPath 进行封装,二是为处理动态 SQL 中的占位符提供支持。 |
模块名称 | 关联package | 做用 |
---|---|---|
配置解析 | builder | 解析 Mybatis 的配置文件和映射文件,包括 xml 和 annotation 两种形式的配置。 |
参数映射 | mapping | 主要是 ParameterMap ,支持对输入参数的判断、组装等。 |
SQL解析 | scripting | 根据传入参数解析映射文件中定义的动态 SQL 节点,处理占位符并绑定传入参数造成可执行的 SQL 语句。 |
SQL执行 | executor | 在 SQL 解析完成以后执行SQL语句德奥结果并返回。 |
结果集映射 | mapping | 主要是 ResultMap ,与 ParameterMap 相似。 |
插件 | plugin | 能够经过添加自定义插件的方式对 MyBatis 进行扩展。 |
接口层对应的 package 主要是 session ,其中的核心是 SqlSession 接口,该接口定义了 MyBatis 暴露给用户调用的一些 API ,包括了 Select() 、 update() 、 insert() 、 delete() 等方法。当接口层收到调用请求时就会调用核心处理层的模块来完成具体的数据库操做。
本节中首先结合快速入门案例与 MyBatis 的总体架构来梳理其运行流程结构,以下图所示:
能够说, MyBatis 的整个运行流程结构,牢牢围绕着配置文件 MyBatis-config.xml 与 SQL 映射文件 Mapper.xml 文件展开。首先 SqlSessionFactory 会话工厂会经过 io 包下的 Resources 资源信息加载对象获取 MyBatis-config.xml 配置文件信息,而后产生能够与数据库进行交互的 SqlSession 会话实例类。会话实例 SqlSession 能够根据 Mapper.xml 配置文件中的 SQL 配置,去执行相应的增删改查操做。而在 SqlSession 类中,是经过执行器 Executor 对数据库进行操做。执行器与数据库交互依靠的是底层封装对象 Mapped Statement ,其封装了从 Mapper 文件中读取的包括 SQL 语句、输入参数类型、输出结果类型的信息。经过执行器 Executor 与 Mapped Statement 的结合, MyBatis 就实现了与数据库进行交互的功能。
本小节以一条具体的 SQL 语句为例,来分析 MyBatis 的执行过程,测试方法以下所示,其对应的语句是根据主键 ID 查询学生信息,测试方法运行先后的执行动做参见第 1 小节中 @Before 与 @After 注解下的方法,此处省略。
@Test public void testSqlExecute() { StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = studentMapper.getStudentById(2); System.out.println(student); }
首先,经过 io 包下的 Resources 类加载配置文件,将 Mapper .xml 文件转换为输入流,具体源码能够参考 org.apache.ibatis.session.io.Resources 类,以下所示。同时在 Resources 类中, MyBatis 还提供了其它的一些文件读取方法,方便用户使用。
public static InputStream getResourceAsStream(ClassLoader loader, String resource)throws IOException { InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader); if (in == null) { throw new IOException("Could not find resource " + resource); } return in; }
在获得配置文件的输入流以后, MyBatis 会调用 org.apache.ibatis.session.SqlSessionFactory 类中的 build() 方法建立 SqlSessionFactory 会话工厂。经过查看源码能够发如今 SqlSessionFactory 类中重载了不少 build() 方法,这里主要介绍下面三个方法:
// 方法一 public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } // 方法二 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 建立XMLConfigBuilder类型的对象用于解析配置文件 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 调用parse()方法生成Configuration对象并调用build()方法返回SqlSessionFactory对象 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } // 方法三 public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
从案例中咱们看到在建立会话工厂时调用方法一,即 build(InputStream inputStream) 方法,在该方法中其实调用了方法二,只不过将 environment 和 propertoes 参数置为 null 。咱们重点看方法二,该方法中涉及到 org.apache.ibatis.builder.xml 包的 XMLConfigBuilder 类,该类继承自 BaseBuilder 类,并初始化了一个用于解析配置文件的对象 parser , 而后在 return 语句中调用的是方法三,看到这里咱们确定发现方法三中 build() 方法的参数 parser.parse() 确定是 Configuration 类型。在建立会话工厂的步骤中, Configuration 的解析过程是一个关键的流程,下面咱们会逆向探究 Configuration 的详细解析过程。
先看看这个 XMLConfigBuilder 类型的 parser 对象下的 parse() 方法,探究这个方法是如何生成 Configuration 类型的对象的。 parse() 方法定义在 org.apache.ibatis.session.builder.XMLConfigBuilder 类中,该方法的源代码以及相应注释以下所示,能够看出真正重要的是 parseConfiguration() 方法。
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; // 先调用parser的evalNode()方法获取 "/configuration"下的节点 // 而后调用parseConfiguration()方法解析节点的信息并返回Configuration对象 parseConfiguration(parser.evalNode("/configuration")); return configuration; }
直接查看 XMLConfigBuilder#parseConfiguration() 方法的源码以下所示:
// 解析配置文件的各个节点并将其设置到configuration对象 private void parseConfiguration(XNode root) { try { // 1.处理properties节点 propertiesElement(root.evalNode("properties")); // 2.处理settings节点 Properties settings = settingsAsProperties(root.evalNode("settings")); // 3.加载自定义的VFS设置 loadCustomVfs(settings); // 4.加载自定义的日志实现设置 loadCustomLogImpl(settings); // 5.处理typeAliases节点 typeAliasesElement(root.evalNode("typeAliases")); // 6.处理plugins节点 pluginElement(root.evalNode("plugins")); // 7.处理objectFactory节点 objectFactoryElement(root.evalNode("objectFactory")); // 8.处理objectWrapperFactory节点 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 9.处理reflectorFactory节点 reflectorFactoryElement(root.evalNode("reflectorFactory")); // 10.处理settings节点 settingsElement(settings); // 11.处理environments节点 environmentsElement(root.evalNode("environments")); // 12.处理databaseIdProvider节点 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 13.处理typeHandlers节点 typeHandlerElement(root.evalNode("typeHandlers")); // 14.处理mappers节点 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
要注意的是 parseConfiguration() 方法在处理配置文件的节点后会把相应配置写入到该类的成员变量 configuration 中而后返回。咱们以处理 mappers 节点的 mapperElement() 方法为例来进行说明,对其它主配置文件节点的解析方法读者能够自行参照源码阅读和理解。
在 mappers 节点下主要是 mapper 的配置方式,是 MyBatis 中重要的一部分。首先要明确在 MyBatis 配置文件的 mappers 节点下配置 mapper 的四种方式:
<mappers> <!-- 1.使用相对于类路径的资源引用 --> <mapper resource="mapper/StudentMapper.xml"/> <!-- 2.使用彻底限定资源定位符(URL) --> <mapper url="file:src/main/resources/mapper/StudentMapper.xml"/> <!-- 3.使用映射器接口实现类的彻底限定类名--> <mapper class="com.chiaki.mapper.StudentMapper"/> <!-- 4.使用包内的映射器接口实现所有注册为映射器 --> <package name="com.chiaki.mapper"/> </mappers>
下面咱们经过 MyBatis 的源码来看看 mappers 节点是如何被解析的,在 XMLConfigBuilder 类中找到 mapperElement() 方法,以下所示:
private void mapperElement(XNode parent) throws Exception { if (parent != null) { // 遍历子节点 for (XNode child : parent.getChildren()) { // 子节点是package,也就是上面配置mapper的第四种方式 if ("package".equals(child.getName())) { // 获取package的路径 String mapperPackage = child.getStringAttribute("name"); // 向Configuration的类成员遍历MapperRegistry添加mapper接口 // addMappers()方法位于Configuration类中 configuration.addMappers(mapperPackage); } else { // 永远先执行else语句,由于dtd文件声明mappers节点下mapper子节点必须在package子节点前面 // 获取mapper节点中的resource、url以及class属性 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // 只有resource属性,也就是上面配置mapper的第一种方式 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); try(InputStream inputStream = Resources.getResourceAsStream(resource)) { // 生成XMLMapperBuilder类型的mapperParser对象,即mapper解析器 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 调用解析器的parse方法进行解析 mapperParser.parse(); } } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); try(InputStream inputStream = Resources.getUrlAsStream(url)){ // 只有url属性,也就是上面配置mapper的第二种方式 // 仍然是生成XMLMapperBuilder类型的mapper解析器 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); // 调用parse()方法 mapperParser.parse(); } } else if (resource == null && url == null && mapperClass != null) { // 只有class属性,也就是上面配置的第三种方式 // 经过反射获取mapper接口类 Class<?> mapperInterface = Resources.classForName(mapperClass); // 调用addMapper()方法 configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
这段代码中对应着 mappers 节点配置的四种状况:
方式一和方式二指定了 Mapper 接口与 xml 配置文件,方式三和方式四指定了 Mapper 接口。
方式一和方式二都涉及到构造 XMLMapperBuilder ,该类位于 org.apache.ibatis.builder.xml 包下,一样继承自 BaseBuilder 类。同时以上两种方式都涉及到 XMLMapperBuilder类下的一个 parse() 方法,要注意与 XMLConfigBuilder 类中的 parse() 方法进行对比区分理解。显然, XMLConfigBuilder 负责解析 MyBatis 的配置文件,而 XMLMapperBuilder 负责解析 Mapper.xml 文件。找到 XMLMapperBuilder#parse() 方法,其源码以下:
public void parse() { // Configuration类中定义了Set<String> loadedResources表示已加载的mapper.xml文件 // 判断是否已加载过该mapper.xml文件 if (!configuration.isResourceLoaded(resource)) { // 解析文件中的各类配置 configurationElement(parser.evalNode("/mapper")); // 解析完毕后将该文件添加到loadedResources中 configuration.addLoadedResource(resource); // 为接口的全限定类名绑定相应的Mapper代理 bindMapperForNamespace(); } // 移除Configuration中解析失败的resultMap节点 parsePendingResultMaps(); // 移除Configuration中解析失败的cache-ref节点 parsePendingCacheRefs(); // 移除Configuration中解析失败的statement parsePendingStatements(); }
在 parse() 方法中涉及到的 configurationElement() 方法是一个核心方法,其源码以下:
private void configurationElement(XNode context) { try { // 获取全限定类名 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } // 设置全限定类名 builderAssistant.setCurrentNamespace(namespace); // 解析cache-ref节点 cacheRefElement(context.evalNode("cache-ref")); // 解析cache节点 cacheElement(context.evalNode("cache")); // 解析parameterMap parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析resultMap resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析sql节点 sqlElement(context.evalNodes("/mapper/sql")); // 解析select|insert|update|delete节点 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } }
该方法解析了 mapper 节点中全部子标签,最终经过 buildStatementFromContext() 方法解析具体 SQL 语句并生成 MappedStatement 对象。
进一步找到 XMLMapperBuilder#buildStatementFromContext() 方法,该方法进行了重载,功能是遍历全部标签,而后建立一个 XMLStatementBuilder 类型的对象对表示实际 SQL 语句的标签进行解析,重点调用的是 parseStatementNode() 方法,源码以下所示:
private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); } private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { // 建立XMLStatementBuilder类型的statementParse用于对select|insert|update|delete节点进行解析 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { // 调用parseStatementNode()方法解析 statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
找到 parseStatementNode() 方法,其位于 org.apache.ibatis.builder.xml.XMLStatementBuilder 类下,源码以下:
public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } // 解析标签属性 String nodeName = context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // 将include标签内容替换为sql标签订义的sql片断 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // 获取Mapper返回结果类型的Class对象 String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); // 获取LanguageDriver对象 String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); // 解析selectKey标签 processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); // 获取主键生成策略 if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } // 经过LanguageDriver对象解析SQL内容生成SqlSource对象 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); // 默认Statement的类型为PreparedStatament StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); // 解析并获取select|update|delete|insert标签属性 Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String resultType = context.getStringAttribute("resultType"); Class<?> resultTypeClass = resolveClass(resultType); String resultMap = context.getStringAttribute("resultMap"); String resultSetType = context.getStringAttribute("resultSetType"); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); if (resultSetTypeEnum == null) { resultSetTypeEnum = configuration.getDefaultResultSetType(); } String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); String resultSets = context.getStringAttribute("resultSets"); // 调用addMappedStatement()将解析内容组装生成MappedStatement对象并注册到Configuration builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
在上面的源码中会解析 select|update|delete|insert 标签的属性,而后重点是用 LanguageDriver 对象来解析 SQL 生成 SqlSource 对象。 org.apache.ibatis.scripting.LanguageDriver 类是一个接口,对应的实现类有 XMLLanguageDriver 和 RawLanguageDriver ,同时涉及到 XMLScriptBuilder 类与 SqlSourceBuilder 类等。关于 LanguageDriver 对象解析 SQL 的详细过程,读者能够按部就班去阅读 MyBatis 的源码,这里限于篇幅就不作详细介绍了。最后会调用 org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement() 方法将解析内容组装成 MappedStatement 对象并将其注册到 Configuration 的 mappedStatements 属性中。至此, Mapper 接口对应的 xml 文件配置就解析完成了。下面咱们再回到 XMLMapperBuilder#parse() 方法看看 Mapper 是如何注册接口的。
在 XMLMapperBuilder#parse() 中经过 XMLMapperBuilder#configurationElement() 方法解析完 xml 文件配置后会将其添加到已加载的资源 loadedResources 中,而后会调用 XMLMapperBuilder#bindMapperForNameSpace() 方法为接口的全限定类名绑定 Mapper 代理,即为 Mapper 接口建立对应的代理类,找到相应源码以下:
private void bindMapperForNamespace() { String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { // ignore, bound type is not required } if (boundType != null && !configuration.hasMapper(boundType)) { // 调用Configuration#hasMapper()方法判断当前Mapper接口是否已经注册 configuration.addLoadedResource("namespace:" + namespace); // 调用Configuration#addMapper()注册接口 configuration.addMapper(boundType); } } }
在上面的代码中先调用 Configuration#hasMapper() 方法判断当前 Mapper 接口是否已经注册,只有没被注册过的接口会调用 Configuration#addMapper() 方法类注册接口。
在 Configuration 类中, 找到 addMapper() 方法发现其调用的是 MapperRegistry#addMapper() 方法。
// Configuration#addMapper()方法 public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); }
找到 MapperRegistry#addMapper() 方法对应的源码以下:
// Configuration#addMapper()方法 public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); } // MapperRegistry#addMapper()方法 public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 调用Configuration#knownMappers属性的put方法 knownMappers.put(type, new MapperProxyFactory<>(type)); // 建立MapperAnnotationBuilder对象parser MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 调用MapperAnnotationBuilder#parse()方法 parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
在 MapperRegistry#addMapper() 方法中,首先会调用 Configuration 类下 knownMappers 属性的 put() 方法,能够看到 key 值为 Mapper 接口对应的 Class 对象,而 value 值为 Mapper 接口对应的 Class 类型的代理工厂类 MapperProxyFactory 。这里 MapperProxyFactory 会根据 sqlSeesion 建立 Mapper 接口的一个 MapperProxy 代理实例,具体的分析咱们将在后续小节解读。
在 Mapper 接口注册以后,继续往下能够看到建立了一个 MapperAnnotationBuilder 类型的对象 parser ,而后调用 MapperAnnotationBuilder#parse() 方法进行解析,咱们找到 MapperAnnotationBuilder#parse() 的源码以下:
public void parse() { String resource = type.toString(); // 判断是否被加载过 if (!configuration.isResourceLoaded(resource)) { // 若是没有被加载则对资源进行加载 loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); // 解析缓存 parseCache(); parseCacheRef(); for (Method method : type.getMethods()) { if (!canHaveStatement(method)) { continue; } // 解析Mapper接口的使用SQL注解的方法,好比@Select以及@SelectProvider if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent() && method.getAnnotation(ResultMap.class) == null) { parseResultMap(method); } try { // 调用parseStatement()方法 parseStatement(method); } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
经过阅读源码能够知道 MapperAnnotationBuilder#parse() 方法会对接口上的 SQL 注解进行解析,解析完成后生成对应的 MappedStatement 对象并注册到 Configuration 的 mappedStatements 属性中,这里在后面展开详细解析。
至此咱们已经梳理清楚了在指定 xml 文件时 Mapper 的解析与加载流程。回过头看,咱们从 XMLMapperBuilder#parse() 方法开始层层递进,犹如抽丝剥茧通常,让人以为酣畅淋漓,也在这里作一个小结。
看完了方式一和方式二的解析与加载流程以后,咱们继续回到 XMLConfigBuilder#mapperElement() 方法探究方式三和方式四中指定 Mapper 接口时的 Mapper 解析与加载流程。方式三和方式四涉及到调用 configuration 对象的 addMappers() 和 addMapper() 方法。咱们找到这两个方法,发现其都位于 org.apache.ibatis.seesion 包的 Configuration 类中,其源码以下:
public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); } public void addMappers(String packageName) { mapperRegistry.addMappers(packageName); }
看到这是否是以为十分熟悉?没错,其实 addMappers() 和 addMapper() 方法的底层都是调用 MapperRegistry#addMapper() 方法实现 Mapper 接口的注册,这个方法咱们已经在上文中详细介绍过了。是否是感受少了什么流程?确实,读者可能疑惑的是:在上文中提到的指定 xml 文件时的解析和加载流程中,会先有不少解析 xml 文件的步骤而后才到 MapperRegistry#addMapper() 方法实现 Mapper 接口的注册,而在如今这种指定 Mapper 接口时的流程中一开始就调用 MapperRegistry#addMapper() 方法,那这种状况是否是就不解析 xml 了呢?说到这,就不得不提 MapperRegistry#addMapper() 方法中建立的 MapperAnnotationBuilder 对象了,上文中咱们提到该对象用于解析 Mapper 接口的 SQL 注解并生成对应 MappedStatement 对象并将其注册到 Configuration 的 mappedStatements 属性中。其实方式三和方式四的重点就是对指定 Mapper 接口上的注解进行解析的,而咱们知道 MyBatis 的基于注解的配置方式最大的优势就是没有 xml 配置文件,连 xml 配置文件都没有的话天然就没有 xml 文件相关的解析流程啦!不过,若是指定了 xml 文件,仍会使用 XMLMapperBuilder 来解析 xml 文件。
如今再看看 MapperAnnotationBuilder#parse() 的源码,以下所示:
public void parse() { String resource = type.toString(); // 判断是否被加载过 if (!configuration.isResourceLoaded(resource)) { // 若是没有被加载则对资源进行加载 loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); // 解析缓存 parseCache(); parseCacheRef(); for (Method method : type.getMethods()) { if (!canHaveStatement(method)) { continue; } // 解析Mapper接口的使用SQL注解的方法,好比@Select以及@SelectProvider if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent() && method.getAnnotation(ResultMap.class) == null) { parseResultMap(method); } try { // 调用parseStatement()方法解析SQL注解 parseStatement(method); } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
阅读源码咱们发现关键的方法,即 MapperAnnotationBuilder#parseStatement() 方法,该方法是解析 SQL 注解的入口方法。
在 org.apache.ibatis.builder.annotation 找到 MapperAnnotationBuilder#parseStatement() 方法的源码,以下所示:
// 四个类成员变量 // 注解对应Class对象组成的set集合 // 包括@Select、@Insert、@Update、@Delete、@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider注解 private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream .of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class, InsertProvider.class, DeleteProvider.class) .collect(Collectors.toSet()); // 核心配置对象Configuration private final Configuration configuration; // Mapper构建工具 private final MapperBuilderAssistant assistant; // 要解析的Mapper接口的Class对象 private final Class<?> type; // parseStatement()方法,入参为Mapper中的方法 void parseStatement(Method method) { // 获取输入参数类型的Class对象 final Class<?> parameterTypeClass = getParameterType(method); // 获取LanguageDriver对象 final LanguageDriver languageDriver = getLanguageDriver(method); // 流方法中的ifPresent()方法,包含lambda表达式 getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> { // 获取SqlSource final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method); // 经过注解获取SQL命令类型 final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType(); // 获取方法上的@Options注解 final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options)x.getAnnotation()).orElse(null); // 映射语句id设置为类的全限定名.方法名 final String mappedStatementId = type.getName() + "." + method.getName(); // 键生成器 final KeyGenerator keyGenerator; String keyProperty = null; String keyColumn = null; // 若是是insert或者update,只有insert或者update才解析@SelectKey注解 if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) { // 首先检查@SelectKey注解,它会覆盖其它任何配置 // 获取方法上的@SelectKey注解 SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey)x.getAnnotation()).orElse(null); // 若是存在@SelectKey注解 if (selectKey != null) { keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } else { keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; keyProperty = options.keyProperty(); keyColumn = options.keyColumn(); } } else { // 其它SQL命令没有键生成器 keyGenerator = NoKeyGenerator.INSTANCE; } Integer fetchSize = null; Integer timeout = null; StatementType statementType = StatementType.PREPARED; ResultSetType resultSetType = configuration.getDefaultResultSetType(); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = !isSelect; boolean useCache = isSelect; if (options != null) { // 省略 } String resultMapId = null; if (isSelect) { // 若是是查询,获取@ResultMap注解 ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class); if (resultMapAnnotation != null) { // @ResultMap注解不为空则解析@ResultMap注解 resultMapId = String.join(",", resultMapAnnotation.value()); } else { resultMapId = generateResultMapName(method); } } // 调用addMappedStatement()将解析内容组装生成MappedStatement对象并注册到Configuration assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout, null, parameterTypeClass, resultMapId, getReturnType(method), resultSetType, flushCache, useCache, false, keyGenerator, keyProperty, keyColumn, statementAnnotation.getDatabaseId(), languageDriver, options != null ? nullOrEmpty(options.resultSets()) : null);}); }
经过阅读这段源码,咱们发现 parseStatement() 方法中关于注解的解析过程与 XMLStatementBuilder#parseStatementNode() 方法中对 xml 文件的解析有些许类似之处。在对 xml 解析时是获取对应标签而后解析,而对注解解析时是获取方法上的注解而后进行解析,解析完成后两者都是调用 MapperBuilderAssistant#addMappedStatement() 方法组装解析内容生成 MappedStatement 对象并注册到 Configuration 中。
在指定 Mapper 接口的状况下,咱们分析了 Mapper 的解析与加载流程。在这种状况下主要是从 MapperAnnotationBuilder#parse() 方法入手,调用 MapperAnnotationBuilder#parseStatement() 方法对 Mapper 接口上的注解进行解析,而后将解析内容组装并生成 MappedStatement 对象并注册到 Configuration 对象的 mappedStatements 属性中。这里要注意的是,指定 Mapper 接口这种方式通常没有指定 xml 文件,这样就只会对注解进行解析,当指定 xml 文件后仍会按上小节中的步骤对 xml 文件进行解析。同理,指定 xml 文件的方式通常也没有注解,所以也只会解析 xml 文件,当存在注解时也一样会对注解进行解析。
在上一小节中,咱们花了很大的篇幅如剥洋葱通常一层一层地理清了建立会话工厂中 Configuration 对象的解析流程,读者是否感受对 MyBatis 的源码阅读渐入佳境了呢?下面介绍经过会话工厂 SqlSessionFactory 建立会话 SqlSession 的步骤, SqlSession 是 MyBatis 暴露给外部使用的统一接口层。
经过案例能够看到调用 SqlSessionFactory#openSession() 方法能够建立 SqlSession 对象,找到对应源码以下:
public interface SqlSessionFactory { SqlSession openSession(); SqlSession openSession(boolean autoCommit); SqlSession openSession(Connection connection); SqlSession openSession(TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType); SqlSession openSession(ExecutorType execType, boolean autoCommit); SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType, Connection connection); Configuration getConfiguration(); }
SqlSessionFactory 是一个接口,其中重载了不少 openSession() 方法,同时还包括一个获取 Configuration 对象的 getConfiguration() 方法。
对于 SqlSessionFactory 接口,其对应的默认实现类是 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory 类,在该类中找到了对应的 openSession() 方法的实现,其底层调用的是 DefaultSqlSessionFactory#openSessionFromDataSource() 方法来获取 SqlSession 对象,对应源码以下所示:
@Override public SqlSession openSession(boolean autoCommit) { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit); } private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { // jdbc事务管理器 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); // 获取SqlSession会话对象,其中org.apache.ibatis.session.defaults.DefaultSqlSession是SqlSession的默认实现类 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
经过下面的时序图能够更好地理解建立 SqlSession 的过程:
在案例中,建立了 SqlSession 对象后会调用 getMapper() 方法建立 Mapper 接口的代理实例,下面咱们先看调用该方法的时序图,以下所示:
由时序图咱们能够得知真正建立 MapperProxy 代理实例涉及到的核心类是 MapperProxyFactoy 类和 MapperProxy 类,这两个类咱们在上文中提到过,这里咱们详细阅读相关源码。
在 org.apache.ibatis.binding 包找到 MapperProxyFactory 类,其源码以下所示。
public class MapperProxyFactory<T> { // Mapper接口 private final Class<T> mapperInterface; // Mapper接口中的方法和方法封装类的映射 private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethodInvoker> getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") // newInstance()方法一:代理模式,建立一个MapperProxy protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } // newInstance()方法二:根据SqlSession为Mapper接口建立一个MapperProxy代理实例 public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
一看这个类的名字就知道这是个工厂类,目的就是为了生成 MapperProxy 。该类中由两个 newInstance() 方法,第二个 newInstance() 方法中结合经过 SqlSession 类型的参数生成了一个 MapperProxy 代理实例,而后调用第一个 newInstance() 方法返回。在第一个方法中使用 Java 的 Proxy 类生成了一个 Mapper 接口的代理类,采用的是动态代理的方式。
紧接着找到 MapperProxy 类的部分源码以下。能够看到 MapperProxy 类实现了 InvocationHandler 接口并实现了其中的 invoke() 方法,这就是由于动态代理。
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } // 省略...... @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { 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); } } // 省略...... }
在建立了 Mapper 接口的代理对象以后,代理对象又是怎么执行相应的方法的呢?咱们在 3.2 节开头展现案例中根据ID查找学生的语句处打上断点进行调试,如图所示。
点击 step into 按钮会进入 org.apache.ibatis.binding.MapperProxy#invoke() 方法,以下图所示。
在执行 invoke() 方法后会调用 cacheMapperMethod() 方获取 MapperMethod 对象。在 MapperProxy 类中找到 cacheMapperMethod() 方法,源码以下:
private MapperMethod cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); }
在上述代码中经过 new MapperMethod() 方法建立 MapperMethod , 其中 mapperInterface 就是 com.jd.yip.mapper.StudentMapper 接口, method 就是 cacheMapperMethod() 方法传入的 Method 类型的参数,即 getStudentById() 方法,而 sqlSession.getConfiguration() 获取的就是 Configuration 配置对象。在获取到 MapperMethod 后,会执行 mapperMethod.execute(sqlSession, args) 方法返回。该方法位于 org.apache.ibatis.binding 包下的 MapperMethod 类中,源码以下所示,首先会获取 SQL 语句的类型,而后进入 switch-case 结构。
public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 获取SQL命令类型进入switch-case 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 { // 将参数转换成SQL语句的参数 Object param = method.convertArgsToSqlCommandParam(args); // 调用SqlSession#selectOne()方法获得结果 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; }
案例中根据 ID 查询学生信息属于 SELECT ,所以进入对应的 case 分支判断当前方法的返回状况,案例的状况会直接进入最后的 else 语句,先将参数转化成 SQL 语句所需参数,而后进入到 SqlSession 的默认实现类 DefaultSqlSession 中调用 selectOne() 方法,以下图所示。
进入 DefaultSqlSession#selectOne() 方法后会继续调用当前类的 selectList() 方法,如图所示。
继续 step into 进入到 selectList() 方法,能够看到该方法有多种重载形式,其中最关键的是调用 Executor#query() 方法获取到查询结果。
至此,咱们经过调试案例代码,就理清了 Mpaaer 接口代理对象执行相关方法的流程,现对上述流程进行小结。
本节中以根据学生 ID 查找学生信息这样一条 SQL 语句为案例,结合 MyBatis 的源码梳理了 SQL 语句的具体执行流程:
本文从 MyBatis 的简单快速案例出发介绍了 MyBatis 的总体架构,而后介绍了 MyBatis 的运行流程结构,进一步以一条实际的 SQL 语句为案例从源码角度剖析了 SQL 语句的详细执行流程,其中重点在于 Mapper 的解析与加载以及 Mapper 接口代理对象的建立,最后对 MyBatis 的运行流程作了必定的总结。在阅读源码的过程当中不难发现 MyBatis 运行时对方法的调用是一层套一层,这时候就须要读者耐心地从入口函数开始层层深刻,如升级打怪通常,到最后理清整个流程后你能够得到的就是如游戏通关般的畅快感。固然,因为笔者水平有限,本文只是管中窥豹,只可见一斑而不能得全貌,读者能够跟着文章的解读思路自行探索直到窥得 MyBatis 全貌。
MyBatis 官网:https://mybatis.org/mybatis-3/
MyBatis 源码仓库:https://github.com/mybatis/mybatis-3
《互联网轻量级 SSM 框架解密:Spring 、 Spring MVC 、 MyBatis 源码深度剖析》
《MyBatis3 源码深度解析》
《Spring MVC + MyBatis 开发从入门到实践》