关于Mybatis中MapperProxy的深入

为了方便研读,先搭建一个简单的SSM项目(本文不做赘述),然后完成一套流程,下面已增加insert为例。

1. 先给出关键代码

public class PersonDto {

    private Long id;

    private String name;
   
   ......... 构造、get、set
}

为了方便演示,先建一个简单的实体类

@Mapper
public interface PersonMapper {

    int insertPerson(PersonDto personDto) throws Exception;
}
<?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="com.balls.mapper.PersonMapper">

    <insert id="insertPerson" parameterType="com.balls.dto.PersonDto">
        INSERT
        INTO PERSON(ID, NAME)
        VALUES(#{id}, #{name})
    </insert>

</mapper>

编写mapper接口和对应的xml

@RestController
@RequestMapping(value = "/personController")
public class PersonController {

    @Resource
    private PersonMapper personMapper;

    @RequestMapping(value = "/{id}/{name}/insert.do", method = RequestMethod.GET)
    public int gets(@PathVariable Long id, @PathVariable String name) throws Exception {
        int result = 0;
        result = personMapper.insertPerson(new PersonDto(id, "white"));
        return result;
    }
}

因为是学习,先不管业务,跳过service层,这个影响不大

2. 初识MapperProxy
完成之后先测试接口的可用性,这里不做赘述,我们直奔主题,找到MapperProxy类。
打开这个类,可以看到这个类的声明

public class MapperProxy<T> implements InvocationHandler, Serializable

首先是个泛型,这个在实例使用时需要将T换成实际的对象类型,还有实现序列化,但本次的关注重点是invoke等方法。
下面继续看常量定义

	private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

UID不管他,这只是序列化对应的一种机制,有兴趣可以理解(以后补充)
sqlSession这个好理解,根据这个session去执行映射的语句,提交或者是回滚链接,不需要的时候就closeSession。当使用Mybatis-Spring后,我们不需要再对事物进行管理,因为bean会被注入到一个线程安全的SqlSession中,它可以基于Spring的事物配置来自动提交、回滚、关闭session等。这里我们先把他理解为是用来执行sql的,稍后再深入研究。
mapperInterface顾名思义就是mapper接口的对象了。
最后这个methodCache,先了解两个概念:

  • MapperMethod封装了Mapper接口与其中对应的信息,以及对应sql语句的信息。也就是说MapperMethod是连接Mapper接口欧以及配置文件中定义的sql语句的桥梁。
  • Method用于在程序运行状态中,动态地获取方法信息。

3. 又见MapperProxy

为了方便叙述,我先粘一下源码。

	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 {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }

            if (this.isDefaultMethod(method)) {
                return this.invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }

        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        return mapperMethod.execute(this.sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
            this.methodCache.put(method, mapperMethod);
        }

        return mapperMethod;
    }

    @UsesJava7
    private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
        Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class, Integer.TYPE);
        if (!constructor.isAccessible()) {
            constructor.setAccessible(true);
        }

        Class<?> declaringClass = method.getDeclaringClass();
        return ((Lookup)constructor.newInstance(declaringClass, Integer.valueOf(2))).unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
    }

    private boolean isDefaultMethod(Method method) {
        return (method.getModifiers() & 1033) == 1 && method.getDeclaringClass().isInterface();
    }

以上就是全部源码,不是太多,主要是invoke。先大概了解一下整体构成,在下一模块详细解释。
4. 深入MapperProxy
准备工作基本完毕,用第一部分的接口开始打断点调试。
首先,会执行到MapperProxy方法,先将刚刚提到的sqlSession、mapperInterface和methodCache初始化,这个好好理解。(如果想知道这些入参从何处来,请另寻研究,本文不做讨论)
初始化技术后,然后进入invoke方法。在这个地方我们可以看到,参数中的proxy就是我们刚刚初始化的那个对象,
关于Mybatis中MapperProxy的深入
method就是封装了mapper中的方法,
关于Mybatis中MapperProxy的深入
args对应的是mapper方法里的参数。
关于Mybatis中MapperProxy的深入
继续执行,先是判断proxy的类和method的clazz是否一致,如果一致执行method.invoke(this, args)并返回结果结束我们这里的invoke。但是这是我自己写的mapper接口,显然不会一致(一个是Proxy类,一个是Mapper类)。
继续执行判断是不是默认的方法。this.isDefaultMethod(method)点进去可以查看判断的逻辑。判断时先取method的获取方法或变量的修饰符的整数形式(不理解可以参考这位博主的文章方法修饰符转整形传送门)然后与1033进行&运算(一假则假,全真才真,如1010 & 0011 = 0010)判断结果是不是对于1并且再判断method的类是否是接口。叙述麻烦,下面直观的体现以下。
(method.getModifiers() & 1033) == 1 && method.getDeclaringClass().isInterface()
等于如下:
(method修饰符的对于整形 & 1033) == 1 && 方法所在的类是接口
一般来说都是1025,因为Mapper是接口,而接口中的方法默认都是public abstract修饰。public (1) + abstract(1024) = 1025
关于Mybatis中MapperProxy的深入
这里1033代表着不为static也不为abstract的public方法,在之后的版本中会看到源码已经换了写法(原理不变),看起来容易理解些。关于Mybatis中MapperProxy的深入
那么什么样的方法不为static也不为abstract却是public的呢?这些方法是有的,就比如sqlSession直接操作数据库时使用的selectOne之类的函数。
现在的是通过Mapper,所以方法是abstract,也就不走invokeDefaultMethod(proxy, method, args)了,进而继续执行MapperMethod mapperMethod = cachedMapperMethod(method);继续执行进入cachedMapperMethod(method)方法,在此方法中如果是第一次执行的话,methodCache是空的,会对其进行初始化,后续执行的时候会直接返回methodCache对象。执行完继续返回到invoke方法体中继续执行。然后进入mapperMethod.execute(this.sqlSession, args)方法。执行得到最终结果。
mapperMethod.execute(this.sqlSession, args)是与接数据库交互最关键的一步。稍后在了一篇文章中叙述。至此,MapperProxy的任务结束