最近看到这样一则消息《全球首位AI工程师诞生,“码农”未来会消失?》,文章提到百度董事长兼首席执行官李彦宏认为未来将不会存在“程序员”这种职业。行业大佬的这种说法,让我异常恐慌。难道程序员就这样被淘汰了?AI真的要打败创造它的造物主吗?在这个急速发展的社会中,从事程序员工作的我们究竟该怎么办呢?或许《MyBatis是纸老虎吗?》系列文章会为我们带来一些不一样的答案。
在《MyBatis是纸老虎吗?(四)》这篇文章中我们一起学习了MyBatis配置文件中的plugins元素,梳理了该元素的解析过程。通过这篇文章我们知道什么是MyBatis拦截器,了解了该控件基本用法,学会了自定义该控件的方法。今天我将继续学习MyBatis框架。我希望通过这篇文章捋清MyBatis配置文件中的mappers元素。
1 mappers元素的定义
大家都知道使用MyBatis框架时,需要定义一个配置文件(在这个系列文章中我们也一直强调这个文件),而mappers元素就是定义在这个文件中的。先来看下面这样一段代码:
<?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>
<mappers>
<mapper resource="user.xml"/>
</mappers>
</configuration>
在这段代码中,我们通过mappers元素将自定义的user.xml文件引入到MyBatis框架中,由其对这个文件进行管理。那这个文件是怎么被MyBatis框架解析和管理的呢?
2 mappers元素的解析
在《MyBatis是纸老虎吗?(三)》和《MyBatis是纸老虎吗?(四)》这两篇文章中我们着重介绍了MyBatis框架解析MyBatis配置文件及相关元素(plugins)的流程。本小节将继续前一篇文章的思路,介绍MyBatis配置文件中mappers元素的解析流程。这次我们不再啰嗦直进入主题:进入XMLConfigBuilder类【这个类的主要作用是解析MyBatis配置文件】的parse()方法中,然后继续进入该类的parseConfiguration()方法,接着重点关注该方法中这样一行代码mappersElement(root.evalNode("mappers")),下面一起看一下mappersElement()方法的源码(注意该方法会接收一个XNode元素,root.evalNode("mappers")这句的主要作用是解析配置文件中的mappers元素):
private void mappersElement(XNode context) throws Exception {
if (context == null) {
return;
}
for (XNode child : context.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);
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try (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.");
}
}
}
}
从源码不难看出mappers的子元素有:package(通过name属性指定MyBatis框架要扫描包路径)、mapper(通过resource属性指定自定义sql文件路径、通过url属性指定、通过class属性指定Dao接口文件路径。注意:mapper标签中只能存在这三个属性中的一个,不能同时存在多个)。如果配置文件配置合理,接下来就开始解析流程。这里我们以mapper+resource的形式来梳理,先看源码中的一个小技巧,如下所示:
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
……
}
之前我们写流代码时,一般都用try{}catch{}finally{}格式,在finally中对流进行关闭,但是这种写法我们不需要去关注流关闭的操作,因为InputStream实现了Closeable接口,在这种写法中java会自动关闭InputStream对象。
下面就让我们深入研究一下try分支。这段代码分支中有一个XMLMapperBuilder类,通过名字可以判断出该类主要用于解析MyBatis中的mapper文件,即sql定义文件。这段代码的详情如下所示:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
configuration.getSqlFragments());
mapperParser.parse();
创建XMLMapperBuilder对象时,会接收三个参数,它们分别为:mapper文件流、Configuration对象、mapper文件路径、Map<String, XNode>对象(位于Configuration对象中,实际名称为sqlFragments)。接着会调用XMLMapperBuilder对象中的parse方法开始解析操作,先来看一下这个方法(XMLMapperBuilder#parse())的源码:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
这个方法首先会调用Configuration中的isResourceLoaded()方法判断当前的mapper文件是否被加载过,该方法的源码如下所示:
public boolean isResourceLoaded(String resource) {
return loadedResources.contains(resource);
}
该源码中的loadedResources变量的定义位于Configuration类中,其源码为:Set<String> loadedResources = new HashSet<>()。由于调用时,该属性中尚未有数据,所以该方法会返回false。回到XMLMapperBuilder的parse()方法中,由于调用返回了false,所以这个方法会走进if分支中,然后调用XMMapperBuilder中的configurationElement()方法解析mapper文件,接着调用Configuration中的addLoadedResource()方法将已经解析的resource添加到Configuration中的loadedResources变量中,以防止重复解析。接着在调用XMMapperBuilder类中的bindMapperForNamespace()方法。下面让我们先来看一下XMMapperBuilder中的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);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 注意context.evalNodes()方法会解析出满足参数条件的xml节点数据,比如这里的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);
}
}
这个方法的处理逻辑和MyBatis配置文件的处理逻辑是一样的,就是逐个元素解析。从这个源码不难发现mapper文件中的重要数据有:
- mapper元素上的namespace属性(该属性值一般是相应Dao文件的包名+接口,注意这个属性值不能为空)
- mapper元素中可以定义的子元素有:cache-ref、cache、parameterMap、resultMap、sql、select、insert、update、delete。其中工作中最常用的是resultMap、sql、select、insert、update及delete。resultMap用于定义sql查询结果和目标对象属性之间的映射关系;sql用于定义一些公共的数据,比如select语句中的查询字段和条件语句等;select、insert、update及delete则主要用于定义相应的sql语句。个人理解parameterMap用于定义查询参数和字段之间的映射关系,而cache和cache-ref则用于定义与缓存相关的信息
下面先来看一个mapper文件,这个文件中的基本符合上面根据代码梳理出来的信息,具体如下所示:
<?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.com.chinasofti.springtransaction.UserDao">
<!--<cache></cache>
<cache-ref namespace=""/>
<parameterMap id="" type=""></parameterMap>-->
<!-- table 实体映射 -->
<resultMap id="userDomain" type="user">
<id property="id" column="id"/>
<result property="loginName" column="login_name"/>
<result property="nickName" column="nick_name"/>
<result property="userName" column="user_name"/>
<result property="cellphone" column="cellphone"/>
<result property="gender" column="gender"/>
<result property="birthday" column="birthday"/>
<result property="chIdCard" column="ch_id_card"/>
<result property="email" column="email"/>
<result property="socialMeda" column="social_meda"/>
<result property="socialtype" column="social_type"/>
<result property="pass" column="pass"/>
<result property="modiTimes" column="modi_times"/>
<result property="modiDate" column="modi_date"/>
<result property="cardNo" column="card_no"/>
<result property="integral" column="integral"/>
<result property="rMoney" column="r_money"/>
<result property="gMoney" column="g_money"/>
<result property="stat" column="stat"/>
<result property="insTime" column="ins_time"/>
<result property="uptTime" column="upt_time"/>
<result property="rmk1" column="rmk1"/>
<result property="rmk2" column="rmk2"/>
<result property="rmk3" column="rmk3"/>
<result property="rmk4" column="rmk4"/>
</resultMap>
<!-- dto 实体映射 -->
<resultMap id="userDtoDomain" type="userDto">
<id property="id" column="id"/>
<result property="nickName" column="nick_name"/>
<result property="userName" column="user_name"/>
<result property="cellphone" column="cellphone"/>
<result property="gender" column="gender"/>
<result property="birthday" column="birthday"/>
<result property="chIdCard" column="ch_id_card"/>
<result property="email" column="email"/>
<result property="socialMeda" column="social_meda"/>
<result property="socialtype" column="social_type"/>
<result property="cardNo" column="card_no"/>
<result property="integral" column="integral"/>
<result property="rMoney" column="r_money"/>
<result property="gMoney" column="g_money"/>
<result property="insTime" column="ins_time"/>
</resultMap>
<!-- 查询字段 -->
<sql id="userColumn">
`id`,
`login_name`,
`nick_name`,
`user_name`,
`cellphone`,
`gender`,
`birthday`,
`ch_id_card`,
`email`,
`social_meda`,
`social_type`,
`pass`,
`modi_times`,
`modi_date`,
`card_no`,
`integral`,
`r_money`,
`g_money`,
`stat`,
`ins_time`,
`upt_time`,
`rmk1`,
`rmk2`,
`rmk3`,
`rmk4`
</sql>
<!-- 查询条件,根据主键查询、昵称模糊查询、姓名模糊查询、性别查询、电话模糊查询及他们之间的组合查询 -->
<sql id="queryConditon">
<where>
<if test="id != null">AND `id`=#{id}</if>
<if test="nickName != null and nickName != ''.toString()">AND `nick_name` LIKE '%${nickName}%'</if>
<if test="userName != null and userName != ''.toString()">AND `user_name` LIKE '%${userName}%'</if>
<if test="gender != null and gender != ''.toString()">AND `gender` = '${gender}'</if>
<if test="cellphone != null and cellphone != ''.toString()">AND `cellphone` LIKE '%${cellphone}%'</if>
</where>
</sql>
<!-- =============================================================================================================================================================== -->
<!-- 删除数据,依据主键进行删除 -->
<delete id="deleteById" parameterType="long">
DELETE FROM `tbl_user` WHERE `id` = #{id}
</delete>
<!-- 删除数据,依据 id 集合批量删除 -->
<delete id="deleteByIds" parameterType="userDto">
DELETE FROM `tbl_user` WHERE `id` IN <foreach collection="serialNumber" open="(" close=")" item="item" separator=",">#{item}</foreach>
</delete>
<!-- =============================================================================================================================================================== -->
<!-- =============================================================================================================================================================== -->
<!-- 新增数据,依据非空字段进行新增 -->
<insert id="insertBySelect" parameterType="user" useGeneratedKeys="true">
INSERT INTO `tbl_user`
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">`id`,</if>
<if test="loginName != null and loginName != ''.toString()">`login_name`,</if>
<if test="nickName != null and nickName != ''.toString()">`nick_name`,</if>
<if test="userName != null and userName != ''.toString()">`user_name`,</if>
<if test="cellphone != null and cellphone != ''.toString()">`cellphone`,</if>
<if test="gender != null">`gender`,</if>
<if test="birthday != null">`birthday`,</if>
<if test="chIdCard != null and chIdCard != ''.toString()">`ch_id_card`,</if>
<if test="email != null and email != ''.toString()">`email`,</if>
<if test="socialMeda != null and socialMeda != ''.toString()">`social_meda`,</if>
<if test="socialtype != null">`social_type`,</if>
<if test="pass != null and pass != ''.toString()">`pass`,</if>
<if test="modiTimes != null">`modi_times`,</if>
<if test="modiDate != null">`modi_date`,</if>
<if test="cardNo != null">`card_no`,</if>
<if test="integral != null">`integral`,</if>
<if test="rMoney != null">`r_money`,</if>
<if test="gMoney != null">`g_money`,</if>
<if test="stat != null">`stat`,</if>
<if test="insTime != null">`ins_time`,</if>
<if test="uptTime != null">`upt_time`,</if>
<if test="rmk1 != null and rmk1 != ''.toString()">`rmk1`,</if>
<if test="rmk2 != null and rmk2 != ''.toString()">`rmk2`,</if>
<if test="rmk3 != null and rmk3 != ''.toString()">`rmk3`,</if>
<if test="rmk4 != null and rmk4 != ''.toString()">`rmk4`,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="loginName != null and loginName != ''.toString()">#{loginName},</if>
<if test="nickName != null and nickName != ''.toString()">#{nickName},</if>
<if test="userName != null and userName != ''.toString()">#{userName},</if>
<if test="cellphone != null and cellphone != ''.toString()">#{cellphone},</if>
<if test="gender != null">#{gender},</if>
<if test="birthday != null">#{birthday},</if>
<if test="chIdCard != null and chIdCard != ''.toString()">#{chIdCard},</if>
<if test="email != null and email != ''.toString()">#{email},</if>
<if test="socialMeda != null and socialMeda != ''.toString()">#{socialMeda},</if>
<if test="socialtype != null">#{socialtype},</if>
<if test="pass != null and pass != ''.toString()">#{pass},</if>
<if test="modiTimes != null">#{modiTimes},</if>
<if test="modiDate != null">#{modiDate},</if>
<if test="cardNo != null and cardNo != ''.toString()">#{cardNo},</if>
<if test="integral != null">#{integral},</if>
<if test="rMoney != null">#{rMoney},</if>
<if test="gMoney != null">#{gMoney},</if>
<if test="stat != null">#{stat},</if>
<if test="insTime != null">#{insTime},</if>
<if test="uptTime != null">#{uptTime},</if>
<if test="rmk1 != null and rmk1 != ''.toString()">#{rmk1},</if>
<if test="rmk2 != null and rmk2 != ''.toString()">#{rmk2},</if>
<if test="rmk3 != null and rmk3 != ''.toString()">#{rmk3},</if>
<if test="rmk4 != null and rmk4 != ''.toString()">#{rmk4},</if>
</trim>
</insert>
<!-- =============================================================================================================================================================== -->
<!-- =============================================================================================================================================================== -->
<!-- 修改数据,依据非空字段进行修改 -->
<update id="modifySelectById" parameterType="user">
UPDATE `tbl_user`
<set>
<if test="loginName != null and loginName != ''.toString()">`login_name` = #{loginName},</if>
<if test="nickName != null and nickName != ''.toString()">`nick_name` = #{nickName},</if>
<if test="userName != null and userName != ''.toString()">`user_name` = #{userName},</if>
<if test="cellphone != null and cellphone != ''.toString()">`cellphone` = #{cellphone},</if>
<if test="gender != null">`gender` = #{gender},</if>
<if test="birthday != null">`birthday` = #{birthday},</if>
<if test="chIdCard != null and chIdCard != ''.toString()">`ch_id_card` = #{chIdCard},</if>
<if test="email != null and email != ''.toString()">`email` = #{email},</if>
<if test="socialMeda != null and socialMeda != ''.toString()">`social_meda` = #{socialMeda},</if>
<if test="socialtype != null">`social_type` = #{socialtype},</if>
<if test="pass != null and pass != ''.toString()">`pass` = #{pass},</if>
<if test="modiTimes != null">`modi_times` = #{modiTimes},</if>
<if test="modiDate != null">`modi_date` = #{modiDate},</if>
<if test="cardNo != null and cardNo != ''.toString()">`card_no` = #{cardNo},</if>
<if test="integral != null">`integral` = #{integral},</if>
<if test="rMoney != null">`r_money` = #{rMoney},</if>
<if test="gMoney != null">`g_money` = #{gMoney},</if>
<if test="stat != null">`stat` = #{stat},</if>
<if test="insTime != null">`ins_time` = #{insTime},</if>
<if test="uptTime != null">`upt_time` = #{uptTime},</if>
<if test="rmk1 != null and rmk1 != ''.toString()">`rmk1` = #{rmk1},</if>
<if test="rmk2 != null and rmk2 != ''.toString()">`rmk2` = #{rmk2},</if>
<if test="rmk3 != null and rmk3 != ''.toString()">`rmk3` = #{rmk3},</if>
<if test="rmk4 != null and rmk4 != ''.toString()">`rmk4` = #{rmk4},</if>
</set>
WHERE `id` = #{id}
</update>
<!-- =============================================================================================================================================================== -->
<!-- =============================================================================================================================================================== -->
<!-- 依据主键进行查询 -->
<select id="queryById" parameterType="long" resultMap="userDtoDomain">
SELECT
<include refid="userColumn"/>
FROM `tbl_user` WHERE `id` = #{id}
</select>
<!-- 依据指定条件进行查询 -->
<select id="queryByCondition" parameterType="userDto" resultMap="userDtoDomain">
SELECT
<include refid="userColumn"/>
FROM `tbl_user` <include refid="queryConditon"></include>
<!-- <foreach collection="sorts" open="ORDER BY" close="" item="item" separator=",">${item.sort} ${item.order}</foreach>LIMIT #{start}, #{rows} -->
</select>
<!-- 依据指定条件进行查询 -->
<select id="queryCountByCondition" parameterType="userDto" resultType="long">
SELECT COUNT(*) FROM `tbl_user` <include refid="queryConditon"></include>
</select>
<!-- =============================================================================================================================================================== -->
</mapper>
下面就一起来看一下select、insert、update及delete语句的解析逻辑,首先看一下这个解析过程中涉及到的一些方法的源码:
private void buildStatementFromContext(List<XNode> list) {
// 注意这里接到的是一个XNode对象组成的集合,由于MyBatis配置文件中未指定databaseId,所以这里不会执行if分支中的代码
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 注意下面这段代码的主要目的是将mapper文件中定义的sql语句解析为Statement
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context,
requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
从源码不难看出buildStatementFromContext()方法的主要作用就是循环遍历list参数,然后创建XMLStatementBuilder对象,接着调用该对象上的parseStatementNode()方法解析mapper文件中定义的sql语句。下图展示的是buildStatementFromContext()方法运行时状态图,从图中可以看出list参数就是在mapper文件中定义的7个sql语句(具体可以参看上面的mapper文件案例),详细信息如下图所示:
注意这段代码中有一个XMLStatementBuilder类,这个类的主要作用是解析在mapper文件中定义的sql语句,比如select、insert等。下面看一下XMLStatementBuilder类中的parseStatementNode()方法的源码:
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 Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
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;
}
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType
.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
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");
if (resultTypeClass == null && resultMap == null) {
resultTypeClass = MapperAnnotationBuilder.getMethodReturnType(builderAssistant.getCurrentNamespace(), id);
}
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");
boolean dirtySelect = context.getBooleanAttribute("affectData", Boolean.FALSE);
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}
这个方法的源码比较长,主要过程有这样几个:1)解析在select、delete、insert、update标签上定义的id属性。2)解析节点名称,比如delete;接着将节点名称解析为SqlCommandType中的枚举(SqlCommandType是一个枚举类,其中的枚举值有UNKNOWN、INSERT、UPDATE、DELETE、SELECT、FLUSH),如果是delete,解析的结果是DELETE;接着就是根据这个枚举值确定isSelect的值(这里是false,因为解析出来的是DELETE);紧接着再解析标签上的flushCache、useCache、resultOrdered属性(注意这几个属性位于select标签上)。3)接着解析标签中的include子标签。4)然后就是解析标签上的parameterType属性,这个想必大家都非常熟悉了,就是sql语句需要的参数的类型,后面会调用resolveClass()方法去解析别名对应的实际类型。5)解析标签上的lang属性并加载对应的驱动。5)后面就是标签中其他属性的解析,比如statementType、fetchSize、timeout、parameterMap、resultType、resultMap、resultSetType、keyColumn、resultSets等等(关于这些属性的解析,这里就不再介绍了,有兴趣可以翻阅并跟踪一下源码)。6)将解析的这些数据添加到BuilderAssistant对象中,通过调用addMappedStatement()方法完成此操作。注意:BuilderAssistant对象在XMLMapperBuilder类的configurationElement(XNode context)方法中出现过,它是XMLMapperBuilder类中的一个属性,之后在创建XMLStatementBuilder对象时,会将其传递给XMLStatementBuilder对象,所以我们在XMLStatementBuilderparseStatementNode()方法中看到的BuilderAssistant对象就是XMMapperBuilder类中那个。这里我们有必要了解一下这个类的继承结构,不过这个已经在前面文章中梳理过了,有兴趣的话可以看一下《MyBatis是纸老虎吗?(三)》这篇博文。未来方便阅读,这里再贴一下这个结构图:
好了继续回到上面梳理的第六步,这一步中会调用MapperBuilderAssistant类中的addMappedStatement()方法完成mapper中相关sql语句信息的存储,该方法的源码为:
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, boolean dirtySelect) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType)
.keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang)
.resultOrdered(resultOrdered).resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType)
.flushCacheRequired(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
首先看id = applyCurrentNamespace(id, false)这一行代码,它的主要作用就是将前面解析出来的namespace值和mapper中定义的sql语句的id值合并组成一个唯一的数据,比如本节案例中的org.com.chinasofti.springtransaction.UserDao.deleteById。这里有一点需要注意以下applyCurrentNamespace()方法中的currentNamespace的值来源于XMLMapperBuilder中configurationElement ()方法的builderAssistant.setCurrentNamespace()一句。接着创建MappedStatement.Builder对象(这里是一个标准的建造者模式),该对象持有了mapper文件中配置sql语句时设置的所有信息。然后调用本类中的getStatementparameterMap()方法获得一个ParameterMap对象,如果这个对象不为空,则将其设置到MappedStatement.Builder对象的parameterMap属性上。最后调用MappedStatement.Builder对象上的builder()方法创建一个MappedStatement对象并将这个对象添加到Configuration对象的mappedStatement属性上,这是一个Map<String, MappedStatement>类型的map对象。下面看一下MappedStatement对象的详细信息,如下图所示:
由此不难看出MappedStatement存储了sql语句中的所有相关信息,比如sql语句的id、sql语句类型(参见statementType)、参数及响应结果(分别参见parameterMap和resultMaps)、sql语句(参见sqlSource)等等。另外最终解析出来的这个MappedStatement对象被存储到Configuration对象的mappedStatement属性中。关于MappedStatement类的源码这里就不在罗列了,有兴趣的可以翻阅一下MyBatis源码。
下面让我们回到XMLMapperBuilder类的parse()方法中,继续看if分支中的最后一行代码,这里会调用bindMapperForNamespace()方法,该方法的源码如下所示:
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)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
再开始介绍该方法前,先来看一下configurationElement()方法执行后Configuration对象的变化,这里主要关注的是Configuration对象中的mappedStatements属性(该属性存储了所有解析出来的sql语句的详细信息),具体如下图所示:
注意这个属性中会将每个sql语句存储两次,一个是以id名为key,一个是以namespace+id为key。为什么这里会注册两个呢?这是因为mappedStatements的实际类型为StrictMap,这是一个继承了ConcurrentHashMap的类,其源码如下所示:
class StrictMap<V> extends ConcurrentHashMap<String, V> {
private static final long serialVersionUID = -4950446264854982944L;
private final String name;
private BiFunction<V, V, String> conflictMessageProducer;
public StrictMap(String name, int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
this.name = name;
}
public StrictMap(String name, int initialCapacity) {
super(initialCapacity);
this.name = name;
}
public StrictMap(String name) {
this.name = name;
}
public StrictMap(String name, Map<String, ? extends V> m) {
super(m);
this.name = name;
}
/**
* Assign a function for producing a conflict error message when contains value with the same key.
* <p>
* function arguments are 1st is saved value and 2nd is target value.
*
* @param conflictMessageProducer
* A function for producing a conflict error message
*
* @return a conflict error message
*
* @since 3.5.0
*/
public StrictMap<V> conflictMessageProducer(BiFunction<V, V, String> conflictMessageProducer) {
this.conflictMessageProducer = conflictMessageProducer;
return this;
}
@Override
@SuppressWarnings("unchecked")
public V put(String key, V value) {
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains key " + key
+ (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
}
if (key.contains(".")) {
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
return super.put(key, value);
}
@Override
public boolean containsKey(Object key) {
if (key == null) {
return false;
}
return super.get(key) != null;
}
@Override
public V get(Object key) {
V value = super.get(key);
if (value == null) {
throw new IllegalArgumentException(name + " does not contain value for " + key);
}
if (value instanceof Ambiguity) {
throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
+ " (try using the full name including the namespace, or rename one of the entries)");
}
return value;
}
protected static class Ambiguity {
private final String subject;
public Ambiguity(String subject) {
this.subject = subject;
}
public String getSubject() {
return subject;
}
}
private String getShortName(String key) {
final String[] keyParts = key.split("\\.");
return keyParts[keyParts.length - 1];
}
}
其泛型类型为MappedStatement,调用该Map类的put方法时,会解析出sql的id值(短id值),然后执行两次put,一次是短key,一次是namespace+key。
下面让我们继续看bindMapperForNamespace()方法的处理逻辑:该方法会拿到MapperBuilderAssistant对象上的命名空间。接着判断该命名空间是否为空,如果不为空,则用Resources加载类,并将结果赋值给Class<?>类型的boundType变量;如果为空则直接结束。紧接着判断boundType变量是否为空,以及Configuration对象是否包含boundType,这个判断最终用的是MapperRegistry对象上的hasMapper()方法判断的,这里我们拿到的数据是org.com.chinasofti.springtransaction.UserDao,并且经过Configuration对象的判断后,可以执行if分支,所以接下来会调用Configuration对象上的addLoadedResource()方法将namespace: org.com.chinasofti.springtransaction.UserDao存放到Configuration对象中的loadedResources属性中(注意此时该属性中已经有user.xml了)。接着继续调用Configuration对象上的addMapper()方法,该方法接收一个Class<?>类型的参数,最终该方法会走到MapperRegistry类中的addMapper(Class<T>)方法中,该方法的源码如下所示:
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 {
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
从源码可知,该方法会首先判断传递进来的type是否是一个接口,如果是则继续,否则不做任何处理。然后再次调用MapperRegistry类中的hasMapper()方法进行判断,看看该对象上的knownMappers属性中是否存在这个接口,如果存在则直接抛出异常,否则继续。接着首先定义一个loadCompleted对象,赋值为false,然后向knownMappers中存放数据,其中key为代表接口的Class<?>对象,value为MapperProxyFactory对象,该对象持有一个Class<?>类型的数据。接下来创建MapperAnnotationBuilder对象,该对象持有一个Configuration对象和一个Class<?>对象,然后调用MapperAnnotationBuilder对象上的parse()方法。该方法首先拿到type所代表的接口的字符串,然后判断这个数据是否被加载过(就是调用Configuration类中的isResourceLoaded ()方法去判断的,与mapper文件解析时调用的方法是一致的),如果没有被加载过,则执行if分支,先是加载该接口对应的xml资源,即loadXmlResource()方法(注意这个方法会首先判断namespace:+接口全包名是否被加载过,就是调用Configuration对象上的isResourceLoaded()方法进行判断的,注意在调用bindMapperForNamespace()方法时,会向loadedResources中添加一个namespace:+接口全包名,所以调用该方法后,不会执行这个方法的具体加载逻辑),接着获取当前接口的命名空间(同时设置该数据到MapperBuilderAssistant对象上,这个逻辑与解析mapper文件时的逻辑是一样的),解析cache和cacheRef,遍历接口中的所有方法,并进行处理(这里会首先判断这个方法是否是继承过来的,如果不是则继续判断该方法上是否存在Select、SelectProvider及ResultMap注解,如果存在则先解析ResultMap,否则就调用本类中的parseStatement()方法去解析sql语句,与xml解析不同的是,这里解析的是注解)。最后调用本类中的parsePendingMethods()方法解析那些没有解析完成的方法。如果感兴趣,可以看一下MapperAnnotationBuilder的源码。
3 总结
很幸运,经过繁杂啰嗦的讨论,我们终于可以腾出脑子梳理一下了。在本篇文章中,我们着重梳理了mappers元素的解析过程,以及mapper元素中的子元素mapper所表示的sql配置文件的解析流程。其实这篇文章的重点就是mapper文件的解析,而这个重点中的重点就是sql命令的解析(在MyBatis中mapper文件中的sql命令是通过MapperStatement类来表示的)。下面就本篇的知识点做个简单的梳理:
- 通过这篇文章我们知道mapper文件中的sql命令在java中是通过MapperStatement类来表示的,在mapper中,我们可以配置的元素有很多,最常见的是resultMap、insert、delete、update、select及sql等
- 通过这篇文章我们知道解析mapper文件的核心类是XMLMapperBuilder,这个类中的configurationElement ()方法执行具体的解析逻辑。mapper元素的解析入口位于XMLMapperBuilder类的parse()方法中。具体调用路径是这样的:XMLConfigBuilder类的mappersElement()方法【用于解析MyBatis配置文件中的mappers元素】,接着该方法调用XMLMapperBuilder类中的parse()方法【该方法先调用本例的configurationElement()方法】
- 通过这篇文章我们知道解析mapper中sql命令的核心类是XMLStatementBuilder,该类中的parseStatementNode()方法是执行解析逻辑的核心。其调用路径为:XMLMapperBuilder#buildStatementFromContext()->XMLMapperBuilder#buildStateemntFromContext()->XMLStatementBuilder#parseStatementNode()
- 通过这篇文章我们还知道MapperAnnotationBuilder类也可以解析出MapperStatement命令,不过这个解析是基于注解完成的(本篇文章没有过多着墨,有兴趣的可以配置一下,然后跟踪一下代码)
- 还是通过这篇文章我们知道了这些解析出来的MapperStatement对象最终会被存储到Configuration对象中Map<String, MappedStatement>类型的mappedStatements属性中
- 又是通过这篇文章我们知道了xml格式的Mapper文件解析后,会主动触发注解式MapperStatement命令的解析流程,这个入口位于XMLStatementBuilder的parse()方法中,这个方法中的bindMapperForNamespace()方法调用就会触发这个逻辑
- 最终还是通过这篇文章我们知道在MyBatis配置文件中指定sql配置文件的方式有两种:一种是通过package元素,一种是通过mapper元素(注意这个元素中不能同时出现resource、url、class中的任意两个或三个同时出现,也就是说mapper元素上只能出现一个resource或一个url或一个class)