本文主要是介绍MyBatis是纸老虎吗?(五),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
最近看到这样一则消息《全球首位AI工程师诞生,“码农”未来会消失?》,文章提到百度董事长兼首席执行官李彦宏认为未来将不会存在“程序员”这种职业。行业大佬的这种说法,让我异常恐慌。难道程序员就这样被淘汰了?AI真的要打败创造它的造物主吗?在这个急速发展的社会中,从事程序员工作的我们究竟该怎么办呢?或许《MyBatis是纸老虎吗?》系列文章会为我们带来一些不一样的答案。
在《MyBatis是纸老虎吗?(四)》这篇文章中我们一起学习了MyBatis配置文件中的plugins元素,梳理了该元素的解析过程。通过这篇文章我们知道什么是MyBatis拦截器,了解了该控件基本用法,学会了自定义该控件的方法。今天我将继续学习MyBatis框架。我希望通过这篇文章捋清MyBatis配置文件中的mappers元素。
1 mappers元素的定义
大家都知道使用MyBatis框架时,需要定义一个配置文件(在这个系列文章中我们也一直强调这个文件),而mappers元素就是定义在这个文件中的。先来看下面这样一段代码:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//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及deletebuildStatementFromContext(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语句解析为Statementfinal 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 parsingXMLIncludeTransformer 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#loadXmlResourceconfiguration.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);}@Overridepublic boolean containsKey(Object key) {if (key == null) {return false;}return super.get(key) != null;}@Overridepublic 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)
这篇关于MyBatis是纸老虎吗?(五)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!