Mybatis 实现原理之 JDK动态代理和XML语句执行

6 篇文章 0 订阅
1 篇文章 0 订阅

引言

对Mybatis一直都没有做实质的记录。 现记录Mybatis的一些实现细节。组成一个系列。

本片文章讲述的是Mybatis是如何使得开发者仅仅编写mapper.xml搭配一个namespace对应的Java接口,就可以直接调用接口的方法,实现数据库的CRUD。

结论Mybatis初始化的时候扫描包路径,以及配置路径, 将Java MapperXML Mapper映射起来, 并使用JDK动态代理构建代理类用以运行。JDK动态代理需要实现InvocationHandler, 依赖的类为org.apache.ibatis.binding.MapperProxy 这个 InvocationHandler。代理类的生产, 则是通过org.apache.ibatis.binding.MapperProxyFactory 完成。

下文通过代码DEMO的展示, 以及源码的解说介绍JDK动态代理, 和Mybatis对其的应用。

JDK动态代理

JDK动态代理的编写方式

JDK动态代理的编写方式, 依托于接口:java.lang.reflect.InvocationHandler, 在Mybatis中, 它的实现类名叫:org.apache.ibatis.binding.MapperProxy

如下是一个很简单的动态代理Demo(该Demo类实现了 Iterator 和一些其它的接口)

public class DynamicProxy implements InvocationHandler {

    private Object o;

    public <T> 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的时候, 需要做的代理前、代理后的动作

JDK动态代理的原理解析

在展示代码中, 有一个 #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[])实现方法。

JDK动态代理的字节码技术源码解析

这是一项字节码技术, 所依赖的方法为 sun.misc.ProxyGenerator#addProxyMethod(Method, Class)。大致调用栈为:

newProxyInstance getProxyClass0 get apply generateProxyClass generateClassFile Proxy方法内自调用(CNDS时序图语法真难用) 调用WeakCache/get方法 调用ProxyClassFactory/apply(JDK8的实现) 调用ProxyGenerator.generateProxyClass来生成类字节 生成类字节 newProxyInstance getProxyClass0 get apply generateProxyClass generateClassFile

其中使用到了类java.lang.reflect.WeakCache, 该似乎是专用于Jdk动态代理, 对生成的被代理类都构造成弱引用, 使得代理类可以方便的被进行垃圾回收。

本质上的代理类的构造来源于sun.misc.ProxyGenerator#generateClassFile。包名由sun.reflect.misc.ReflectUtil.PROXY_PACKAGE提供,类名由ProxyClassFactory#proxyClassNamePrefix提供,还有一个序列号,因为ProxyClassFactory是静态类,nextUniqueNumberAtomicLong,在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[])的执行。

  • 第一个传参Object是代理类的对象,一般是不直接使用的。 被代理类需要在某个地方缓存。
  • 第二个传参Method是所有接口中的某个方法。
  • 第三个传参Object[] 是Method的执行传参。

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的动态代理实现

Mybatis动态代理实现时序图

Mybatis的动态代理类(InvocationHandler)是 org.apache.ibatis.binding.MapperProxy
Mybatis的动态代理生成类(Proxy)是 org.apache.ibatis.binding.MapperProxyFactory

SqlSession Configuration MapperRegistry MapperProxyFactory MapperProxy 通过getMapper方法获取代理类 通过getMapper方法获取代理类 通过newInstance生产代理类 代理类的动态代理类是MapperProxy SqlSession Configuration MapperRegistry MapperProxyFactory MapperProxy

原谅我的画图水平~

在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()这样的接口写法)。

Myabtis的Configuration类

类的位置在org.apache.ibatis.session.Configuration, 里面储存了大量的Mybatis的配置, 不仅限于如下:

  • SQL的XML路径、文件内容
    • XML里面编写的returnType、resultMap、自定义的SQL标签等。
    • namespace。
    • CRUD语句, 以及其对应的method。
    • keyGenerator(insert之后赋予主键id的SQL)等一系列SQL XML相关的东西
  • Mybatis原生支持的typeAlias,用户自定义的typeAlias(TINYINT -> int)。
  • 数据源、事务管理器、各种JDBC配置
  • 缓存配置 等非常多的东西。

Mybatis的SqlSessionFactory接口

类的位置在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

它的构造过程, 主要是SqlSessionFactoryBeanConfiguration的组装。

它提供SqlSession的方式, 则是提供一个org.apache.ibatis.executor.Executor赋予给org.apache.ibatis.session.defaults.DefaultSqlSession

注: org.apache.ibatis.executor.Executor是Myabtis真正的SQL执行接口,有多个实现类,一般都是SimpleExecutor,根据缓存与否, 还会给它配置包装器。 原理都是就是JDBC推荐的Connection/Statement/ResultSet这套体系。


Mybatis的SqlSession接口

翻译于Java DOC: Mybatis的org.apache.ibatis.session.SqlSession类, 是Mybatis的主要的接口。通过这个类可以执行SQL命令(其它节讲解)、获取Mapper、管理事务(其它节讲解)。

它有三个实现类:

  • SqlSessionManager
  • SqlSessionTemplate
  • DefaultSqlSession

在有了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


Mybatis的Mapper接口执行过程

上述章节中讲到了 Myabtis 是怎么获取到一个代理后的Spring Bean的。 该Bean是怎么被执行的呢?

Mapper运行 – 动态代理

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的方法与XML的SQL映射

所有的Mapper的方法, 都会对应一个org.apache.ibatis.binding.MapperMethod。它有两个元素, 都是内部类:

  • SqlCommand 标记该条SQL的name(全局唯一,映射一个MappedStatement), 该SQL类型:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
  • MethodSignature 返参类型等相关的处理

XML的解析由org.apache.ibatis.builder.xml.XMLMapperBuilder完成,主要解析方法为#parseStatementNode()。 具体调用栈如下

parse configurat build1 build2 parseStatementNode 处理每个不同的XML(configurationElement) 处理CRUD SQL(buildStatementFromContext) buildStatementFromContext(List<XNode>, String) 每条SQL的处理(parseStatementNode) parse configurat build1 build2 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。 这个操作分为如下两拨:

  • 解析XML, 获取每条SQL语句;组装id(namespace + 每个SQL的id)。
  • 扫描Mybatis的Mapper接口, 得到id(接口名称 + 方法名称)。

上述两个id是能够一一匹配的。

在执行Mapper的方法的时候,也是分为两拨:

  • 得到注入的Spring Bean(这个Bean是Mybatis通过JDK动态代理生成的)。
  • 执行Bean方法(通过Bean里面的id找到具体的SQL,并执行)。
  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值