Mybaits 源码解析 (三)----- Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
上一篇我们讲解到mapperelement方法用来解析mapper,我们这篇文章具体来看看mapper.xml的解析过程
mappers配置方式
mappers 标签下有许多 mapper 标签,每一个 mapper 标签中配置的都是一个独立的映射配置文件的路径,配置方式有以下几种。
接口信息进行配置
<mappers> <mapper class="org.mybatis.mappers.usermapper"/> <mapper class="org.mybatis.mappers.productmapper"/> <mapper class="org.mybatis.mappers.managermapper"/> </mappers>
注意:这种方式必须保证接口名(例如usermapper)和xml名(usermapper.xml)相同,还必须在同一个包中。因为是通过获取mapper中的class属性,拼接上.xml来读取usermapper.xml,如果xml文件名不同或者不在同一个包中是无法读取到xml的。
相对路径进行配置
<mappers> <mapper resource="org/mybatis/mappers/usermapper.xml"/> <mapper resource="org/mybatis/mappers/productmapper.xml"/> <mapper resource="org/mybatis/mappers/managermapper.xml"/> </mappers>
注意:这种方式不用保证同接口同包同名。但是要保证xml中的namespase和对应的接口名相同。
绝对路径进行配置
<mappers> <mapper url="file:///var/mappers/usermapper.xml"/> <mapper url="file:///var/mappers/productmapper.xml"/> <mapper url="file:///var/mappers/managermapper.xml"/> </mappers>
接口所在包进行配置
<mappers> <package name="org.mybatis.mappers"/> </mappers>
这种方式和第一种方式要求一致,保证接口名(例如usermapper)和xml名(usermapper.xml)相同,还必须在同一个包中。
注意:以上所有的配置都要保证xml中的namespase和对应的接口名相同。
我们以packae属性为例详细分析一下:
mappers解析入口方法
接上一篇文章最后部分,我们来看看mapperelement方法:
private void mapperelement(xnode parent) throws exception { if (parent != null) { for (xnode child : parent.getchildren()) { //包扫描的形式 if ("package".equals(child.getname())) { // 获取 <package> 节点中的 name 属性 string mapperpackage = child.getstringattribute("name"); // 从指定包中查找 所有的 mapper 接口,并根据 mapper 接口解析映射配置 configuration.addmappers(mapperpackage); } else { // 获取 resource/url/class 等属性 string resource = child.getstringattribute("resource"); string url = child.getstringattribute("url"); string mapperclass = child.getstringattribute("class"); // resource 不为空,且其他两者为空,则从指定路径中加载配置 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(); // url 不为空,且其他两者为空,则通过 url 加载配置 } else if (resource == null && url != null && mapperclass == null) { errorcontext.instance().resource(url); inputstream inputstream = resources.geturlasstream(url); xmlmapperbuilder mapperparser = new xmlmapperbuilder(inputstream, configuration, url, configuration.getsqlfragments()); // 解析映射文件 mapperparser.parse(); // mapperclass 不为空,且其他两者为空,则通过 mapperclass 解析映射配置 } 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."); } } } } }
在 mybatis 中,共有四种加载映射文件或信息的方式。第一种是从文件系统中加载映射文件;第二种是通过 url 的方式加载和解析映射文件;第三种是通过 mapper 接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中。最后一种是通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。
我们先看下以packae扫描的形式,看下configuration.addmappers(mapperpackage)方法
public void addmappers(string packagename) { mapperregistry.addmappers(packagename); }
我们看一下mapperregistry的addmappers方法:
1 public void addmappers(string packagename) { 2 //传入包名和object.class类型 3 this.addmappers(packagename, object.class); 4 } 5 6 public void addmappers(string packagename, class<?> supertype) { 7 resolverutil<class<?>> resolverutil = new resolverutil(); 8 /* 9 * 查找包下的父类为 object.class 的类。 10 * 查找完成后,查找结果将会被缓存到resolverutil的内部集合中。上一篇文章我们已经看过这部分的源码,不再累述了 11 */ 12 resolverutil.find(new isa(supertype), packagename); 13 // 获取查找结果 14 set<class<? extends class<?>>> mapperset = resolverutil.getclasses(); 15 iterator i$ = mapperset.iterator(); 16 17 while(i$.hasnext()) { 18 class<?> mapperclass = (class)i$.next(); 19 //我们具体看这个方法 20 this.addmapper(mapperclass); 21 } 22 23 }
其实就是通过 vfs(虚拟文件系统)获取指定包下的所有文件的class,也就是所有的mapper接口,然后遍历每个mapper接口进行解析,接下来就和第一种配置方式(接口信息进行配置)一样的流程了,接下来我们来看看 基于 xml 的映射文件的解析过程,可以看到先创建一个xmlmapperbuilder,再调用其parse()方法:
1 public void parse() { 2 // 检测映射文件是否已经被解析过 3 if (!configuration.isresourceloaded(resource)) { 4 // 解析 mapper 节点 5 configurationelement(parser.evalnode("/mapper")); 6 // 添加资源路径到“已解析资源集合”中 7 configuration.addloadedresource(resource); 8 // 通过命名空间绑定 mapper 接口 9 bindmapperfornamespace(); 10 } 11 12 parsependingresultmaps(); 13 parsependingcacherefs(); 14 parsependingstatements(); 15 }
我们重点关注第5行和第9行的逻辑,也就是configurationelement和bindmapperfornamespace方法
解析映射文件
在 mybatis 映射文件中,可以配置多种节点。比如 <cache>,<resultmap>,<sql> 以及 <select | insert | update | delete> 等。下面我们来看一个映射文件配置示例。
<?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="mapper.employeemapper"> <cache/> <resultmap id="basemap" type="entity.employee"> <result property="id" column="id" jdbctype="integer"></result> <result property="name" column="name" jdbctype="varchar"></result> </resultmap> <sql id="table"> employee </sql> <select id="getall" resultmap="basemap"> select * from <include refid="table"/> where id = #{id} </select> <!-- <insert|update|delete/> --> </mapper>
接着来看看configurationelement解析mapper.xml中的内容。
1 private void configurationelement(xnode context) { 2 try { 3 // 获取 mapper 命名空间,如 mapper.employeemapper 4 string namespace = context.getstringattribute("namespace"); 5 if (namespace == null || namespace.equals("")) { 6 throw new builderexception("mapper's namespace cannot be empty"); 7 } 8 9 // 设置命名空间到 builderassistant 中 10 builderassistant.setcurrentnamespace(namespace); 11 12 // 解析 <cache-ref> 节点 13 cacherefelement(context.evalnode("cache-ref")); 14 15 // 解析 <cache> 节点 16 cacheelement(context.evalnode("cache")); 17 18 // 已废弃配置,这里不做分析 19 parametermapelement(context.evalnodes("/mapper/parametermap")); 20 21 // 解析 <resultmap> 节点 22 resultmapelements(context.evalnodes("/mapper/resultmap")); 23 24 // 解析 <sql> 节点 25 sqlelement(context.evalnodes("/mapper/sql")); 26 27 // 解析 <select>、<insert>、<update>、<delete> 节点 28 buildstatementfromcontext(context.evalnodes("select|insert|update|delete")); 29 } catch (exception e) { 30 throw new builderexception("error parsing mapper xml. the xml location is '" + resource + "'. cause: " + e, e); 31 } 32 }
接下来我们就对其中关键的方法进行详细分析
解析 cache 节点
mybatis 提供了一、二级缓存,其中一级缓存是 sqlsession 级别的,默认为开启状态。二级缓存配置在映射文件中,使用者需要显示配置才能开启。如下:
<cache/>
也可以使用第三方缓存
<cache type="org.mybatis.caches.redis.rediscache"/>
其中有一些属性可以选择
<cache eviction="lru" flushinterval="60000" size="512" readonly="true"/>
- 根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”
- 缓存的容量为 512 个对象引用
- 缓存每隔60秒刷新一次
- 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象
下面我们来分析一下缓存配置的解析逻辑,如下:
private void cacheelement(xnode context) throws exception { if (context != null) { // 获取type属性,如果type没有指定就用默认的perpetual(早已经注册过的别名的perpetualcache) string type = context.getstringattribute("type", "perpetual"); // 根据type从早已经注册的别名中获取对应的class,perpetual对应的class是perpetualcache.class // 如果我们写了type属性,如type="org.mybatis.caches.redis.rediscache",这里将会得到rediscache.class class<? extends cache> typeclass = typealiasregistry.resolvealias(type); //获取淘汰方式,默认为lru(早已经注册过的别名的lrucache),最近最少使用到的先淘汰 string eviction = context.getstringattribute("eviction", "lru"); class<? extends cache> evictionclass = typealiasregistry.resolvealias(eviction); long flushinterval = context.getlongattribute("flushinterval"); integer size = context.getintattribute("size"); boolean readwrite = !context.getbooleanattribute("readonly", false); boolean blocking = context.getbooleanattribute("blocking", false); // 获取子节点配置 properties props = context.getchildrenasproperties(); // 构建缓存对象 builderassistant.usenewcache(typeclass, evictionclass, flushinterval, size, readwrite, blocking, props); } } public <t> class<t> resolvealias(string string) { try { if (string == null) { return null; } else { // 转换成小写 string key = string.tolowercase(locale.english); class value; // 如果没有设置type属性,则这里传过来的是perpetual,能从别名缓存中获取到perpetualcache.class if (this.type_aliases.containskey(key)) { value = (class)this.type_aliases.get(key); } else { //如果是设置了自定义的type,则在别名缓存中是获取不到的,直接通过类加载,加载自定义的type,如rediscache.class value = resources.classforname(string); } return value; } } catch (classnotfoundexception var4) { throw new typeexception("could not resolve type alias '" + string + "'. cause: " + var4, var4); } }
缓存的构建封装在 builderassistant 类的 usenewcache 方法中,我们来看看
public cache usenewcache(class<? extends cache> typeclass, class<? extends cache> evictionclass,long flushinterval, integer size,boolean readwrite,boolean blocking,properties props) { // 使用建造模式构建缓存实例 cache cache = new cachebuilder(currentnamespace) .implementation(valueordefault(typeclass, perpetualcache.class)) .adddecorator(valueordefault(evictionclass, lrucache.class)) .clearinterval(flushinterval) .size(size) .readwrite(readwrite) .blocking(blocking) .properties(props) .build(); // 添加缓存到 configuration 对象中 configuration.addcache(cache); // 设置 currentcache 属性,即当前使用的缓存 currentcache = cache; return cache; }
上面使用了建造模式构建 cache 实例,我们跟下去看看。
public cache build() { // 设置默认的缓存类型(perpetualcache)和缓存装饰器(lrucache) setdefaultimplementations(); // 通过反射创建缓存 cache cache = newbasecacheinstance(implementation, id); setcacheproperties(cache); // 仅对内置缓存 perpetualcache 应用装饰器 if (perpetualcache.class.equals(cache.getclass())) { // 遍历装饰器集合,应用装饰器 for (class<? extends cache> decorator : decorators) { // 通过反射创建装饰器实例 cache = newcachedecoratorinstance(decorator, cache); // 设置属性值到缓存实例中 setcacheproperties(cache); } // 应用标准的装饰器,比如 loggingcache、synchronizedcache cache = setstandarddecorators(cache); } else if (!loggingcache.class.isassignablefrom(cache.getclass())) { // 应用具有日志功能的缓存装饰器 cache = new loggingcache(cache); } return cache; } private void setdefaultimplementations() { if (this.implementation == null) { //设置默认缓存类型为perpetualcache this.implementation = perpetualcache.class; if (this.decorators.isempty()) { this.decorators.add(lrucache.class); } } } private cache newbasecacheinstance(class<? extends cache> cacheclass, string id) { //获取构造器 constructor cacheconstructor = this.getbasecacheconstructor(cacheclass); try { //通过构造器实例化cache return (cache)cacheconstructor.newinstance(id); } catch (exception var5) { throw new cacheexception("could not instantiate cache implementation (" + cacheclass + "). cause: " + var5, var5); } }
如上就创建好了一个cache的实例,然后把它添加到configuration中,并且设置到currentcache属性中,这个属性后面还要使用,也就是cache实例后面还要使用,我们后面再看。
解析 resultmap 节点
resultmap 主要用于映射结果。通过 resultmap 和自动映射,可以让 mybatis 帮助我们完成 resultset → object 的映射。下面开始分析 resultmap 配置的解析过程。
private void resultmapelements(list<xnode> list) throws exception { // 遍历 <resultmap> 节点列表 for (xnode resultmapnode : list) { try { // 解析 resultmap 节点 resultmapelement(resultmapnode); } catch (incompleteelementexception e) { } } } private resultmap resultmapelement(xnode resultmapnode) throws exception { return resultmapelement(resultmapnode, collections.<resultmapping>emptylist()); } private resultmap resultmapelement(xnode resultmapnode, list<resultmapping> additionalresultmappings) throws exception { errorcontext.instance().activity("processing " + resultmapnode.getvaluebasedidentifier()); // 获取 id 和 type 属性 string id = resultmapnode.getstringattribute("id", resultmapnode.getvaluebasedidentifier()); string type = resultmapnode.getstringattribute("type", resultmapnode.getstringattribute("oftype", resultmapnode.getstringattribute("resulttype", resultmapnode.getstringattribute("javatype")))); // 获取 extends 和 automapping string extend = resultmapnode.getstringattribute("extends"); boolean automapping = resultmapnode.getbooleanattribute("automapping"); // 获取 type 属性对应的类型 class<?> typeclass = resolveclass(type); discriminator discriminator = null; //创建resultmapping集合,对应resultmap子节点的id和result节点 list<resultmapping> resultmappings = new arraylist<resultmapping>(); resultmappings.addall(additionalresultmappings); // 获取并遍历 <resultmap> 的子节点列表 list<xnode> resultchildren = resultmapnode.getchildren(); for (xnode resultchild : resultchildren) { if ("constructor".equals(resultchild.getname())) { processconstructorelement(resultchild, typeclass, resultmappings); } else if ("discriminator".equals(resultchild.getname())) { discriminator = processdiscriminatorelement(resultchild, typeclass, resultmappings); } else { list<resultflag> flags = new arraylist<resultflag>(); if ("id".equals(resultchild.getname())) { // 添加 id 到 flags 集合中 flags.add(resultflag.id); } // 解析 id 和 result 节点,将id或result节点生成相应的 resultmapping,将resultmapping添加到resultmappings集合中 resultmappings.add(buildresultmappingfromcontext(resultchild, typeclass, flags)); } } //创建resultmapresolver对象 resultmapresolver resultmapresolver = new resultmapresolver(builderassistant, id, typeclass, extend, discriminator, resultmappings, automapping); try { // 根据前面获取到的信息构建 resultmap 对象 return resultmapresolver.resolve(); } catch (incompleteelementexception e) { configuration.addincompleteresultmap(resultmapresolver); throw e; } }
解析 id 和 result 节点
在 <resultmap> 节点中,子节点 <id> 和 <result> 都是常规配置,比较常见。我们来看看解析过程
private resultmapping buildresultmappingfromcontext(xnode context, class<?> resulttype, list<resultflag> flags) throws exception { string property; // 根据节点类型获取 name 或 property 属性 if (flags.contains(resultflag.constructor)) { property = context.getstringattribute("name"); } else { property = context.getstringattribute("property"); } // 获取其他各种属性 string column = context.getstringattribute("column"); string javatype = context.getstringattribute("javatype"); string jdbctype = context.getstringattribute("jdbctype"); string nestedselect = context.getstringattribute("select"); /* * 解析 resultmap 属性,该属性出现在 <association> 和 <collection> 节点中。 * 若这两个节点不包含 resultmap 属性,则调用 processnestedresultmappings 方法,递归调用resultmapelement解析<association> 和 <collection>的嵌套节点,生成resultmap,并返回resultmap.getid(); * 如果包含resultmap属性,则直接获取其属性值,这个属性值对应一个resultmap节点 */ string nestedresultmap = context.getstringattribute("resultmap", processnestedresultmappings(context, collections.<resultmapping>emptylist())); string notnullcolumn = context.getstringattribute("notnullcolumn"); string columnprefix = context.getstringattribute("columnprefix"); string typehandler = context.getstringattribute("typehandler"); string resultset = context.getstringattribute("resultset"); string foreigncolumn = context.getstringattribute("foreigncolumn"); boolean lazy = "lazy".equals(context.getstringattribute("fetchtype", configuration.islazyloadingenabled() ? "lazy" : "eager")); class<?> javatypeclass = resolveclass(javatype); class<? extends typehandler<?>> typehandlerclass = (class<? extends typehandler<?>>) resolveclass(typehandler); jdbctype jdbctypeenum = resolvejdbctype(jdbctype); // 构建 resultmapping 对象 return builderassistant.buildresultmapping(resulttype, property, column, javatypeclass, jdbctypeenum, nestedselect, nestedresultmap, notnullcolumn, columnprefix, typehandlerclass, flags, resultset, foreigncolumn, lazy); }
看processnestedresultmappings解析<association> 和 <collection> 节点中的子节点,并返回resultmap.id
private string processnestedresultmappings(xnode context, list<resultmapping> resultmappings) throws exception { if (("association".equals(context.getname()) || "collection".equals(context.getname()) || "case".equals(context.getname())) && context.getstringattribute("select") == null) { resultmap resultmap = this.resultmapelement(context, resultmappings); return resultmap.getid(); } else { return null; } }
下面以 <association> 节点为例,演示该节点的两种配置方式,分别如下:
第一种配置方式是通过 resultmap 属性引用其他的 <resultmap> 节点,配置如下:
<resultmap id="articleresult" type="article"> <id property="id" column="id"/> <result property="title" column="article_title"/> <!-- 引用 authorresult --> <association property="article_author" column="article_author_id" javatype="author" resultmap="authorresult"/> </resultmap> <resultmap id="authorresult" type="author"> <id property="id" column="author_id"/> <result property="name" column="author_name"/> </resultmap>
第二种配置方式是采取 resultmap 嵌套的方式进行配置,如下:
<resultmap id="articleresult" type="article"> <id property="id" column="id"/> <result property="title" column="article_title"/> <!-- resultmap 嵌套 --> <association property="article_author" javatype="author"> <id property="id" column="author_id"/> <result property="name" column="author_name"/> </association> </resultmap>
第二种配置,<association> 的子节点是一些结果映射配置,这些结果配置最终也会被解析成 resultmap。
下面分析 resultmapping 的构建过程。
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) { // resulttype:即 <resultmap type="xxx"/> 中的 type 属性 // property:即 <result property="xxx"/> 中的 property 属性 class<?> javatypeclass = resolveresultjavatype(resulttype, property, javatype); typehandler<?> typehandlerinstance = resolvetypehandler(javatypeclass, typehandler); list<resultmapping> composites = parsecompositecolumnname(column); // 通过建造模式构建 resultmapping 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<resultflag>() : flags) .composites(composites) .notnullcolumns(parsemultiplecolumnnames(notnullcolumn)) .columnprefix(columnprefix) .foreigncolumn(foreigncolumn) .lazy(lazy) .build(); } private class<?> resolveresultjavatype(class<?> resulttype, string property, class<?> javatype) { if (javatype == null && property != null) { try { //获取resultmap中的type属性的元类,如<resultmap id="user" type="java.model.user"/> 中user的元类 metaclass metaresulttype = metaclass.forclass(resulttype, this.configuration.getreflectorfactory()); //<result property="name" javatype="string"/>,如果result中没有设置javatype,则获取元类属性对那个的类型 javatype = metaresulttype.getsettertype(property); } catch (exception var5) { ; } } if (javatype == null) { javatype = object.class; } return javatype; } public resultmapping build() { resultmapping.flags = collections.unmodifiablelist(resultmapping.flags); resultmapping.composites = collections.unmodifiablelist(resultmapping.composites); resolvetypehandler(); validate(); return resultmapping; }
我们来看看resultmapping类
public class resultmapping { private configuration configuration; private string property; private string column; private class<?> javatype; private jdbctype jdbctype; private typehandler<?> typehandler; private string nestedresultmapid; private string nestedqueryid; private set<string> notnullcolumns; private string columnprefix; private list<resultflag> flags; private list<resultmapping> composites; private string resultset; private string foreigncolumn; private boolean lazy; resultmapping() { } //略 }
resultmapping就是和resultmap中子节点id和result对应
<id column="wi_id" jdbctype="integer" property="id" /> <result column="warrant_no" jdbctype="string" jdbctype="char" property="warrantno" />
resultmap 对象构建
前面的分析我们知道了<id>,<result> 等节点最终都被解析成了 resultmapping。并且封装到了resultmappings集合中,紧接着要做的事情是构建 resultmap,关键代码在resultmapresolver.resolve():
public resultmap resolve() { return assistant.addresultmap(this.id, this.type, this.extend, this.discriminator, this.resultmappings, this.automapping); } public resultmap addresultmap( string id, class<?> type, string extend, discriminator discriminator, list<resultmapping> resultmappings, boolean automapping) { // 为 resultmap 的 id 和 extend 属性值拼接命名空间 id = applycurrentnamespace(id, false); extend = applycurrentnamespace(extend, true); if (extend != null) { if (!configuration.hasresultmap(extend)) { throw new incompleteelementexception("could not find a parent resultmap with id '" + extend + "'"); } resultmap resultmap = configuration.getresultmap(extend); list<resultmapping> extendedresultmappings = new arraylist<resultmapping>(resultmap.getresultmappings()); extendedresultmappings.removeall(resultmappings); boolean declaresconstructor = false; for (resultmapping resultmapping : resultmappings) { if (resultmapping.getflags().contains(resultflag.constructor)) { declaresconstructor = true; break; } } if (declaresconstructor) { iterator<resultmapping> extendedresultmappingsiter = extendedresultmappings.iterator(); while (extendedresultmappingsiter.hasnext()) { if (extendedresultmappingsiter.next().getflags().contains(resultflag.constructor)) { extendedresultmappingsiter.remove(); } } } resultmappings.addall(extendedresultmappings); } // 构建 resultmap resultmap resultmap = new resultmap.builder(configuration, id, type, resultmappings, automapping) .discriminator(discriminator) .build(); // 将创建好的resultmap加入configuration中 configuration.addresultmap(resultmap); return resultmap; }
我们先看看resultmap
public class resultmap { private string id; private class<?> type; private list<resultmapping> resultmappings; //用于存储 <id> 节点对应的 resultmapping 对象 private list<resultmapping> idresultmappings; private list<resultmapping> constructorresultmappings; //用于存储 <id> 和 <result> 节点对应的 resultmapping 对象 private list<resultmapping> propertyresultmappings; //用于存储 所有<id>、<result> 节点 column 属性 private set<string> mappedcolumns; private discriminator discriminator; private boolean hasnestedresultmaps; private boolean hasnestedqueries; private boolean automapping; private resultmap() { } //略 }
再来看看通过建造模式构建 resultmap 实例
public resultmap build() { if (resultmap.id == null) { throw new illegalargumentexception("resultmaps must have an id"); } resultmap.mappedcolumns = new hashset<string>(); resultmap.mappedproperties = new hashset<string>(); resultmap.idresultmappings = new arraylist<resultmapping>(); resultmap.constructorresultmappings = new arraylist<resultmapping>(); resultmap.propertyresultmappings = new arraylist<resultmapping>(); final list<string> constructorargnames = new arraylist<string>(); for (resultmapping resultmapping : resultmap.resultmappings) { /* * 检测 <association> 或 <collection> 节点 * 是否包含 select 和 resultmap 属性 */ resultmap.hasnestedqueries = resultmap.hasnestedqueries || resultmapping.getnestedqueryid() != null; resultmap.hasnestedresultmaps = resultmap.hasnestedresultmaps || (resultmapping.getnestedresultmapid() != null && resultmapping.getresultset() == null); final string column = resultmapping.getcolumn(); if (column != null) { // 将 colum 转换成大写,并添加到 mappedcolumns 集合中 resultmap.mappedcolumns.add(column.touppercase(locale.english)); } else if (resultmapping.iscompositeresult()) { for (resultmapping compositeresultmapping : resultmapping.getcomposites()) { final string compositecolumn = compositeresultmapping.getcolumn(); if (compositecolumn != null) { resultmap.mappedcolumns.add(compositecolumn.touppercase(locale.english)); } } } // 添加属性 property 到 mappedproperties 集合中 final string property = resultmapping.getproperty(); if (property != null) { resultmap.mappedproperties.add(property); } if (resultmapping.getflags().contains(resultflag.constructor)) { resultmap.constructorresultmappings.add(resultmapping); if (resultmapping.getproperty() != null) { constructorargnames.add(resultmapping.getproperty()); } } else { // 添加 resultmapping 到 propertyresultmappings 中 resultmap.propertyresultmappings.add(resultmapping); } if (resultmapping.getflags().contains(resultflag.id)) { // 添加 resultmapping 到 idresultmappings 中 resultmap.idresultmappings.add(resultmapping); } } if (resultmap.idresultmappings.isempty()) { resultmap.idresultmappings.addall(resultmap.resultmappings); } if (!constructorargnames.isempty()) { final list<string> actualargnames = argnamesofmatchingconstructor(constructorargnames); if (actualargnames == null) { throw new builderexception("error in result map '" + resultmap.id + "'. failed to find a constructor in '" + resultmap.gettype().getname() + "' by arg names " + constructorargnames + ". there might be more info in debug log."); } collections.sort(resultmap.constructorresultmappings, new comparator<resultmapping>() { @override public int compare(resultmapping o1, resultmapping o2) { int paramidx1 = actualargnames.indexof(o1.getproperty()); int paramidx2 = actualargnames.indexof(o2.getproperty()); return paramidx1 - paramidx2; } }); } // 将以下这些集合变为不可修改集合 resultmap.resultmappings = collections.unmodifiablelist(resultmap.resultmappings); resultmap.idresultmappings = collections.unmodifiablelist(resultmap.idresultmappings); resultmap.constructorresultmappings = collections.unmodifiablelist(resultmap.constructorresultmappings); resultmap.propertyresultmappings = collections.unmodifiablelist(resultmap.propertyresultmappings); resultmap.mappedcolumns = collections.unmodifiableset(resultmap.mappedcolumns); return resultmap; }
主要做的事情就是将 resultmapping 实例及属性分别存储到不同的集合中。
解析 sql 节点
<sql> 节点用来定义一些可重用的 sql 语句片段,比如表名,或表的列名等。在映射文件中,我们可以通过 <include> 节点引用 <sql> 节点定义的内容。
<sql id="table"> user </sql> <select id="findone" resulttype="article"> select * from <include refid="table"/> where id = #{id} </select>
下面分析一下 sql 节点的解析过程,如下:
private void sqlelement(list<xnode> list) throws exception { if (configuration.getdatabaseid() != null) { // 调用 sqlelement 解析 <sql> 节点 sqlelement(list, configuration.getdatabaseid()); } // 再次调用 sqlelement,不同的是,这次调用,该方法的第二个参数为 null sqlelement(list, null); } private void sqlelement(list<xnode> list, string requireddatabaseid) throws exception { for (xnode context : list) { // 获取 id 和 databaseid 属性 string databaseid = context.getstringattribute("databaseid"); string id = context.getstringattribute("id"); // id = currentnamespace + "." + id id = builderassistant.applycurrentnamespace(id, false); // 检测当前 databaseid 和 requireddatabaseid 是否一致 if (databaseidmatchescurrent(id, databaseid, requireddatabaseid)) { // 将 <id, xnode> 键值对缓存到xmlmapperbuilder对象的 sqlfragments 属性中,以供后面的sql语句使用 sqlfragments.put(id, context); } } }
解析select|insert|update|delete节点
<select>、<insert>、<update> 以及 <delete> 等节点统称为 sql 语句节点,其解析过程在buildstatementfromcontext方法中:
private void buildstatementfromcontext(list<xnode> list) { if (configuration.getdatabaseid() != null) { // 调用重载方法构建 statement buildstatementfromcontext(list, configuration.getdatabaseid()); } buildstatementfromcontext(list, null); } private void buildstatementfromcontext(list<xnode> list, string requireddatabaseid) { for (xnode context : list) { // 创建 xmlstatementbuilder 建造类 final xmlstatementbuilder statementparser = new xmlstatementbuilder(configuration, builderassistant, context, requireddatabaseid); try { /* * 解析sql节点,将其封装到 statement 对象中,并将解析结果存储到 configuration 的 mappedstatements 集合中 */ statementparser.parsestatementnode(); } catch (incompleteelementexception e) { configuration.addincompletestatement(statementparser); } } }
我们继续看 statementparser.parsestatementnode();
public void parsestatementnode() { // 获取 id 和 databaseid 属性 string id = context.getstringattribute("id"); string databaseid = context.getstringattribute("databaseid"); if (!databaseidmatchescurrent(id, databaseid, this.requireddatabaseid)) { return; } // 获取各种属性 integer fetchsize = context.getintattribute("fetchsize"); integer timeout = context.getintattribute("timeout"); string parametermap = context.getstringattribute("parametermap"); string parametertype = context.getstringattribute("parametertype"); class<?> parametertypeclass = resolveclass(parametertype); string resultmap = context.getstringattribute("resultmap"); string resulttype = context.getstringattribute("resulttype"); string lang = context.getstringattribute("lang"); languagedriver langdriver = getlanguagedriver(lang); // 通过别名解析 resulttype 对应的类型 class<?> resulttypeclass = resolveclass(resulttype); string resultsettype = context.getstringattribute("resultsettype"); // 解析 statement 类型,默认为 prepared statementtype statementtype = statementtype.valueof(context.getstringattribute("statementtype", statementtype.prepared.tostring())); // 解析 resultsettype resultsettype resultsettypeenum = resolveresultsettype(resultsettype); // 获取节点的名称,比如 <select> 节点名称为 select string nodename = context.getnode().getnodename(); // 根据节点名称解析 sqlcommandtype 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> 节点 xmlincludetransformer includeparser = new xmlincludetransformer(configuration, builderassistant); includeparser.applyincludes(context.getnode()); processselectkeynodes(id, parametertypeclass, langdriver); // 解析 sql 语句 sqlsource sqlsource = langdriver.createsqlsource(configuration, context, parametertypeclass); string resultsets = context.getstringattribute("resultsets"); string keyproperty = context.getstringattribute("keyproperty"); string keycolumn = context.getstringattribute("keycolumn"); 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; } /* * 构建 mappedstatement 对象,并将该对象存储到 configuration 的 mappedstatements 集合中 */ builderassistant.addmappedstatement(id, sqlsource, statementtype, sqlcommandtype, fetchsize, timeout, parametermap, parametertypeclass, resultmap, resulttypeclass, resultsettypeenum, flushcache, usecache, resultordered, keygenerator, keyproperty, keycolumn, databaseid, langdriver, resultsets); }
我们主要来分析下面几个重要的方法:
- 解析 <include> 节点
- 解析 sql,获取 sqlsource
- 构建 mappedstatement 实例
解析 <include> 节点
先来看一个include的例子
<mapper namespace="java.mybaits.dao.usermapper"> <sql id="table"> user </sql> <select id="findone" resulttype="user"> select * from <include refid="table"/> where id = #{id} </select> </mapper>
<include> 节点的解析逻辑封装在 applyincludes 中,该方法的代码如下:
public void applyincludes(node source) { properties variablescontext = new properties(); properties configurationvariables = configuration.getvariables(); if (configurationvariables != null) { // 将 configurationvariables 中的数据添加到 variablescontext 中 variablescontext.putall(configurationvariables); } // 调用重载方法处理 <include> 节点 applyincludes(source, variablescontext, false); }
继续看 applyincludes 方法
private void applyincludes(node source, final properties variablescontext, boolean included) { // 第一个条件分支 if (source.getnodename().equals("include")) { //获取 <sql> 节点。 node toinclude = findsqlfragment(getstringattribute(source, "refid"), variablescontext); properties toincludecontext = getvariablescontext(source, variablescontext); applyincludes(toinclude, toincludecontext, true); if (toinclude.getownerdocument() != source.getownerdocument()) { toinclude = source.getownerdocument().importnode(toinclude, true); } // 将 <select>节点中的 <include> 节点替换为 <sql> 节点 source.getparentnode().replacechild(toinclude, source); while (toinclude.haschildnodes()) { // 将 <sql> 中的内容插入到 <sql> 节点之前 toinclude.getparentnode().insertbefore(toinclude.getfirstchild(), toinclude); } /* * 前面已经将 <sql> 节点的内容插入到 dom 中了, * 现在不需要 <sql> 节点了,这里将该节点从 dom 中移除 */ toinclude.getparentnode().removechild(toinclude); // 第二个条件分支 } else if (source.getnodetype() == node.element_node) { if (included && !variablescontext.isempty()) { namednodemap attributes = source.getattributes(); for (int i = 0; i < attributes.getlength(); i++) { node attr = attributes.item(i); // 将 source 节点属性中的占位符 ${} 替换成具体的属性值 attr.setnodevalue(propertyparser.parse(attr.getnodevalue(), variablescontext)); } } nodelist children = source.getchildnodes(); for (int i = 0; i < children.getlength(); i++) { // 递归调用 applyincludes(children.item(i), variablescontext, included); } // 第三个条件分支 } else if (included && source.getnodetype() == node.text_node && !variablescontext.isempty()) { // 将文本(text)节点中的属性占位符 ${} 替换成具体的属性值 source.setnodevalue(propertyparser.parse(source.getnodevalue(), variablescontext)); } }
我们先来看一下 applyincludes 方法第一次被调用时的状态,source为<select> 节点,节点类型:element_node,此时会进入第二个分支,获取到获取 <select> 子节点列表,遍历子节点列表,将子节点作为参数,进行递归调用applyincludes ,此时可获取到的子节点如下:
编号 | 子节点 | 类型 | 描述 |
---|---|---|---|
1 | select * from | text_node | 文本节点 |
2 | <include refid="table"/> | element_node | 普通节点 |
3 | where id = #{id} | text_node | 文本节点 |
接下来要做的事情是遍历列表,然后将子节点作为参数进行递归调用。第一个子节点调用applyincludes方法,source为 select * from 节点,节点类型:text_node,进入分支三,没有${},不会替换,则节点一结束返回,什么都没有做。第二个节点调用applyincludes方法,此时source为 <include refid="table"/>节点,节点类型:element_node,进入分支一,通过refid找到 sql 节点,也就是toinclude节点,然后执行source.getparentnode().replacechild(toinclude, source);,直接将<include refid="table"/>节点的父节点,也就是<select> 节点中的当前<include >节点替换成 <sql> 节点,然后调用toinclude.getparentnode().insertbefore(toinclude.getfirstchild(), toinclude);,将 <sql> 中的内容插入到 <sql> 节点之前,也就是将user插入到 <sql> 节点之前,现在不需要 <sql> 节点了,最后将该节点从 dom 中移除
创建sqlsource
创建sqlsource在createsqlsource方法中
public sqlsource createsqlsource(configuration configuration, xnode script, class<?> parametertype) { xmlscriptbuilder builder = new xmlscriptbuilder(configuration, script, parametertype); return builder.parsescriptnode(); } // -☆- xmlscriptbuilder public sqlsource parsescriptnode() { // 解析 sql 语句节点 mixedsqlnode rootsqlnode = parsedynamictags(context); sqlsource sqlsource = null; // 根据 isdynamic 状态创建不同的 sqlsource if (isdynamic) { sqlsource = new dynamicsqlsource(configuration, rootsqlnode); } else { sqlsource = new rawsqlsource(configuration, rootsqlnode, parametertype); } return sqlsource; }
继续跟进parsedynamictags
/** 该方法用于初始化 nodehandlermap 集合,该集合后面会用到 */ private void initnodehandlermap() { nodehandlermap.put("trim", new trimhandler()); nodehandlermap.put("where", new wherehandler()); nodehandlermap.put("set", new sethandler()); nodehandlermap.put("foreach", new foreachhandler()); nodehandlermap.put("if", new ifhandler()); nodehandlermap.put("choose", new choosehandler()); nodehandlermap.put("when", new ifhandler()); nodehandlermap.put("otherwise", new otherwisehandler()); nodehandlermap.put("bind", new bindhandler()); } protected mixedsqlnode parsedynamictags(xnode node) { list<sqlnode> contents = new arraylist<sqlnode>(); nodelist children = node.getnode().getchildnodes(); // 遍历子节点 for (int i = 0; i < children.getlength(); i++) { xnode child = node.newxnode(children.item(i)); //如果节点是text_node类型 if (child.getnode().getnodetype() == node.cdata_section_node || child.getnode().getnodetype() == node.text_node) { // 获取文本内容 string data = child.getstringbody(""); textsqlnode textsqlnode = new textsqlnode(data); // 若文本中包含 ${} 占位符,会被认为是动态节点 if (textsqlnode.isdynamic()) { contents.add(textsqlnode); // 设置 isdynamic 为 true isdynamic = true; } else { // 创建 statictextsqlnode contents.add(new statictextsqlnode(data)); } // child 节点是 element_node 类型,比如 <if>、<where> 等 } else if (child.getnode().getnodetype() == node.element_node) { // 获取节点名称,比如 if、where、trim 等 string nodename = child.getnode().getnodename(); // 根据节点名称获取 nodehandler,也就是上面注册的nodehandlermap nodehandler handler = nodehandlermap.get(nodename); if (handler == null) { throw new builderexception("unknown element <" + nodename + "> in sql statement."); } // 处理 child 节点,生成相应的 sqlnode handler.handlenode(child, contents); // 设置 isdynamic 为 true isdynamic = true; } } return new mixedsqlnode(contents); }
对于if、trim、where等这些动态节点,是通过对应的handler来解析的,如下
handler.handlenode(child, contents);
该代码用于处理动态 sql 节点,并生成相应的 sqlnode。下面来简单分析一下 wherehandler 的代码。
/** 定义在 xmlscriptbuilder 中 */ private class wherehandler implements nodehandler { public wherehandler() { } @override public void handlenode(xnode nodetohandle, list<sqlnode> targetcontents) { // 调用 parsedynamictags 解析 <where> 节点 mixedsqlnode mixedsqlnode = parsedynamictags(nodetohandle); // 创建 wheresqlnode wheresqlnode where = new wheresqlnode(configuration, mixedsqlnode); // 添加到 targetcontents targetcontents.add(where); } }
我们已经将 xml 配置解析了 sqlsource,下面我们看看mappedstatement的构建。
构建mappedstatement
sql 语句节点可以定义很多属性,这些属性和属性值最终存储在 mappedstatement 中。
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) { if (unresolvedcacheref) { throw new incompleteelementexception("cache-ref not yet resolved"); } // 拼接上命名空间,如 <select id="findone" resulttype="user">,则id=java.mybaits.dao.usermapper.findone id = applycurrentnamespace(id, false); boolean isselect = sqlcommandtype == sqlcommandtype.select; // 创建建造器,设置各种属性 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)) .flushcacherequired(valueordefault(flushcache, !isselect)) .resultsettype(resultsettype).usecache(valueordefault(usecache, isselect)) .cache(currentcache);//这里用到了前面解析<cache>节点时创建的cache对象,设置到mappedstatement对象里面的cache属性中 // 获取或创建 parametermap parametermap statementparametermap = getstatementparametermap(parametermap, parametertype, id); if (statementparametermap != null) { statementbuilder.parametermap(statementparametermap); } // 构建 mappedstatement mappedstatement statement = statementbuilder.build(); // 添加 mappedstatement 到 configuration 的 mappedstatements 集合中 // 通过usermapper代理对象调用findone方法时,就可以拼接usermapper接口名java.mybaits.dao.usermapper和findone方法找到id=java.mybaits.dao.usermapper的mappedstatement,然后执行对应的sql语句 configuration.addmappedstatement(statement); return statement; }
这里我们要注意,mappedstatement对象中有一个cache属性,将前面解析<cache>节点时创建的cache对象,设置到mappedstatement对象里面的cache属性中,以备后面二级缓存使用,我们后面专门来讲这一块。
mapper 接口绑定
映射文件解析完成后,我们需要通过命名空间将绑定 mapper 接口,看看具体绑定的啥
private void bindmapperfornamespace() { // 获取映射文件的命名空间 string namespace = builderassistant.getcurrentnamespace(); if (namespace != null) { class<?> boundtype = null; try { // 根据命名空间解析 mapper 类型 boundtype = resources.classforname(namespace); } catch (classnotfoundexception e) { } if (boundtype != null) { // 检测当前 mapper 类是否被绑定过 if (!configuration.hasmapper(boundtype)) { configuration.addloadedresource("namespace:" + namespace); // 绑定 mapper 类 configuration.addmapper(boundtype); } } } } // configuration public <t> void addmapper(class<t> type) { // 通过 mapperregistry 绑定 mapper 类 mapperregistry.addmapper(type); } // mapperregistry 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 { /* * 将 type 和 mapperproxyfactory 进行绑定,mapperproxyfactory 可为 mapper 接口生成代理类 */ knownmappers.put(type, new mapperproxyfactory<t>(type)); mapperannotationbuilder parser = new mapperannotationbuilder(config, type); // 解析注解中的信息 parser.parse(); loadcompleted = true; } finally { if (!loadcompleted) { knownmappers.remove(type); } } } }
其实就是获取当前映射文件的命名空间,并获取其class,也就是获取每个mapper接口,然后为每个mapper接口创建一个代理类工厂,new mapperproxyfactory<t>(type),并放进 knownmappers 这个hashmap中,我们来看看这个mapperproxyfactory
public class mapperproxyfactory<t> { //存放mapper接口class private final class<t> mapperinterface; private final map<method, mappermethod> methodcache = new concurrenthashmap(); public mapperproxyfactory(class<t> mapperinterface) { this.mapperinterface = mapperinterface; } public class<t> getmapperinterface() { return this.mapperinterface; } public map<method, mappermethod> getmethodcache() { return this.methodcache; } protected t newinstance(mapperproxy<t> mapperproxy) { //生成mapperinterface的代理类 return proxy.newproxyinstance(this.mapperinterface.getclassloader(), new class[]{this.mapperinterface}, mapperproxy); } public t newinstance(sqlsession sqlsession) { mapperproxy<t> mapperproxy = new mapperproxy(sqlsession, this.mapperinterface, this.methodcache); return this.newinstance(mapperproxy); } }
这一块我们后面文章再来看是如何调用的。
上一篇: PHP小补充