2.2Mybatis——代理与SQL映射
合集总览:Mybatis框架梳理
“Java中非静态方法的运行需要实例对象才能运行(即对象点方法),Mybatis中的Mapper都是接口,也没有实现类,那么接口中的方法怎么就被调用执行了呢?”
这是当时用Mybatis时最困扰我的一个问题,搜的资料博客中,大多来一句“动态代理”一笔带过。留我一人风中凌乱,难道"动态代理"四个字这么形象生动、易于理解吗…
也因为这个疑问,了解了代理模式,动态代理,以及Mybatis如何使用动态代理完成mapper接口方法的调用。
1.代理模式
这里简单说一下,例子就不举了,网上都是。说一下工作中的使用场景,一般是某个类无法修改或不敢改动,那就在目标类外面包一层代理类,即不用修改目标类,又可以对目标类做功能的增强。其原理就是:通过实现和目标类相同的接口来平替目标类,然后通过构造函数获取目标类的对象引用,增强的功能由代理类完成,核心的逻辑依旧通过目标类进行调用。(我记得好像写过对应的设计模式笔记,感兴趣的可以翻一下)
2.如何执行接口方法
前面提到的代理模式和文章开头的问题有关系吗?当然有关系,如果你理解了什么是代理,文章开头的问题就可以通过代理来实现。
即使接口没有实现类
,也可以通过InvocationHandler
为其创建代理类,通过代理类执行接口方法。这是java反射包下的一个接口,可以为接口生成代理对象。简单代码示例:
示例demo
// 定义接口
public interface Interface01 {
String m1();
int m2();
}
// 定义调用处理器
public class Interface01Handler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("m1")) {
return "hello world";
}
else {
return 1024;
}
}
}
// 创建代理对象测试
public static void main(String[] args) {
// 生成代理对象
Object proxy = Proxy.newProxyInstance(Interface01.class.getClassLoader(),
new Class[]{Interface01.class}, new Interface01Handler());
if(proxy instanceof Interface01) {
// 调用接口方法
Interface01 a = (Interface01) proxy;
System.out.println(a.m1());
System.out.println(a.m2());
}
}
上述代码中,接口并没有实现类,但接口方法可以被调用。当然你也可以理解成:接口是以另一种形式被实现,接口方法以另一种形式被定义。这种在JVM运行期间动态的为接口创建代理对象的过程,我们称为JDK动态代理。
3.Mybatis是如何做的
通过反射可以为接口创建代理对象。知道了Java中的这个机制后,回到Mybatis的疑问,mapper也是接口,也没有实现类但最终其方法可以被调用,类比一下,你应该可以猜到Mybatis是如何来实现的。
3.1猜想
我们只定义了接口和方法,在未提供接口实现类的情况下却可以直接对接口方法进行调用。基于Mybatis的这个特性,再结合demo示例中Java反射提供的机制。于是我们猜想:
- Mybatis内部应该也是使用了JDK动态代理来执行mapper接口中的方法;
- 由于JDK动态代理中需要
调用处理器
去执行方法逻辑,我们虽然没有为mapper接口编写对应的调用处理器。但是我们为每个mapper接口定义了同名了mapper.xml,而且mapper.xml中的SQL片段其实就是这个方法的执行逻辑。所以Mybatis内部应该做了某些处理,调用处理器的invoke方法执行的应该就是对应的SQL片段:
// 调用处理器 InvocationHandler.invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// <select id="">
// <insert id="">
// <update id="">
// <delete id="">
}
上面是我们合理的猜想,如果得到印证,那么Mybatis内部使用动态代理的过程也就清晰了。接下来我们通过调试源码进行印证。
3.2源码探究
@SpringBootTest
public class SqlMappingTest {
@Resource
AddressMapper addressMapper;
@Test
public void test(){
Address address = addressMapper.selectById(1L);
System.out.println(address);
}
}
以查询为例,我们都知道,spring容器为我们注入了一个代理对象
addressMapper
,代理对象类型为MapperProxy
,刚好实现了InvocationHandler
接口:
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
}
再看一下
invoke
的执行逻辑是否是调用mapper.xml
中的SQL片段,以及它是如何进行调用:
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
...
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
...
break;
}
case UPDATE: {
...
break;
}
case DELETE: {
...
break;
}
case SELECT:
...
break;
case FLUSH:
...
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
}
上述代码中可以看到:
mapper.xml
中的SQL片段最终被加载成MapperMethod
对象,InvocationHandler.invoke
的调用是执行MapperMehtod.execute
方法。execute
方法就是将SQL分成CRUD几个类型,然后根据当前MapperMehtod
的类型进行SQL的执行,显然后面还有SQL参数、结果集的处理等逻辑,但我们的猜想到这里其实就差不多可以被印证了。
扩展:虽然关于
Mybatis中未实现的Mapper接口如何被调用执行
的猜想已经被印证,其过程和我们的猜想符合。但在过程中可能会引出其他的思考,比如:
- 代理对象是如何创建的;
mapper.xml
中的方法如何被转换为MapperMethod
对象;execute
方法中,确定SQL类型后,SQL的处理、结果集的处理等等这些如果有精力的话,后面会陆续展开描述。
梳理与总结
- JDK的动态代理是通过实现
InvocationHandler
接口的形式来实现的。即使接口没有定义实现类,也可以通过JDK动态代理的方式为接口创建代理对象。就接口实现而言,你确实可以把InvocationHandler
的这种机制看成是另一种接口实现,所以,既然可以直接去实现接口,为什么还要多此一举去使用InvocationHandler
呢?如果你有此疑问,Mybatis给出了很好的答案:接口一定需要显式定义实现类吗?接口和实现可以解耦,定义的接口不再局限于通过接口实现类去实现,可以根据需要以其他方式(mapper.xml)去实现接口;- Mybatis中,MapperProxy通过实现
InvocationHandler
接口为mapper接口
创建代理对象 ;- 根据
InvocationHandler
的调用机制,对mapper接口
中方法的调用,最终都将交给调用处理器的invoke
方法,Mybatis在该方法中完成了对mapper.xml SQL方法
的实际调用
- 如有理解错误或不足之处,欢迎大家留言讨论