欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-11-08 17:29:04
刚开始使用Mybaits的同学有没有这样的疑惑,为什么我们没有编写Mapper的实现类,却能调用Mapper的方法呢?本篇文章我带大家一起来解决这个疑问 上一篇文章我们获取到了DefaultSqlSession,接着我们来看第一篇文章测试用例后面的代码 为 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)和方法的运行参数。

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