关于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就是我们刚刚初始化的那个对象,
method就是封装了mapper中的方法,
args对应的是mapper方法里的参数。
继续执行,先是判断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
这里1033代表着不为static也不为abstract的public方法,在之后的版本中会看到源码已经换了写法(原理不变),看起来容易理解些。
那么什么样的方法不为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的任务结束