对Mybatis一直都没有做实质的记录。 现记录Mybatis的一些实现细节。组成一个系列。
本片文章讲述的是Mybatis是如何使得开发者仅仅编写mapper.xml
搭配一个namespace对应的Java接口
,就可以直接调用接口的方法,实现数据库的CRUD。
结论:Mybatis初始化的时候扫描包路径,以及配置路径, 将Java Mapper
和XML Mapper
映射起来, 并使用JDK动态代理构建代理类用以运行。JDK动态代理需要实现InvocationHandler
, 依赖的类为org.apache.ibatis.binding.MapperProxy
这个 InvocationHandler
。代理类的生产, 则是通过org.apache.ibatis.binding.MapperProxyFactory
完成。
下文通过代码DEMO的展示, 以及源码的解说介绍JDK动态代理, 和Mybatis对其的应用。
JDK动态代理的编写方式, 依托于接口:java.lang.reflect.InvocationHandler
, 在Mybatis中, 它的实现类名叫:org.apache.ibatis.binding.MapperProxy
。
如下是一个很简单的动态代理Demo(该Demo类实现了 Iterator 和一些其它的接口)
public class DynamicProxy implements InvocationHandler {
private Object o;
public T getObject(T t) {
this.o = t;
return (T) Proxy.newProxyInstance(
t.getClass().getClassLoader(),
t.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("每调用一次代理类的方法, 都得执行一次本方法, 代理类: " + proxy.getClass() + ", 执行的方法名称: " + method.toString());
printProxyClass(proxy.getClass());
return method.invoke(o, args);
}
private void printProxyClass(Class proxy) throws IllegalAccessException, IOException {
Field[] fields = proxy.getDeclaredFields();
for (int i = 0; i < fields.length; i ++) {
fields[i].setAccessible(true);
System.out.println(fields[i].toGenericString() + " 类元素指代的方法名称是: " + fields[i].get(proxy).toString());
}
System.out.println("==================如下是代理类对方法的代理, 如果有兴趣, 可以取消注释并阅读反编译之后的源码=============");
// byte[] clazz = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{proxy});
// FileOutputStream fos = new FileOutputStream("ProxyClass.class");
// fos.write(clazz);
// fos.close();
System.out.println("===============================");
Method[] methods = proxy.getDeclaredMethods();
for (int i = 0; i < methods.length; i ++) {
System.out.println(methods[i].toGenericString());
}
}
public static void main(String[] args)
throws InvocationTargetException, NoSuchMethodException,
InstantiationException, IllegalAccessException {
DynamicProxy dp = new DynamicProxy();
UnmodifiedIterator uf = dp.getObject(new JDK8DefaultIterator());
System.out.println(uf.hasNext());
}
}
以及Demo的输出结果:
每调用一次代理类的方法, 都得执行一次本方法, 代理类: class com.sun.proxy.$Proxy0, 执行的方法名称: public default boolean GuavaIterator.hasNext()
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m1 类元素指代的方法名称是: public boolean java.lang.Object.equals(java.lang.Object)
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m5 类元素指代的方法名称是: public default java.lang.Object GuavaIterator.computeNext()
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m3 类元素指代的方法名称是: public default boolean GuavaIterator.hasNext()
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m2 类元素指代的方法名称是: public java.lang.String java.lang.Object.toString()
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m8 类元素指代的方法名称是: public default void java.util.Iterator.forEachRemaining(java.util.function.Consumer)
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m7 类元素指代的方法名称是: public abstract java.lang.Object java.util.Iterator.next()
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m4 类元素指代的方法名称是: public abstract void GuavaIterator.xx()
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m6 类元素指代的方法名称是: public default void java.util.Iterator.remove()
private static java.lang.reflect.Method com.sun.proxy.$Proxy0.m0 类元素指代的方法名称是: public native int java.lang.Object.hashCode()
==================如下是代理类对方法的代理, 如果有兴趣, 可以取消注释并阅读反编译之后的源码=============
===============================
public final void com.sun.proxy.$Proxy0.remove()
public final boolean com.sun.proxy.$Proxy0.equals(java.lang.Object)
public final java.lang.String com.sun.proxy.$Proxy0.toString()
public final int com.sun.proxy.$Proxy0.hashCode()
public final boolean com.sun.proxy.$Proxy0.hasNext()
public final java.lang.Object com.sun.proxy.$Proxy0.next()
public final void com.sun.proxy.$Proxy0.forEachRemaining(java.util.function.Consumer)
public final void com.sun.proxy.$Proxy0.xx()
public final java.lang.Object com.sun.proxy.$Proxy0.computeNext()
false
在JDK动态代理中, 重点方法在于生成一个Proxy
以及在执行Method的时候, 需要做的代理前、代理后的动作
在展示代码中, 有一个 #getObject(Object)
方法, 请求传参和返回参数都是泛型T
。通过 java.lang.reflect.Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler)
方法构建出一个传参对象的代理对象。
需要注意的事情是,java.lang.reflect.Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler)
方法的返回接收参数只能是传参值的某个接口, 这是JDK动态代理的原理所限制的。如上一节的Demo代码反编译所示:
public final class $Proxy0
extends Proxy
implements com.sun.proxy..Proxy0
{
private static Method m0;
.... 其它的未展示的元素
private static Method m14;
public $Proxy0(InvocationHandler paramInvocationHandler)
{
super(paramInvocationHandler);
}
public final boolean equals(Object paramObject)
{
.... 其它的未展示的代码
}
public final InvocationHandler getInvocationHandler(Object paramObject)
throws IllegalArgumentException
{
.... 其它的未展示的代码
}
.... 其它的未展示的代码
public final Object next()
{
try
{
return (Object)this.h.invoke(this, m5, null);
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
.... 其它的未展示的代码
static
{
try
{
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
.... 其它的未展示的代码块
m14 = Class.forName("com.sun.proxy.$Proxy0").getMethod("wait", new Class[] { Long.TYPE, Integer.TYPE });
return;
}
.... 其它的未展示的代码
}
可以很明显的看出java.lang.reflect.Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler)
方法返回了一个类com.sun.proxy.$Proxy0
的代理对象。
java.lang.reflect.InvocationHandler#invoke(Object, Method, Object[])
方法,实现动态代理。本质上, JDK动态代理是一种字节码技术。 通过被代理对象的所有接口, 生成一个被代理对象的代理类(这也就是JDK代理必须要实现于某个接口的缘由),对于被代理对象方法的执行, 则是依据于传参时期传入的java.lang.reflect.InvocationHandler#invoke(Object, Method, Object[])
实现方法。
这是一项字节码技术, 所依赖的方法为 sun.misc.ProxyGenerator#addProxyMethod(Method, Class)
。大致调用栈为:
其中使用到了类java.lang.reflect.WeakCache
, 该似乎是专用于Jdk动态代理, 对生成的被代理类都构造成弱引用, 使得代理类可以方便的被进行垃圾回收。
本质上的代理类的构造来源于sun.misc.ProxyGenerator#generateClassFile
。包名由sun.reflect.misc.ReflectUtil.PROXY_PACKAGE
提供,类名由ProxyClassFactory#proxyClassNamePrefix
提供,还有一个序列号,因为ProxyClassFactory
是静态类,nextUniqueNumber
是AtomicLong
,在JVM中自增获得。
同时, 被生成的代理类, 也是可以落地成为本地文件的, 该开关由JVM参数sun.misc.ProxyGenerator.saveGeneratedFiles
控制。
默认会加入hashCode/equals/toString
三个方法(主要是给HashMap等类使用)。
然后通过遍历被代理类的所有接口的所有方法,对其增加默认的实现方。 当然,方法就是很简单的,全部都是调用的sun.misc.ProxyGenerator#addProxyMethod(Method, Class)
方法。该方法的实现则是通过内部类ProxyMethod
实现。
实现方式即插入一段字节码:this.h.invoke(this, method, Object[])
。 反编译出来效果如下:
public final boolean hasNext()
{
try
{
return ((Boolean)this.h.invoke(this, m4, null)).booleanValue();
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
代理类方法的执行实质上是对传参java.lang.reflect.InvocationHandler#invoke(Object, Method, Object[])
的执行。
JDK动态代理中, 这个 Method
为字节码构建时生成。同在sun.misc.ProxyGenerator#generateClassFile
中被构建。与具体的方法一一对应。在执行代理类的方法的时候,#invoke
的第二个传参便是这个元素。
同方法的构建, 只是在构建的时候传参传入一个java.lang.reflect.InvocationHandler
, 即java.lang.reflect.Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler)
的第三个传参。
它在构造器中被赋予给了java.lang.reflect.Proxy#h
Mybatis的动态代理类(InvocationHandler
)是 org.apache.ibatis.binding.MapperProxy
Mybatis的动态代理生成类(Proxy)是 org.apache.ibatis.binding.MapperProxyFactory
原谅我的画图水平~
在Mapper(类)的代理的获取上, 通过配置类org.apache.ibatis.session.Configuration
(主要管理jdbc的配置,和缓存、别名、SQL等)执行org.apache.ibatis.binding.MapperRegistry#getMapper
。最后调用到org.apache.ibatis.binding.MapperProxyFactory#newInstance(MapperProxy)
本质上就是对JDK动态代理的应用。
在Mapper(类)的获取之前,Mybatis(MapperRegistry)已经将Mapper和XML里面的namespace与每个SQL id跟Java Interface映射起来(或者是绑定类似 @SELECT()
这样的接口写法)。
类的位置在org.apache.ibatis.session.Configuration
, 里面储存了大量的Mybatis的配置, 不仅限于如下:
类的位置在org.apache.ibatis.session.SqlSessionFactory
, 顾名思义,就是为了获取SqlSession
的。同时, 它还负责存储org.apache.ibatis.session.Configuration
。它由org.mybatis.spring.SqlSessionFactoryBean
缔造(Spring环境)
SqlSessionFactory
(Spring环境)的默认构造位置, 在org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory()
, 它的默认实现类, 则是org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
。
它的构造过程, 主要是SqlSessionFactoryBean
对 Configuration
的组装。
它提供SqlSession
的方式, 则是提供一个org.apache.ibatis.executor.Executor
赋予给org.apache.ibatis.session.defaults.DefaultSqlSession
注:
org.apache.ibatis.executor.Executor
是Myabtis真正的SQL执行接口,有多个实现类,一般都是SimpleExecutor
,根据缓存与否, 还会给它配置包装器。 原理都是就是JDBC推荐的Connection/Statement/ResultSet这套体系。
翻译于Java DOC: Mybatis的org.apache.ibatis.session.SqlSession
类, 是Mybatis的主要的接口。通过这个类可以执行SQL命令(其它节讲解)、获取Mapper、管理事务(其它节讲解)。
它有三个实现类:
在有了Spring之后(也是企业应用中的主要场景), 一般都是使用的org.mybatis.spring.SqlSessionTemplate
, 由mybatis-spring.jar提供。
Mapper的获取依赖于SqlSession#getMapper(Class)
。获取得到的是一个代理类。获取方式则如Mybatis动态代理实现时序图。被代理类则是期望获取的那个业务代码中的接口。 在Spring中,它通常被org.mybatis.spring.mapper.MapperFactoryBean
这个 FactoryBean
看此处的小介绍调用。
而各类CRUD操作, 则是直接委托给由SqlSessionFactory
在创建该SqlSession
时传递的Executor
实现。
注意,
SqlSessionTemplate
是spring-mybatis.jar的核心,也就是Spring场景中的核心。它的主要操作执行, 还是依托于DefaultSqlSession
上述章节中讲到了 Myabtis 是怎么获取到一个代理后的Spring Bean的。 该Bean是怎么被执行的呢?
Mapper的的方法的执行, 完全依托于动态代理。 其本身的方法是根本没有执行的(毕竟根本没有实现,无法执行)。具体代码片段如下(见org.apache.ibatis.binding.MapperProxy
)
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
... 其它代码
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
它引用的方法为org.apache.ibatis.binding.MapperMethod#execute(SqlSession, Object[])
。它就是执行Mapper的方法时,真正执行的代理代码。具体的执行思路可以抽象为如下伪代码:
SqlSession.select("select * from table;");
所有的Mapper的方法, 都会对应一个org.apache.ibatis.binding.MapperMethod
。它有两个元素, 都是内部类:
MappedStatement
), 该SQL类型:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
XML的解析由org.apache.ibatis.builder.xml.XMLMapperBuilder
完成,主要解析方法为#parseStatementNode()
。 具体调用栈如下
上时序图中CSDN不能将每个时序图的节点展示完全, 方法的名称在括号里面展示出来。
每个独立节点处理完毕之后,创建出来的是org.apache.ibatis.mapping.MappedStatement
, 这就是每条具体的SQL语句了。
SQL节点在Configuration
以Map的形式储存,Key为 接口名称 + 方法名称(即xml里面的id)。 这个Key刚好与org.apache.ibatis.binding.MapperMethod.SqlCommand#name
相同, 在运行MapperMethod
的时候,就是拼出一个接口名称 + 方法名称去Configuration
里面找, 然后执行里面的MappedStatement
使用 Mybatis + Spring 框架, 通过 XML 的编写和 接口的编写,实现数据库的CRUD。 这个操作分为如下两拨:
上述两个id是能够一一匹配的。
在执行Mapper的方法的时候,也是分为两拨: