2.2、MyBatis动态代理
我们在写MyBatis的时候会想一个问题,为什么xxxDao接口没有实现类却可以实现对应的操作?其实答案很简单,因为在MyBatis的内部采用了动态代理的技术(类在JVM运行时创建的,字节码文件自始至终就没有存在过,在JVM结束的时候类就消失了),在JVM运行时那么此时有两个问题:
- 如何创建Dao接口的实现类?
- 实现类是如何进行实现的?
一般来说需要实现动态代理有以下的几种场景:
- 为原始对象(目标)增加一些额外功能
- 远程代理
- 接口实现类,我们看不见的实实在在的类文件,但是在运行中却可以体现出来,典型的无中生有。
我们来看看MyBatis动态代理的源码,他有两个核心的类:
- MapperProxy
- MapperProxyFactory
2.2.1、MapperProxyFactory
他是一个创造MapperProxy的工厂,完成的事情就是创建Map(perProxy对象,通过newInstance()方法
我们可以看到MapperProxyFactory中有一个属性是mapperInterface,MapperProxyFactory实际上对应的就是我们自己写的xxxDao接口,他肯定会调用的方法是newInstance方法。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
我们可以看到new MapperProxy的时候,他穿了三个参数,我们来想一下为什么需要传这三个参数:
- sqlSession:底层需要调用sqlSession的增删改查的方法
- mapperInterface:需要接口名来拼接namespace
- methodCache:通过方法名完成namespace最后一步的拼接
2.2.2、MapperProxy
MapperProxy实现了InvocationHandler这个类,他的核心作用是通过动态代理创建了DAO的实现类,通过invoke方法去调用底层的SqlSession的增删改查操作,从而实现数据库的操作。我们来看了一下这个类的基本属性和构造方法。
接着我们就去看MapperProxy这个类,根据以前学习的动态代理我们可以知道,MapperProxy一定会实现InvocationHandler
我们找到对应的invoke方法去看看。
我们可以看到,MyBatis想的比我们更细致,我们看到这里if,他做了一个判断。
如果你的类是Object类的话,就不用交给SqlSession去执行,直接原样交给invoke去执行即可,如果是其他类的话,就去执行cachedInvoker方法,我们再追进去看。
里面有一个很重要的对象,那就是MapperMethod这个对象。我们点进去MapperMethod这个类看看。可以发现他有两个很重要的成员变量。
为了搞清楚这两个属性的作用,我们来看看SqlCommand的构造方法。
我们可以注意到,name这个属性被赋值成了id,而这个id就是namespace命名空间的id。而他的type表明的是这条sql是insert还是delete还是update还是select,以此来对应不同的SqlSession方法。
我们再来看他的第二个属性MethodSignature对象。
我们来把成员变量一个个来解析。
private final boolean returnsMany; // 返回值是否是多个
private final boolean returnsMap; // 你返回的是否是一个map
private final boolean returnsVoid; // 是否没有返回值
private final boolean returnsCursor;
private final boolean returnsOptional; // 返回值类型
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex; // 分页
private final Integer rowBoundsIndex;
private final ParamNameResolver paramNameResolver; // 你的参数是什么
我们可以发现MethodSignature主要针对的是返回值、分页和参数。我们接下来看看参数名的解析器ParamNameResolver,看看他是如何解析参数的。
在看这个代码的源码的时候,我们发现了一个老朋友,那就是@Param注解,如果有@Param注解的话,就可以通过注解来获取注解参数的值。
看完这个注解后,我们再回到MapperProxy这个类里面,有一个invoke重载的方法。
我们来看看是如何执行sql语句的,这下就要去点进去execute方法。当你点进去这个方法以后就真的一目了然了。
在execute方法中,根据不同的case对不同的操作来作区分。我们拿insert来距离,如果操作是insert的话,就可以开始准备参数了。
Object param = method.convertArgsToSqlCommandParam(args);
然后就可以通过调用sqlSession.insert方法来执行insert插入操作。而sqlSession.insert这个方法还需要两个参数,第一个参数是通过command.getName()来获取namespaceId,第二个参数是插入的时候传进来的参数。
result = rowCountResult(sqlSession.insert(command.getName(), param));
最后来看一下比较复杂的查询操作。
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
他分了好多种情况:
- 如果方法的返回值为空同时他又有返回值的话就执行
executeWithResultHandler(sqlSession, args); - 如果方法的返回值是一个list即表示她会返回很多个的时候的话,就会执行
result = executeForMany(sqlSession, args); - 如果返回值是一个map的话,他会执行
result = executeForMap(sqlSession, args); - 如果都不满足的话,最终会执行
sqlSession.selectOne(command.getName(), param);
我们可以发现,无论是什么操作,最终的落脚点一定都是sqlSession的相关操作。