Mybatis的整个底层流程其实可以分为两个大的部分:一个是配置文件加载解析的过程;另一个是方法执行的流程。前者是后者的基础,只有配置文件都加载好了,后面我们执行方法的时候,才能及时且方便的拿到我们需要拿到的信息。本文我们就从源码的角度来分析分析整个Mybatis的加载流程。文末会有彩蛋哦。
从SqlSessionFactory的构建说起
首先我们需要知道的是,Mybatis对外提供的一个主要接口就是SqlSession,当我们拿到了SqlSession就可以通过生产Mapper的代理对象,然后进行数据库操作了。但是SqlSession的获取是需要SqlSessionFactory来进行创建的,那我们需要怎样获取到SqlSessionFactory呢?在官方文档中,为我们提供了两种方式:一种是基于XML配置文件的方式来进行创建;另一种是基于java API的方式来进行创建,我们分别来看一看吧:
方式一:基于XML的方式生产SqlSessionFactory
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
其中mybatis-config.xml demo如下:
<?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>
<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="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
方式二:基于java代码的方式生成SqlSessionFactory
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
其实JavaAPI的方式是将xml配置文件中的信息封装成了Configuration而已,而且我们使用XML的方式在底层也是将XML文件中的信息封装到了Configuration中,最终我们操作的还是Configuration这个重要的配置类,这个配置类的生命周期贯穿整个Mybatis的生命周期,说它是最重要的类也不为过。
下面我们就以经典的XML配置的这种方式来进行讲述整个配置文件解析的全流程,其实java配置类的方式大差不大,会了XML方式的底层原理,那么Java API方式的底层原理你也就懂了。
Mybatis配置文件的解析
其实,这个解析流程就是对:properties、settings、typeAliases、plugins、objectFactory、objectWrapperFactory、reflectorFactory、environments、databaseIdProvider、typeHandlers、mappers这几个标签的解析,解析的入口在XMLConfigBuilder类中的parseConfiguration方法中进行,代码如下:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// mappers标签的解析
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
对于前面几个标签的解析都不是很难,我们就不多说了,可以自己看一看,而精彩的是最后一个对于mappers标签的解析,也就是对于我们写的Mapper接口进行解析,这也是下面我们需要详细分析的部分。
在mapperElement方法中,我们也能看到对官方文档中所说的四种配置映射器的方法的分别解析过程:
Mybatis中支持的四种映射器的配置方法:
1.使用相对于类路径的资源引用:
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
2.使用完全限定资源定位符(URL)
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
3.使用映射器接口实现类的完全限定类名
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
4.将包内的映射器接口实现全部注册为映射器
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
再来看看源码中对着四种方法的解析过程:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 类型一:将包内的映射器接口实现全部注册为映射器
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// 类型二:使用相对于类路径的资源引用
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// 类型三:使用完全限定资源定位符(URL)
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 类型四:使用映射器接口实现类的完全限定类名
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
虽然有四种配置方式,但这也仅仅是对外的四种方式,最终我们还是要拿到所有的映射器的Class对象,然后执行循环遍历执行mapperRegistry.addMapper(Class type)方法,该方法中主要干了两件事:
1. 为映射器Mapper创建了一个映射器代理工厂MapperProxyFactory,并存储起来,我们都知道mybatis底层是基于jdk动态代理的方式的,而这个代理工厂就是为了后面执行时生成MapperProxy的代理对象;
2. Mybatis开始解析对应的XxxMapper.xml文件。代码如下:
// 生成Mapper代理工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 在此解析XxxMapper.xml文件
parser.parse();
解析XxxMapper.xml其实也就是挨个的标签进行解析,并将其赋值给相应的Configuration的属性,而XxxMpper.xml文件中的select|insert|delete|update等这些我们写的sql逻辑地方的解析使我们需要重点掌握的。在Mybatis底层,每一个select|insert|delete|update里面的内容都会被封装为MappedStatement对象,而在MappedStatement中保存sql相关信息的属性是SqlSource,所以我们来看下SqlSource的创建逻辑(还是比较重要的),SqlSource的构建是借助于XMLScriptBuilder来进行XML解析的,解析的过程中会通过parseDynamicTags方法(代码如下):
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 检测TextSqlNode中是否有${variable}的占位符,有的话就是动态的
if (textSqlNode.isDynamic()) {
// SQL 语句中含有 ${} 占位符
contents.add(textSqlNode);
isDynamic = true;
} else {
// 纯 SQL 语句和 #{} 占位符,不包含任何动态 SQL 语句(包含 ${} 占位符)
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName); // 拿到结点处理器
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
// 是动态sql标签的话也是动态sql
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
从解析的代码中,我们会发现Mybatis会把整个select|insert|delete|update标签中的内容,解析为SqlNode的集合(也就是源码中的 List contents = new ArrayList<>()),封装在MixedSqlNode对象中,其中在Mybatis中,sqlNode的类型有如下几类:
- StaticTextSqlNode:纯SQL语句和#{}占位符,不包含任何动态SQL语句(包含${}占位符 )
- TextSqlNode: SQL语句中含有${}占位符;
- IfSqlNode:if/when子标签里面的SQL语句;
- ChooseSqlNode:choose子标签里面的SQL语句;
- ForEachSqlNode:foreach子标签里面的SQL语句;
- VarDecSqlNode:bind子标签里面的SQL语句;
- TrimSqlNode:trim子标签里面的SQL语句;
- WhereSqlNode:where子标签里面的SQL语句;
- SetSqlNode:set 子标签里面的 SQL 语句;
- MixedSqlNode: 如果insert/update/delete/select标签的SQL文本不止一行,则把所有的SqlNode组装在一起的SqlNode。
围绕sqlNode的解析,我们给出如下demo:
demo1:
sql:SELECT * from employee_info where employee_code = #{code}
MixedSqlNode:这个就仅仅解析为一个StaticTextSqlNode
demo2:
sql:
SELECT ei.department_id as departmentId, ei.department_name as departmentName FROM `crm_employee_info` AS ei where ei.department_id = ${req.departmentId} <if test=" req.departmentId != null "> AND ei.department_id = #{req.departmentId} </if> <if test=" req.employeeStatus != null "> AND ei.employee_status = #{req.employeeStatus} </if> GROUP BY ei.department_id, ei.department_name
MixedSqlNode:这时候解析的就有5个sqlNode分别是:
- TextSqlNode
- IfSqlNode
- StaticTextSqlNode(这是一个换行符)
- IfSqlNode
- StaticTextSqlNode
除此之外,我们还可以在parseDynamicTags方法中发现了,Mybatis所定义的动态sql的含义:
-
动态的sql:
- 包含${}的占位符
- 包含trim/where/set/foreach/if/choose/when/otherwise/bind 这些动态的标签的sql
-
静态sql:
- 包含#{}的占位符
- 纯静态的sql
好了,在解析完所有的sqlNode返回MixedSqlNode后,Mybatis会根据是否是动态sql而分别构建出DynamicSqlSource和RawSqlSource,两者的构建又有什么不同呢?
-
动态的SqlSource在此时的构建是比较简单的,直接保存了MixedSqlNode就好,处理的过程留到了后面执行的时候(逻辑在DynamicSqlSource#getBoundSql中);
-
而静态的SqlSource在构建时就做一些处理:
- 将#{}中的参数配置信息有序的解析封装成了ParameterMapping对象,并保存在parameterMappings的List集合中;
- 将#{}替换成了占位符"?";
- 最终封装成StaticSqlSource;
其实也就是说静态的sql在此时就已经解析好了;这也是动态sql和静态sql解析不同的地方,解析的时机不同。
到此配置文件的解析也就差不多了,整个解析的过程最重要就是MappedStatement的构建过程了,而MappedStatement的构建过程中最重要的就是SqlSource的构建过程,应当着重理解,这个的理解对后面的执行流程也是有很大相关性的,
最后,其实说的再多也不过一张图来的一目了然,希望能帮助你更好的理解Mybatis。