Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
刚开始使用mybaits的同学有没有这样的疑惑,为什么我们没有编写mapper的实现类,却能调用mapper的方法呢?本篇文章我带大家一起来解决这个疑问
上一篇文章我们获取到了defaultsqlsession,接着我们来看第一篇文章测试用例后面的代码
employeemapper employeemapper = sqlsession.getmapper(employee.class); list<employee> allemployees = employeemapper.getall();
为 mapper 接口创建代理对象
我们先从 defaultsqlsession 的 getmapper 方法开始看起,如下:
1 public <t> t getmapper(class<t> type) { 2 return configuration.<t>getmapper(type, this); 3 } 4 5 // configuration 6 public <t> t getmapper(class<t> type, sqlsession sqlsession) { 7 return mapperregistry.getmapper(type, sqlsession); 8 } 9 10 // mapperregistry 11 public <t> t getmapper(class<t> type, sqlsession sqlsession) { 12 // 从 knownmappers 中获取与 type 对应的 mapperproxyfactory 13 final mapperproxyfactory<t> mapperproxyfactory = (mapperproxyfactory<t>) knownmappers.get(type); 14 if (mapperproxyfactory == null) { 15 throw new bindingexception("type " + type + " is not known to the mapperregistry."); 16 } 17 try { 18 // 创建代理对象 19 return mapperproxyfactory.newinstance(sqlsession); 20 } catch (exception e) { 21 throw new bindingexception("error getting mapper instance. cause: " + e, e); 22 } 23 }
这里最重要就是两行代码,第13行和第19行,我们接下来就分析这两行代码
获取mapperproxyfactory
根据名称看,可以理解为mapper代理的创建工厂,是不是mapper的代理对象由它创建呢?我们先来回顾一下knownmappers 集合中的元素是何时存入的。这要在我前面的文章中找答案,mybatis 在解析配置文件的 <mappers> 节点的过程中,会调用 mapperregistry 的 addmapper 方法将 class 到 mapperproxyfactory 对象的映射关系存入到 knownmappers。有兴趣的同学可以看看我之前的文章,我们来回顾一下源码:
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); } } } }
在解析mapper.xml的最后阶段,获取到mapper.xml的namespace,然后利用反射,获取到namespace的class,并创建一个mapperproxyfactory的实例,namespace的class作为参数,最后将namespace的class为key,mapperproxyfactory的实例为value存入knownmappers。
注意,我们这里是通过映射文件的命名空间的class当做knownmappers的key。然后我们看看getmapper方法的13行,是通过参数employee.class也就是mapper接口的class来获取mapperproxyfactory,所以我们明白了为什么要求xml配置中的namespace要和和对应的mapper接口的全限定名了
生成代理对象
我们看第19行代码 return mapperproxyfactory.newinstance(sqlsession);,很明显是调用了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 对象,mapperproxy 实现了 invocationhandler 接口,代理逻辑封装在此类中 * 将sqlsession传入mapperproxy对象中,第二个参数是mapper的接口,并不是其实现类 */ mapperproxy<t> mapperproxy = new mapperproxy(sqlsession, this.mapperinterface, this.methodcache); return this.newinstance(mapperproxy); } }
上面的代码首先创建了一个 mapperproxy 对象,该对象实现了 invocationhandler 接口。然后将对象作为参数传给重载方法,并在重载方法中调用 jdk 动态代理接口为 mapper接口 生成代理对象。
这里要注意一点,mapperproxy这个invocationhandler 创建的时候,传入的参数并不是mapper接口的实现类,我们以前是怎么创建jdk动态代理的?先创建一个接口,然后再创建一个接口的实现类,最后创建一个invocationhandler并将实现类传入其中作为目标类,创建接口的代理类,然后调用代理类方法时会回调invocationhandler的invoke方法,最后在invoke方法中调用目标类的方法,但是我们这里调用mapper接口代理类的方法时,需要调用其实现类的方法吗?不需要,我们需要调用对应的配置文件的sql,所以这里并不需要传入mapper的实现类到mapperproxy中,那mapper接口的代理对象是如何调用对应配置文件的sql呢?下面我们来看看。
mapper代理类如何执行sql?
上面一节中我们已经获取到了employeemapper的代理类,并且其invocationhandler为mapperproxy,那我们接着看mapper接口方法的调用
list<employee> allemployees = employeemapper.getall();
知道jdk动态代理的同学都知道,调用代理类的方法,最后都会回调到invocationhandler的invoke方法,那我们来看看这个invocationhandler(mapperproxy)
public class mapperproxy<t> implements invocationhandler, serializable { private final sqlsession sqlsession; private final class<t> mapperinterface; private final map<method, mappermethod> methodcache; public mapperproxy(sqlsession sqlsession, class<t> mapperinterface, map<method, mappermethod> methodcache) { this.sqlsession = sqlsession; this.mapperinterface = mapperinterface; this.methodcache = methodcache; } public object invoke(object proxy, method method, object[] args) throws throwable { // 如果方法是定义在 object 类中的,则直接调用 if (object.class.equals(method.getdeclaringclass())) { try { return method.invoke(this, args); } catch (throwable var5) { throw exceptionutil.unwrapthrowable(var5); } } else { // 从缓存中获取 mappermethod 对象,若缓存未命中,则创建 mappermethod 对象 mappermethod mappermethod = this.cachedmappermethod(method); // 调用 execute 方法执行 sql return mappermethod.execute(this.sqlsession, args); } } private mappermethod cachedmappermethod(method method) { mappermethod mappermethod = (mappermethod)this.methodcache.get(method); if (mappermethod == null) { //创建一个mappermethod,参数为mapperinterface和method还有configuration mappermethod = new mappermethod(this.mapperinterface, method, this.sqlsession.getconfiguration()); this.methodcache.put(method, mappermethod); } return mappermethod; } }
如上,回调函数invoke逻辑会首先检测被拦截的方法是不是定义在 object 中的,比如 equals、hashcode 方法等。对于这类方法,直接执行即可。紧接着从缓存中获取或者创建 mappermethod 对象,然后通过该对象中的 execute 方法执行 sql。我们先来看看如何创建mappermethod
创建 mappermethod 对象
public class mappermethod { //包含sql相关信息,比喻mappedstatement的id属性,(mapper.employeemapper.getall) private final sqlcommand command; //包含了关于执行的mapper方法的参数类型和返回类型。 private final methodsignature method; public mappermethod(class<?> mapperinterface, method method, configuration config) { // 创建 sqlcommand 对象,该对象包含一些和 sql 相关的信息 this.command = new sqlcommand(config, mapperinterface, method); // 创建 methodsignature 对象,从类名中可知,该对象包含了被拦截方法的一些信息 this.method = new methodsignature(config, mapperinterface, method); } }
mappermethod包含sqlcommand 和methodsignature 对象,我们来看看其创建过程
① 创建 sqlcommand 对象
public static class sqlcommand { //name为mappedstatement的id,也就是namespace.methodname(mapper.employeemapper.getall) private final string name; //sql的类型,如insert,delete,update private final sqlcommandtype type; public sqlcommand(configuration configuration, class<?> mapperinterface, method method) { //拼接mapper接口名和方法名,(mapper.employeemapper.getall) string statementname = mapperinterface.getname() + "." + method.getname(); mappedstatement ms = null; //检测configuration是否有key为mapper.employeemapper.getall的mappedstatement if (configuration.hasstatement(statementname)) { //获取mappedstatement ms = configuration.getmappedstatement(statementname); } else if (!mapperinterface.equals(method.getdeclaringclass())) { string parentstatementname = method.getdeclaringclass().getname() + "." + method.getname(); if (configuration.hasstatement(parentstatementname)) { ms = configuration.getmappedstatement(parentstatementname); } } // 检测当前方法是否有对应的 mappedstatement if (ms == null) { if (method.getannotation(flush.class) != null) { name = null; type = sqlcommandtype.flush; } else { throw new bindingexception("invalid bound statement (not found): " + mapperinterface.getname() + "." + methodname); } } else { // 设置 name 和 type 变量 name = ms.getid(); type = ms.getsqlcommandtype(); if (type == sqlcommandtype.unknown) { throw new bindingexception("unknown execution method for: " + name); } } } } public boolean hasstatement(string statementname, boolean validateincompletestatements) { //检测configuration是否有key为statementname的mappedstatement return this.mappedstatements.containskey(statementname); }
通过拼接接口名和方法名,在configuration获取对应的mappedstatement,并设置设置 name 和 type 变量,代码很简单
② 创建 methodsignature 对象
methodsignature 包含了被拦截方法的一些信息,如目标方法的返回类型,目标方法的参数列表信息等。下面,我们来看一下 methodsignature 的构造方法。
public static class methodsignature { private final boolean returnsmany; private final boolean returnsmap; private final boolean returnsvoid; private final boolean returnscursor; private final class<?> returntype; private final string mapkey; private final integer resulthandlerindex; private final integer rowboundsindex; private final paramnameresolver paramnameresolver; public methodsignature(configuration configuration, class<?> mapperinterface, method method) { // 通过反射解析方法返回类型 type resolvedreturntype = typeparameterresolver.resolvereturntype(method, mapperinterface); if (resolvedreturntype instanceof class<?>) { this.returntype = (class<?>) resolvedreturntype; } else if (resolvedreturntype instanceof parameterizedtype) { this.returntype = (class<?>) ((parameterizedtype) resolvedreturntype).getrawtype(); } else { this.returntype = method.getreturntype(); } // 检测返回值类型是否是 void、集合或数组、cursor、map 等 this.returnsvoid = void.class.equals(this.returntype); this.returnsmany = configuration.getobjectfactory().iscollection(this.returntype) || this.returntype.isarray(); this.returnscursor = cursor.class.equals(this.returntype); // 解析 @mapkey 注解,获取注解内容 this.mapkey = getmapkey(method); this.returnsmap = this.mapkey != null; /* * 获取 rowbounds 参数在参数列表中的位置,如果参数列表中 * 包含多个 rowbounds 参数,此方法会抛出异常 */ this.rowboundsindex = getuniqueparamindex(method, rowbounds.class); // 获取 resulthandler 参数在参数列表中的位置 this.resulthandlerindex = getuniqueparamindex(method, resulthandler.class); // 解析参数列表 this.paramnameresolver = new paramnameresolver(configuration, method); } }
执行 execute 方法
前面已经分析了 mappermethod 的初始化过程,现在 mappermethod 创建好了。那么,接下来要做的事情是调用 mappermethod 的 execute 方法,执行 sql。传递参数sqlsession和method的运行参数args
return mappermethod.execute(this.sqlsession, args);
我们去mappermethod 的execute方法中看看
mappermethod
public object execute(sqlsession sqlsession, object[] args) { object result; // 根据 sql 类型执行相应的数据库操作 switch (command.gettype()) { case insert: { // 对用户传入的参数进行转换,下同 object param = method.convertargstosqlcommandparam(args); // 执行插入操作,rowcountresult 方法用于处理返回值 result = rowcountresult(sqlsession.insert(command.getname(), param)); break; } case update: { object param = method.convertargstosqlcommandparam(args); // 执行更新操作 result = rowcountresult(sqlsession.update(command.getname(), param)); break; } case delete: { object param = method.convertargstosqlcommandparam(args); // 执行删除操作 result = rowcountresult(sqlsession.delete(command.getname(), param)); break; } case select: // 根据目标方法的返回类型进行相应的查询操作 if (method.returnsvoid() && method.hasresulthandler()) { executewithresulthandler(sqlsession, args); result = null; } else if (method.returnsmany()) { // 执行查询操作,并返回多个结果 result = executeformany(sqlsession, args); } else if (method.returnsmap()) { // 执行查询操作,并将结果封装在 map 中返回 result = executeformap(sqlsession, args); } else if (method.returnscursor()) { // 执行查询操作,并返回一个 cursor 对象 result = executeforcursor(sqlsession, args); } else { object param = method.convertargstosqlcommandparam(args); // 执行查询操作,并返回一个结果 result = sqlsession.selectone(command.getname(), param); } break; case flush: // 执行刷新操作 result = sqlsession.flushstatements(); break; default: throw new bindingexception("unknown execution method for: " + command.getname()); } return result; }
如上,execute 方法主要由一个 switch 语句组成,用于根据 sql 类型执行相应的数据库操作。我们先来看看是参数的处理方法convertargstosqlcommandparam是如何将方法参数数组转化成map的
public object convertargstosqlcommandparam(object[] args) { return paramnameresolver.getnamedparams(args); } public object getnamedparams(object[] args) { final int paramcount = names.size(); if (args == null || paramcount == 0) { return null; } else if (!hasparamannotation && paramcount == 1) { return args[names.firstkey()]; } else { //创建一个map,key为method的参数名,值为method的运行时参数值 final map<string, object> param = new parammap<object>(); int i = 0; for (map.entry<integer, string> entry : names.entryset()) { // 添加 <参数名, 参数值> 键值对到 param 中 param.put(entry.getvalue(), args[entry.getkey()]); final string genericparamname = generic_name_prefix + string.valueof(i + 1); if (!names.containsvalue(genericparamname)) { param.put(genericparamname, args[entry.getkey()]); } i++; } return param; } }
我们看到,将object[] args转化成了一个map<参数名, 参数值> ,接着我们就可以看查询过程分析了,如下
// 执行查询操作,并返回一个结果 result = sqlsession.selectone(command.getname(), param);
我们看到是通过sqlsession来执行查询的,并且传入的参数为command.getname()和param,也就是namespace.methodname(mapper.employeemapper.getall)和方法的运行参数。
查询操作我们下一篇文章单独来讲