Mybatis3详解(十八)——Mybatis运行原理之Mapper接口的动态代理过程

1、写在前面

       前两章分别介绍了SqlSessionFactory和SqlSession的构建过程,然后就可以通过SqlSession来获取Mapper对象了,所以这章来学习Mapper接口是如何通过动态代理来创建对象的。

c0f342d3a58264d0ab7ffbaebfc22a67

2、Mapper动态代理

       ①、程序入口:

image


       ②、SqlSession的实现类为DefaultSqlSession,所以进入DeaultSqlSession类找到重写的getMapper()方法。

image

       可以发现它明显是调用了Configuration对象中的getMapper()方法来获取对应的接口对象。


       ③、所以点击进入Configuration类找到对应的getMapper()方法。

image

       可以发现这里也是将工作继续交到MapperRegistry的getMapper()的方法中,所以我们继续向下进行。


       ④、点击进入MapperRegistry类找到getMapper()方法。

image

       上面的代码中MapperProxyFactory对象是通过knownMappers来获取的,它是一个HashMap,这个knownMapper的定义:

image


       ⑤、通过上面可以发现是使用MapperProxyFactory来生成代理类,所以进入MapperProxyFactory的newInstance()方法中。

image

       注意:这里调用了两个newInstance()方法,上面的代码中进入后先调用第二个newInstance方法并创建MapperProxy代理对象,所以这里的重点是关注这个类。然后再去调用第一个newInstance方法并将创建好的MapperProxy代理对象传入进去,根据该对象创建代理类并返回。所以到这里已经得到需要的代理类了,但是我们的代理类所做的工作还得继续向下看MapperProxy类。


       ⑥、找到MapperProxy类,这个类非常重要。可以发现这个类实现了InvocationHandler接口,因为JDK的动态代理必须实现这个接口,只要类实现了InvocationHandler接口的类最终都会执行invoke()方法,所以我们重点关注重写的invoke()方法。

image

       invoke()方法中首先判断代理对象是不是一个类,这里的Mapper代理对象是一个接口,不是一个类,所有会调用cachedInvoker(method).invoke()方法来生成MapperMethod对象,它是用来描述Mapper接口里面一个方法的内容的。有点类似于Spring中的BeanDefinition类。(MapperMethod对象下面会有介绍)。

       下面我们来看看这个MapperMethod是如何生成的。它是调用了cachedInvoker(method).invoke()方法来生成的(注意:这里是一个方法链,先是调用了cachedInvoker(method),然后再调用的invoke()),cachedInvoker(method)这个方法就不仔细看了,它的返回值为MapperMethodInvoker对象,这个类是MapperProxy类中定义的一个内部接口,并且定义了内部实现类,我们重点要看的是调用invoke()方法,如下。

image

       可以发现这里定义了MapperMethod对象,并且通过内部实现类重写的invoke()方法调用了mapperMethod.execute()方法,这个execute()方法是具体执行操作的方法,然后将结果返回。


       ⑦、所以我们来具体看看MapperMethod类。注意:我们首先需要知道这个MapperMethod类是干什么的?它用来描述Mapper接口里面一个方法的内容的,有点像Spring中的BeanDefinition。MapperMethod类是整个代理机制的核心类,它对SqlSession中的操作进行了封装使用,主要的功能是执行SQL的相关操作,例如执行sqlSession.selectList操作。该类里面定义了两个内部类分别为:SqlCommand(Sql命令)和MethodSignature(方法签名),其中SqlCommand听名字就知道是用来封装SQL命令的,是的,它是用来封装CRUD操作,也就是我们在xml中配置的操作的节点,每个节点都会生成一个MappedStatement类。另一个MethodSignature用来封装方法的参数以及返回类型。MapperMethod在初始化时会实例化两个组件SqlCommand(Sql命令)和MethodSignature(方法签名),必须同时给这两个组件提供参数:Mapper的接口路径(mapperInterface),待执行的方法(method),配置的Configuration。然后通过获取SqlCommand中的执行类型,MapperMethod才知道该Mapper接口将要执行什么样的操作。

image

image


       ⑧、我们重点来看一下execute()方法中属性为SELECT时调用的executeForMany(SqlSession sqlSession, Object[] args)方法,这个方法表示查询多条数据。

image

       这是查询多条记录的一个方法,从上面的方法可以看到,虽然在SELECT操作中,是调用了MapperMethod中的方法,但本质上仍是通过Sqlsession下的selectList()方法实现的,而其它增删改查都是类似的。最后经过一大圈的代理又回到了原地,这就是整个动态代理的实现过程了。同时我们可以看到执行的参数是通过command.getName()来获取的,所以继续来跟踪command.getName()是怎么来的,这里是SQLCommand的内容。


       ⑨、MapperMethod的内部类——SqlCommand,它封装了具体执行的动作。

image

image

       首先是获取了方法名和类名,然后调用了resolveMappedStatement()方法来解析生成MappedStatement对象。所以我们重点来看看这个解析的方法,进入方法后可以看到首先就是构建了statementId,注意到这个id是由接口名字+方法名字组成的。接着往下走,发现configuration.getMappedStatement(statementId);这句话,参数传入的是statementId,也就是说要找的MappedStatement并不是new出来的,而是通过statementId从Configuration类对象中get出来的。也就是说很早之前MappedStatement在很早之前就已经被初始化,并且放到Configuration对象里面,是的,我们的MappedStatement在SqlSessionFactory构建的时候就已经封装完成了,它包含了一条SQL语句的所有信息,MappedStatement本身是一个Map,它的可以为接口名字+方法名字组成的,所以这里我们可以根据statementId来获取MappedStatement对象。最后根据获取的MappedStatement对象来初始化name和type的值。


       ⑩、MapperMethod的内部类——MethodSignature,它主要封装了Mapper接口中方法的参数类型、返回值类型等信息。

image

       这里主要是对Mapper接口中的方法进行解析处理并且封装,它是通过参数解析器ParamNameResolver来完成的。

       上面用到反射类Method中的很多方法,所以我们来简单掌握Method类常用的方法:

  • getName():获取方法名
  • getDeclaringClass:获取全限定类名
  • getModifiers():获取权限修饰符
  • getReturnType():获取返回类型
  • getExceptionTypes():获取所有抛出的异常类型
  • getParameterTypes():获取所有参数的类型
  • getParameterAnnotations():获取方法中的所有注解
  • getAnnotations():获取方法级别的注解

       至此mapper的动态代理介绍完了。下面还介绍了Mapper接口中的方法参数是怎么来获取的,它是通过参数解析器(ParamNameResolver)来完成的,有兴趣可以了解一下。

       ⑪、在上述代码中经常能看见这样一句代码 method.convertArgsToSqlCommandParam(args); 这个方法是MethodSignature内部类中的,该方法主要的功能是获取@Param注解上的参数值。而实现方式便是通过参数解析器(ParamNameResolver),convertArgsToSqlCommandParam(args)方法实际上调用的是ParamNameResolver下的getNamedParams(args)方法。在分析该方法之前,先看看构造器都做了些什么操作。

image

image

       构造器同样需要两个入参,配置类和方法名,ParamNameResolver类下包含了两个属性字段GENERIC_NAME_PREFIX属性前缀和参数集合names构造器会将Method中所有参数级别的注解全部解析出来方法有序参数集中,names中存储形式为<参数下标,参数名>,如果在注解上设置了参数名,则会直接获取注解的value值,如果没有使用@Param注解,则使用真实的参数名,注意:真实参数名其实是arg0,arg1....的形式展现的,在判断真实参数名时,Mybatis会检查JDK版本是否包含java.lang.reflect.Parameter类,不存在该类的化会抛出ClassNotFoundException异常。完成初始化后,就可以调用getNamedParams(args)方法了,如下代码所示,该方法使用了类中的names属性,从有序集合中取出所有的<参数索引,参数名>键值对>,随后填充到另外一个集合中,以<参数名,参数下标索引,>形式呈现,同时会保留一份<param+下标索引,参数下标>的键值对。

image


       参考链接:

  1. 《Java EE 互联网轻量级框架整合开发》
  2. https://www.cnblogs.com/hopeofthevillage/p/11384848.html
  3. https://www.cnblogs.com/zsg88/p/7552600.html
  4. https://www.cnblogs.com/zhengzuozhanglina/p/11212200.html
原文地址:https://www.cnblogs.com/tanghaorong/p/14083403.html