Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)

2022-10-13,,,,

刚开始使用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)和方法的运行参数。

查询操作我们下一篇文章单独来讲

 

《Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?).doc》

下载本文的Word格式文档,以方便收藏与打印。