zoukankan      html  css  js  c++  java
  • 《深入浅出MyBatis技术原理与实战》——6. MyBatis的解析和运行原理

    MyBatis的运行分为两大部分,第一部分是读取配置文件缓存到Configuration对象,用以创建SqlSessionFactory,第二部分是SqlSession的执行过程。

    6.1 涉及的技术难点简介

    Mapper是一个接口,而接口是没有办法去执行的,那么它是怎么运行的呢?答案是动态代理,MyBaits会为Mapper产生代理类,为此先来学习下动态代理。一般而言,动态代理分为两种,一种是JDK反射机制提供的代理,另一种是CGLIB代理。

    6.1.1 反射技术

    6.1.2 JDK动态代理

    由java.lang.reflect.*包提供支持的,我们需要完成这么几个步骤。

    • 编写服务类和接口,这个是真正的服务提供者,在JDK代理中接口是必须的
    • 编写代理类,提供绑定和方法。

    JDK最大的缺点是需要提供接口,而MyBatis的Mapper就是一个接口,他采用的就是JDK动态代理。我们首先提供一个服务接口,如:

    然后写一个实现类:

    现在让我们写一个代理类,提供真实对象的绑定和代理方法。代理类的要求是实现InvocationHandler接口的代理方法,当一个对象被绑定后,执行其方法的时候就会进入到代理方法里,如:

    下面这段代码:

    让JDK产生一个代理对象。这个代理对象有3个参数,第一个参数target.getClass().getClassLoader()是类加载器,第二个参数target.getClass().getInterfaces()是接口(代理对象挂在哪个接口下),第三个参数this代表当前HelloServiceProxy类。

    一旦绑定后,在进入代理对象方法调用的时候就会到HelloServiceProxy的代理方法上,代理方法有三个参数:第一个proxy是代理对象,第二个是当前调用的那个方法,第三个是方法的参数。比方说,现在HelloServiceImpl对象用bind方法绑定后,返回其占位,我们再调用proxy.sayHello("张三"),那么它就会进入到HelloServiceProxy的invoke()方法。而invoke参数中第一个便是代理对象proxy,方法便是sayHello(),参数是张三。

    我们已经用HelloServiceProxy类的属性target保存了真实的服务对象,那么我们可以通过反射技术调度真实对象的方法:

    现在让我们测试一下动态代理:

    运行结果:

    6.1.3 CGLIB动态处理

    JDK提供的动态代理存在一个缺陷,就是必须要提供接口才可以使用,为了克服这个缺陷,我们可以使用开源框架——CGLIB。让我们看看如何使用CGLIB动态代理。

    HelloService.java和HelloServiceImpl.java都不需要改变,但是我们要实现CGLIB的代理类:

    这样便能够实现CGLIB的动态代理。在MyBatis中通常在延迟加载的时候才会用到CGLIB的动态代理。

    6.2 构建SqlSessionFactory过程

    第一步,通过org.apache.ibaits.builder.xml.XMLConfigBuilder解析配置的XML文件,读出配置参数,并将读取的数据存入这个org.apache.ibatis.session.Configuration类中。注意,MyBatis几乎所有的配置都是存在这里的。

    第二步,使用Configuration对象去创建SqlSessionFactory实现类,我们一般都会使用org.apache.ibatis.session.defaults.DefaultSqlSessionFactory这个默认的实现类。

    这种创建的方式就是一种Builder模式。对于复杂的对象而言,直接使用构造方法构建室友困难的,这会导致大量的逻辑放在构造方法中。这个时候使用一个参数总领全局,例如,Configuration类,然后分布构建。

    6.2.1 构建Configuration

    6.2.2 映射器的内部组成

    一般而言,一个映射器是由3个部分组成:

    这里只列举了主要的属性和方法。MappedStatement对象涉及的东西较多,一般不去修改它。SqlSource是一个接口,主要作用是根据参数和其他的规则组装SQL,一般也不需要修改。对于参数和SQL而言,主要的规则都反映在BoundSql类对象上,在插件中往往需要拿到它进而可以拿到当前运行的SQL和参数以及参数规则,做出适当的修改,来满足我们特殊的需求。

    BoundSql会提供3个主要的属性:parameterMappings、parameterObject和sql。

    6.2.3 构建SqlSessionFactory

    6.3 SqlSession运行过程

    6.3.1 映射器的动态代理

    Mapper映射是通过动态代理来实现的,我们首先来看看代码清单:

    这里可以看到动态代理对接口的绑定,作用就是生成动态代理对象,而代理的方法则被放到了MapperProxy中。MapperProxy的源码:

    上面运用了invoke方法。一旦mapper是一个代理对象,那么它就会运行到invoke方法里面,invoke首先判断他是否是一个类,显然这里Mapper是一个接口不是类,所以判定失败。那么就会生成MapperMethod对象,它是通过cachedMapperMethod方法对其初始化的,人后执行execute方法,把sqlSession和当前运行的参数传递进去。让我们看看这个execute方法的源码:

    MapperMethod采用命令模式运行,根据上下文跳转到许多方法中。看以看到里面的executeForMany方法,实际上它最后就是通过sqlSession对象去运行对象的SQL。现在便可以明白MyBatis为什么只用Mapper接口便能够运行SQL,因为映射器的XML文件的命名空间对应的便是这个接口的全路径,那么根据全路径和方法名便能够绑定起来,通过动态代理技术,让这个接口跑起来。然后采用命令模式,最后还是使用SqlSession接口的方法使得它能够执行查询,有了这层封装我们便可以使用接口编程。

    6.3.2 SqlSession下的四大对象

    我们已经知道了映射器其实就是一个动态代理对象,进入到了MapperMethod的execute方法。它经过简单的判断就进入了SqlSession的删除,更新,插入,选择等方法,那么这些方法如何执行呢?

    通过类名和方法名字就可以匹配到我们配置的SQL,我们不需要去关心这些细节,我们关心的是设计框架。Mapper的执行过程是通过Executor、StatementHandler、ParameterHandler和ResultHandler来完成数据库操作和结果返回的。

    6.3.2.1 执行器

    执行器起到了至关重要的作用。它是一个真正地执行java和数据库交互的东西。在MyBatis中存在三种执行器。我们可以在MyBatis的配置文件中进行选择:

    让我们看看MyBatis如何闯将Executor:

    在创建对象后,他会执行下面这样一行代码:

    这就是MyBatis的插件,这里它将我们构建一层层的动态代理对象。在调度真实的Executor方法之前执行配置插件的代码可以修改。现在我们可以看看执行器方法内部,以SIMPLE执行器SimpleExecutor的查询方法作为例子尽心讲解,如:

    显然MyBatis根据Configuration来构建StatementHandler,然后使用prepareStatement方法,对SQL编译并对参数进行初始化。调用了StatementHandler的prepare()进行了预编译和基础设置,然后通过StatementHandler的parameterize()来设置参数并执行,resultHandler再组装查询结果返回给调用者来完成一次查询。这样我们的焦点又转移到了StatementHandler上。

    6.3.2.2 数据库会话器

    StatementHandler是用来专门处理数据库会话的,让我门先来看看MyBatis是如何创建StatementHandler的,再看Configuration.java生成会话器的地方,如:

    很显然创建的真实对象是一个RoutingStatementHandler对象,它实现了接口StatementHandler。和Executor一样,用代理对象做一层层的封装。

    RoutingStatement不是我们真实的服务对象,它是通过适配器模式找到对应的StatementHandler来执行的。在MyBatis中,StatementHandler和Executor一样分为三种:SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler。它所对应的是之前提到过的三种执行器。

    在初始化RoutingStatementHandler对象的时候它会根据上下文环境决定创建哪个StatementHandler对象,下面是RoutingStatementHandler的源码:

    数据库会话器定义了一个对象的适配器delegate。它是一个StatementHandler接口对象,构造方法根据配置来适配对应的StatementHandler对象。它的作用是给实现类对象的使用提供一个统一,简易的使用适配器。此为对象的适配器模式,可以让我们使用现有的类和方法对外提供服务,也可以根据实际的需求对外屏蔽一些方法,甚至是加入新的服务。现在已最常用的PreparedStatementHandler为例,看看MyBatis是怎么执行查询的。看它三个主要的方法,prepare,parameterize和query

    然后会调用parameterize()方法去设置参数:

    这个时候它是调用ParameterHandler去完成的,这里先我们先来看看StatementHandler的查询方法:

    在Executor里,会先调用StatementHandler的prepare()方法预编译SQL语句,同时设置一些基本运行的参数。然后用parameterize()方法启用ParameterizeHandler设置参数,完成预编译,跟着就是执行查询,而update()也是这样的,最后如果需要查询,我们就用ResultHandler封装结果返回给调用者。

    下面再讨论另外两个对象的使用,ParameterHandler和ResultSetHandler

    6.3.2.3 参数处理器

    在6.3.2.2节中看到了MyBatis是通过参数处理器(ParameterHandler)对预编译语句进行参数设置的。它的作用是完成对预编译参数的设置。它的定义如下:

    其中,getParameterObject()方法的作用是返回参数对象,setParameter()方法的作用是设置预编译SQL语句的参数。

    MyBatis为ParameterHandler提供了一个实现类DefaultParameterHandler,我们来看看setParameters的实现:

    从parameterObject对象中取参数,然后使用typeHandler进行参数处理。而typeHandler也是在MyBatis初始化的时候,注册在Configuration里面的,我们需要的时候可以直接拿来用。这样就完成了参数的设置。

    6.3.2.4 结果处理器

    组装结果集返回。下面是结果处理器(ResultSetHandler)的接口定义:

    其中,handleOutputParameter()方法是处理存储过程输出参数的,重点看一下handleResultSets()方法,它是包装结果集的。MyBatis同样为我们提供了一个DefaultResultSetHandler类,默认情况下都是通过这个类处理的。这个实现有些复杂,涉及到JAVASSIST或者CGLIB作为延迟加载,然后通过typeHandler和ObjectFactory进行组装结果再返回。

    6.3.3 SqlSession总结

    SqlSession是通过Executor创建StatementHandler来运行的,而StatementHandler要经过下面三步:

    • prepared预编译SQL
    • parameterize设置参数
    • query/update执行SQL

  • 相关阅读:
    求24点
    关于参数和返回值的常量性
    点到平面的距离公式
    大端序与小端序
    Quake3中的绝对值函数
    整数超出范围时如何表示?
    关于数组的几道面试题
    在移位数组中查找数
    时间复杂度O(n),空间复杂度O(1)的排序
    C++之对象切割
  • 原文地址:https://www.cnblogs.com/f91og/p/7129065.html
Copyright © 2011-2022 走看看