MyBatis源码解析
如何编译源码
1、idea中打开项目(推荐在导入idea之前,在命令行先将项目编译通过,命令mvn clean compile
)
2、等待解析依赖
备注:解析依赖的过程中有可能某些jar包的版本找不到,可自行在https://mvnrepository.com/ 找寻替代版本
运行流程
XML解析
mybatis3支持注解方式声明SQL语句,但是最终经过mybatis内部解析,作用和使用XML定义是一样的。
只要你理解了XML的整个解析流程,其实就已经吃透了mybatis。
在阅读源码的过程中,mybatis
的中文官网必须时刻去翻阅。特别是如下几章内容:
XML配置讲解的是mybatis-config.xml
(配置数据库地址、插件、类型别名)
XML映射器讲解的是我们自定义业务SQL语句的***Mapper.xml
如何开始阅读源码呢?当然不可能一个一个包一个一个类去翻阅,这样是没有意义的。我们必须找到一个入口,也就是写一个最简单的例子跑完整个流程。然后使用idea跟踪这个流程的每一步。
可以参照入门搭建一个最小的示例。
入口示例
我参照入门构造了一个示例,目录结构如下:
在mybatis-config.xml
中,配置了数据库地址、以及业务mapper xml
文件 (projectInfo.xml)
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>
<plugins>
<plugin interceptor="com.twx.mybatis.interceptor.InterceptorDemo1" />
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/dashboard_heart_beat"/>
<property name="username" value="twx"/>
<property name="password" value="soyuan123"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/twx/mybatis/mapper/xml/projectInfo.xml"/>
</mappers>
</configuration>
projectInfo.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="com.twx.mybatis.mapper.ProjectInfoMapper">
<resultMap id="projectInfoMap" type="com.twx.mybatis.entity.ProjectInfo">
<id column="id" property="id" jdbcType="INTEGER"></id>
<result column="project_name" property="projectName" jdbcType="VARCHAR"></result>
<result column="url" property="url" jdbcType="VARCHAR"></result>
<result column="description" property="description" jdbcType="VARCHAR"></result>
<result column="tags" property="tags" jdbcType="VARCHAR"></result>
<result column="username" property="username" jdbcType="VARCHAR" />
<result column="password" property="password" jdbcType="VARCHAR" />
<result column="thumbnail_url" property="thumbnailUrl" jdbcType="VARCHAR" />
<result column="second_category_id" property="secondCategoryId" jdbcType="INTEGER" />
<result column="project_sort" property="projectSort" jdbcType="INTEGER" />
<result column="maintainer" property="maintainer" jdbcType="VARCHAR" />
<result column="status" property="status" jdbcType="BOOLEAN" />
<result column="back_service" property="backService" jdbcType="BOOLEAN" />
<result column="edit" property="edit" jdbcType="BOOLEAN" />
<result column="create_time" property="createTime" jdbcType="DATE" />
<result column="update_time" property="updateTime" jdbcType="DATE" />
</resultMap>
<sql id="info_columns">
p.id, p.project_name,p.url,p.description,p.tags,
p.username,p.`password`,p.thumbnail_url,p.second_category_id,
p.project_sort,p.maintainer,p.`status`,p.back_service,p.create_time
</sql>
<select id="getProjectInfo" resultMap="projectInfoMap">
select
<include refid="info_columns"></include>
from project_info p
LEFT JOIN second_category sc on sc.id = p.second_category_id
INNER JOIN primary_category pc on pc.id = sc.p_id
where p.logic_delete = 0
<if test="fId != null">
and pc.id = #{fId}
<if test="sId !=null">
and sc.id = #{sId}
</if>
</if>
ORDER BY pc.sort,sc.sort,p.project_sort
</select>
<!-- 大屏前端使用 -->
<resultMap id="projectFrontMap" type="com.twx.mybatis.entity.ProjectFront">
<result column="project_name" property="projectName" jdbcType="VARCHAR"></result>
<result column="url" property="url" jdbcType="VARCHAR"></result>
<result column="description" property="description" jdbcType="VARCHAR"></result>
<result column="username" property="username" jdbcType="VARCHAR" />
<result column="password" property="password" jdbcType="VARCHAR" />
<result column="thumbnail_url" property="thumbnailUrl" jdbcType="VARCHAR" />
<result column="category" property="category" jdbcType="VARCHAR" />
<result column="category_sort" property="categorySort" jdbcType="INTEGER" />
<result column="second_category" property="secondCategory" jdbcType="VARCHAR" />
<result column="second_category_sort" property="secondCategorySort" jdbcType="INTEGER" />
<result column="project_sort" property="projectSort" jdbcType="INTEGER" />
<result column="maintainer" property="maintainer" jdbcType="VARCHAR" />
<result column="create_time" property="createTime" jdbcType="DATE" />
</resultMap>
<sql id="front_columns">
p.project_name,p.url,p.description,p.tags,
p.username,p.`password`,p.thumbnail_url,
pc.name as category,pc.sort as category_sort,
sc.name as second_category,sc.sort as second_category_sort,
p.project_sort,p.maintainer,p.create_time
</sql>
<select id="getProjectFront" resultMap="projectFrontMap">
select
<include refid="front_columns"></include>
from project_info p
LEFT JOIN second_category sc on sc.id = p.second_category_id
INNER JOIN primary_category pc on pc.id = sc.p_id
WHERE p.back_service=0
ORDER BY pc.sort,sc.sort,p.project_sort
</select>
</mapper>
mybatis解析xml文件的入口是 mybatis-config.xml
。
依次解析properties->settings->typeAliases->plugins->objectFactory->objectWrapperFactory->reflectorFactory->environments->databaseIdProvider->typeHandlers->mappers
最后的mapperElement(root.evalNode("mappers"));
解析的就是我们的业务mapper:
<mappers>
<mapper resource="com/twx/mybatis/mapper/xml/projectInfo.xml"/>
</mappers>
我们先来看mybatis-config.xml
的解析
mybatis-config.xml
String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); assert inputStream!=null; SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
我们的入口程序中指定了mybatis-config.xml的路径,传递给了SqlSessionFactoryBuilder
的build
方法。
XMLConfigBuilder1
我们进入看看:
注意XMLConfigBuilder
对象,它负责解析mybatis-config.xml中除了mappers
标签以外的元素。
我们进入XMLConfigBuilder
的构造方法中看看,同时也要注意一下XMLConfigBuilder
继承了一个BaseBuilder
类:
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
//XMLMapperEntityResolver实现了org.xml.EntityResolver接口
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
}
注意:构造方法中新建了一个new XPathParser
对象,我们不深入这个类,它的作用是可以根据标签方便的获取xml内容。例如parser.evalNode("/configuration")
就能获取到configuration
中的内容。
<?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>
<plugins>
<plugin interceptor="com.twx.mybatis.interceptor.InterceptorDemo1" />
</plugins>
<environments default="development">
</environments>
<mappers>
<mapper resource="com/twx/mybatis/mapper/xml/projectInfo.xml"/>
</mappers>
</configuration>
刚刚看了XMLConfigBuilder
的构造方法,我们继续:
仍然还是调用构造方法,注意在构造方法中调用了父类的构造方法,传进了一个新的Configuration
对象。
特别说明,Configuration
类在mybatis中可是非常非常重要,几乎无处不在。它保存着mybatis-config.xml中的配置信息以及我们业务mapper中的类信息。真的超级重要。
Configuration1
既然这么重要,我们看看new Configuration
的时候做了些什么操作吧。
/**
* 初始化了内置别名、LanguageDriver
*/
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
...
省略了其他
}
因为configuration的内容真的太多了,它有很多成员变量,这些成员变量都是new出来的,所以新建一个configuration对象是个耗时的工作。
TypeAliasRegistry
我们先看看构造器中用到的这个typeAliasRegistry对象吧,它在成员变量中的定义如下:
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
我们再来看看它:
public class TypeAliasRegistry {
/**
* 例子:<typeAlias alias="Author" type="domain.blog.Author"/>
*/
private final Map<String, Class<?>> typeAliases = new HashMap<>();
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
...
省略
}
}
TypeAliasRegistry
从它的名称就能知道它的作用:别名注册器,啥意思呢?
比如我在mybaits-config中定义了一个别名 <typeAlias alias="Author" type="domain.blog.Author"/>
,最终就会调用TypeAliasRegistry
的registerAlias
方法
registerAlias('Author',Author.class)
如何使用呢?遇到的时候再告诉你吧。
注意一下:在TypeAliasRegistry构造器中我们初始化了基本类型的别名
Configuration2
现在我们回到Configuration1一节的最后,继续。
在Configuration的构造器中我们初始化了
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
再来看mybatis-config.xml中的环境变量:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
</dataSource>
</environment>
</environments>
这里提一下,后面我们在解析environments
标签的时候会用到我们的别名JDBC、POOLED
现在我们回到XMLConfigBuilder1中的构造器:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
//注意:在此new Configuration实例
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
构造器后续的几条语句就没什么好说的了。
我们又回到了XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
紧接着调用XMLConfigBuilder
的parse方法,正式开始解析mybatis-config.xml
XMLConfigBuilder.parse
因为我们在mybatis-config.xml
中只定义了plugins--environments--mappers
这几个节点,所以我们就只看这几个节点的解析。(其中settings标签设置了很多默认值,比如下划线转驼峰,可以去看看,这里不讲解)
解析plugins
plugin在mybatis中可是非常出名的,像我们最经常使用的分页插件,入口就在这。
因为这一节只是说xml的解析,所以不能说得太详细。
先贴一下xml中的配置:
<plugins>
<plugin interceptor="com.twx.mybatis.interceptor.InterceptorDemo1" />
</plugins>
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//获取plugin节点的interceptor属性
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
//反射new
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
//添加到过滤器链interceptorChain中
configuration.addInterceptor(interceptorInstance);
}
}
}
- 获取plugin节点的interceptor属性
- 通过反射生成我们自定义的拦截器(比如分页插件)
- 最后添加到过滤器链中
过滤器链设计得非常巧妙,很精彩。以后再给你们分享。
解析environments
环境变量主要是设置数据库连接地址、用户名、密码等
再贴xml配置:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/dashboard_heart_beat"/>
<property name="username" value="twx"/>
<property name="password" value="soyuan123"/>
</dataSource>
</environment>
</environments>
配合着代码看:
我们来看dataSourceElement
这个方法,正好把上面提到的TypeAliasRegistry
用上:
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
//这里的type=POOLED
String type = context.getStringAttribute("type");
//这里存储用户名、密码、数据库地址
Properties props = context.getChildrenAsProperties();
//如果type=POOLED => PooledDataSourceFactory
//别名注册地:typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
请看代码里的注释。
其中resolveClass(type)
在这里是resolveClass("POOLED")
,跟踪一下代码就很容易发现,resolveClass(type)
最终调用的是typeAliasRegistry.resolveAlias(alias);
。
通过resolveClass(type)
拿到Class类型后,就能通过反射生成PooledDataSourceFactory
实例了。
接下来就是最最复杂的mappers解析了,我们的select、update、delete
都定义在此。
projectInfoMapper.xml
//映射器--这里面非常复杂--转交给XMLMapperBuilder实现 mapperElement(root.evalNode("mappers"));
配合着xml,来看看mapperElement
方法吧:
<mappers>
<mapper resource="com/twx/mybatis/mapper/xml/projectInfo.xml"/>
</mappers>
由于我的演示示例中没有定义package
标签,代码不会进入。
此次我会进入红色箭头指示的地方,因为我只定义了resource
属性
来看硬核的地方:XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
XMLMapperBuilder
进入构造方法看看,同时注意它也继承了父类BaseBuilder
(之前已经提过了)
同样也new
了个XPathParser
解析器,跳过。看上图我红框标记的地方:
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
这也是个非常重要的类,看名字就知道它是个助手类,负责组装SQL用的。很重要很重要
参数:
- configuration: 之前我们在
XMLConfigBuilder
构造器中new出来的,还有印象吗 - resource: mapper文件路径,这里是
com/twx/mybatis/mapper/xml/projectInfo.xml
我们来看看它吧。
MapperBuilderAssistant
public class MapperBuilderAssistant extends BaseBuilder {
/**
* xml文件中的命名空间 <br/>
* 例如:<mapper namespace="com.twx.mybatis.mapper.ProjectInfoMapper">
*/
private String currentNamespace;
/**
* mapper xml 文件的全路径
*/
private final String resource;
private Cache currentCache;
private boolean unresolvedCacheRef; // issue #676
public MapperBuilderAssistant(Configuration configuration, String resource) {
super(configuration);
ErrorContext.instance().resource(resource);
this.resource = resource;
}
}
它也继承了BaseBuilder
,所以它也有一个Configuration
对象的引用(还记得我说过的,Configuration对象很重要,后面非常多类都要用到它。
注意它的几个成员变量:
- currentNamespace:业务xml文件中的命名空间
- resource:业务xml 文件的全路径,设置在
mybatis-config.xml mapper
标签的resource属性中 - currentCache:一级缓存,暂时不讲解
它还有很多重要的方法,这里先列举几个常用的:
- applyCurrentNamespace-返回mapper接口方法的完全限定名称
- useNewCache--创建缓存对象
- addResultMap--生成ResultMap,add到configure对象的resultMaps中
- addMappedStatement--生成MappedStatement对象,set到config对象的mappedStatements中
- getStatementResultMaps--根据resultMap参数从config对象中获取ResultMap对象
- buildResultMapping--构建ResultMapping对象
- resolveResultJavaType-返回java bean属性对应的Java类型
XMLMapperBuilder解析
parse
从XMLMapperBuilder构造器出来,紧接着就调用parse方法
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
String namespace = context.getStringAttribute("namespace");
拿到的是
我们再来看看更具体的configurationElement(XNode context)
方法
接下来会着重讲解三个方法:
- resultMapElements
- sqlElement
- buildStatementFromContext
这三个方法对应下图xml文件中的三个标签
resultMapElements1
先来看如何解析resultMap
标签的。
真正解析<resultMap>
的方法很长。我们分段看。
<resultMap id="projectInfoMap" type="com.twx.mybatis.entity.ProjectInfo">
省略
</resultMap>
//type = com.twx.mybatis.entity.ProjectInfo
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
由于只传了一个type属性,所以String type=com.twx.mybatis.entity.ProjectInfo
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
typeClass
理所应当是com.twx.mybatis.entity.ProjectInfo
再来看上面这段代码,先解释一下ResultMapping
.
ResultMapping
一个ResultMapping
对应entity中的一个属性,亦即
上图xml中的每一行最后都被解析成一个一个的ResultMapping
对象。
理解了这点,我们回到java代码中的for循环。for循环就是解析这些id,result。由于我们没有定义constructor,discriminator
,就跳过这几行代码。直接看id
和 result
标签的解析。
我们来看这行代码:
//解析<id> <result>标签
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
来看buildResultMappingFromContext
方法,buildResultMappingFromContext 用于生成一个ResultMapping
对象。
上图这段java代码对照着下图看就很清晰了,不做过多解析。(注意,我们没对resultMap的每个属性声明typeHandler)
代码最后一行我特别标注,最终ResultMapping
对象的生成是转交给builderAssistant
助手实现的(前面我们提到过,还有印象吗,当时只是提到了方法名称,并没有具体去分析,现在我们去看看)。
我们来看builderAssistant
的buildResultMapping
方法:
public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType, JdbcType jdbcType,
String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags,
String resultSet, String foreignColumn, boolean lazy) {
//根据属性property名称例如username获取对应的java类型String
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
//typeHandler在我们的示例中是null,返回null
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
//是否有组合对象
List<ResultMapping> composites;
//假如<result column="username" property="username" jdbcType="VARCHAR" />
// 有配置select属性,那么这里的nestedSelect!=null
//有配置foreignColumn属性,这里的foreignColumn!=null
//在我们的示例中,显然两者都为空,所以composites=null
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
composites = Collections.emptyList();
} else {
composites = parseCompositeColumnName(column);
}
return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
.jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
.resultSet(resultSet)
.typeHandler(typeHandlerInstance)
.flags(flags == null ? new ArrayList<>() : flags)
.composites(composites)
.notNullColumns(parseMultipleColumnNames(notNullColumn))
.columnPrefix(columnPrefix)
.foreignColumn(foreignColumn)
.lazy(lazy)
.build();
}
这个方法的作用是为了:
- 确定java bean属性对应的Class类型
- 确定属性交给什么TypeHandler处理
- 以及属性是否有嵌套查询
最后的return语句其实就是new ResultMapping()
,只不过用的是建造者模式而已。
resultMapElements2
我们回到resultMapElements1一节的最后。
//每一个ResultMapping对应entity中的一个属性 List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings); for (XNode resultChild : resultChildren) { .... 省略 //解析<id> <result>标签 resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); }
经过我们上面对ResultMapping
的分析,执行完上面这个for循环后,我们继续。
再次贴上xml:
<resultMap id="projectInfoMap" type="com.twx.mybatis.entity.ProjectInfo">
...
省略了
</resultMap>
看代码注释就好。
最后调用ResultMapResolver.resolve()
生成ResultMap
对象。
ResultMapResolver.resolve()
最终还是调用我一再强调的助手类MapperBuilderAssistant
的addResultMap
public ResultMap resolve() { return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping); }
我们来看MapperBuilderAssistant.addResultMap()
/**
* 生成ResultMap,add到configure对象的resultMaps中
* @param id resultMapId
* @param type mapper返回类型
* @param extend
* @param discriminator
* @param resultMappings
* @param autoMapping
* @return
*/
public ResultMap addResultMap(String id, Class<?> type, String extend,
Discriminator discriminator, List<ResultMapping> resultMappings, Boolean autoMapping) {
//resultMap的完全限定名称
//例如:com.twx.mybatis.mapper.ProjectInfoMapper.projectInfoMap
id = applyCurrentNamespace(id, false);
//TODO: 构造示例研究
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
...
省略
}
//build方法内部做了很多操作,主要是提取ResultMapping中的属性
//ResultMapping存储了很多@Result的属性
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
configuration.addResultMap(resultMap);
return resultMap;
}
不需要关心太多,直接看最后new ResultMap.Builder().build()
这个方法不详细讲了,大概的内容就是遍历之前生成的List<ResultMapping> resultMappings
给自己的成员变量赋值。下面列举几个关键的成员变量。
/**
* resultMap的id
*/
private String id;
/**
* 返回类型,例如com.twx.mybatis.entity.ProjectInfo
*/
private Class<?> type;
private List<ResultMapping> resultMappings;
/**
* id标签对应的ResultMapping
*/
private List<ResultMapping> idResultMappings;
/**
* 构造器对应的constructorResultMappings
*/
private List<ResultMapping> constructorResultMappings;
/**
* id+result标签对应的ResultMapping
*/
private List<ResultMapping> propertyResultMappings;
至此,终于完成了<resultMap>
标签的解析了。
sqlElement
这是我们的第二大标签。我们先看看xml中的定义与使用。你一定很熟悉。
<sql id="info_columns">
p.id, p.project_name,p.url,p.description,p.tags,
p.username,p.`password`,p.thumbnail_url,p.second_category_id,
p.project_sort,p.maintainer,p.`status`,p.back_service,p.create_time
</sql>
<select id="getProjectInfo" resultMap="projectInfoMap">
select
<include refid="info_columns"></include>
from project_info p
</select>
代码实现很简单,只是把<sql>
标签的id和内容存储到sqlFragments
。
sqlFragments是Configuration中的sqlFragments引用
sqlFragments的定义:
/**
* 存储<sql id="all_col"></sql>标签语句,key指代id
*/
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
注意 sqlFragments.put(id, context);
中的context是一个Xnode节点,后续会拿出来解析的。这里不做处理
buildStatementFromContext
最重磅的一个方法了。非常复杂!
做好准备。这是xml解析最后一步了。
那来看buildStatementFromContext
:
由于解析select delete insert
等语句比较复杂,转交给单独的类XMLStatementBuilder
解析
parseStatementNode()
同样继承了BaseBuilder
,构造方法没什么好讲的了。之前都说过了。
注意一下context代表的是xml node节点内容。例如:
<select id="getProjectInfo" resultMap="projectInfoMap">
select
<include refid="info_columns"></include>
from project_info p
LEFT JOIN second_category sc on sc.id = p.second_category_id
INNER JOIN primary_category pc on pc.id = sc.p_id
where p.logic_delete = 0
<if test="fId != null">
and pc.id = #{fId}
<if test="sId !=null">
and sc.id = #{sId}
</if>
</if>
ORDER BY pc.sort,sc.sort,p.project_sort
</select>
我们直接来看parseStatementNode()
,这方法也是超级的长。慢慢分析。
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
id在此是getProjectInfo
,databaseId=null
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
nodeName是select insert update delete
其中一种,在此是select
sqlCommandType=SqlCommandType.SELECT
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。
//这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
看代码中的注释。针对select查询语句,useCache
默认是true
//解析include语句
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
上面两行用于解析<include>
标签,例如示例中的:
select
<include refid="info_columns"></include>
from project_info p
上面替换<include>
标签的方法使用到了递归,最核心的是第一个红色框里的代码,找到include对应的<sql>
标签,然后替换内容。因为递归的原因,不太好通过文字讲解。(可以自己调试代码感受)
我们继续回到parseStatementNode()
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
//如果未指定,默认是XMLLanguageDriver
//初始化new Configuration时
//languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
LanguageDriver langDriver = getLanguageDriver(lang);
parameterType
我们没传,所以parameterType=null
LanguageDriver langDriver
默认是XMLLanguageDriver
,看注释
//<selectKey keyProperty="id" resultType="int" order="BEFORE">
// select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
// </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 {
//(仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键
// (比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
过程就不讲了。不重要。
keyStatementId="com.twx.mybatis.mapper.ProjectInfoMapper.getProjectInfo!selectKey"
KeyGenerator keyGenerator = NoKeyGenerator
//如果是动态的,返回DynamicSqlSource
//如果是静态的,返回RawSqlSource
//SqlSource接口有一个方法BoundSql getBoundSql(Object parameterObject);
//能拿到BoundSql
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
这里面也是非常复杂的。如果你的select语句中有if | foreach | trim| when (...)
或者 ${} 那就是动态的,否则是静态的。
如果是动态的,返回DynamicSqlSource
如果是静态的,返回RawSqlSource
我们来看看XMLLanguageDriver
的createSqlSource
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
内部又借助XMLScriptBuilder
的parseScriptNode()
实现。
那我们转战战场。
XMLScriptBuilder
- 同样继承了
BaseBuilder
- 构造方法中调用了
initNodeHandlerMap()
- initNodeHandlerMap内部有些TrimHandler之类的东西,这些是内部类
我们来看parseScriptNode()方法:
public SqlSource parseScriptNode() {
//解析<select>标签中的动态变量
//MixedSqlNode 维护者一个列表List<SqlNode> contents,
// contents可能包含 StaticTextSqlNode和IfSqlNode或者WhereSqlNode或者其他
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
parseDynamicTags(context)
的作用是解析<select>
标签中的动态变量。
返回的MixedSqlNode
维护着一个列表List<SqlNode> contents
,contents可能包含 StaticTextSqlNode
和IfSqlNode
或者WhereSqlNode
或者其他。
我们粗略地看看parseDynamicTags(context)
就行,内部的textSqlNode.isDynamic()
非常复杂的。主要是数据处理。
解析完parseDynamicTags
,必然是动态的(我们的示例),所以返回DynamicSqlSource
parseStatementNode()2
那我们继续回到parseStatementNode()
的这个地方:
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
继续往下看。
如果未指定,默认是StatementType.PREPARED
这非常重要,在真正取数据的时候,会根据这个枚举类型new不同的statementHandler
对象
详情参照RoutingStatementHandler
构造器
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
在此我们只要知道StatementType statementType=StatementType.PREPARED
,用到的时候再细聊。
接下来的这些代码没什么好说的,都是从xml配置取值。
parseStatementNode
()方法的最后就是生产MappedStatement
对象。
一个MappedStatement
对象就代表着你定义的一个<select>
或者<update>
或者<delete>
等等
//非常重要: 生成MappedStatement对象,set到config对象的mappedStatements中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
我们结合idea看一下具体传进来的是些什么参数吧:
结合着上图的参数,我们来看看builderAssistant.addMappedStatement
内部代码
无它--建造者模式生成MappedStatement
对象
注意,最后把生成的statement
添加到了Configuration
对象中去了,即:
/**
* 很重要:用于存储select insert 等语句
*/
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
添加到了Configuration
对象的mappedStatements
容器中。key是