栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

mybatis使用详解(mybatis详解)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

mybatis使用详解(mybatis详解)

MyBatis 学习笔记

MyBatis 的解析和运行原理

代理模式反射技术JDK 动态代理CGLIB 动态代理构建 SqlSessionFactory 过程

构建 Configuration映射器的内部组成构建 SqlSessionFactory SqlSession 运行过程

映射器的动态代理 SqlSession 下的四大对象

执行器数据库会话器参数处理器结果处理器 SqlSession 运行总结 MyBatis 插件

插件接口插件的初始化插件的代理和反射设计常用的工具类——metaObject插件开发过程和实例

确定需要拦截的签名实现插件方法配置和运行 插件实例MyBatis 插件总结 常见面试题

1. 为什么 MyBatis 只用 Mapper 接口便能够运行 SQL?2. #{}和 ${}的区别是什么?3. Xml 映射文件中,除了常见的 select|insert|updae|delete 标签之外,还有哪些标签?4. 最佳实践中,通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?5. Mybatis 是如何进行分页的?分页插件的原理是什么?6. 简述 Mybatis 的插件运行原理,以及如何编写一个插件。7. Mybatis 执行批量插入,能返回数据库主键列表吗?8. Mybatis 动态 sql 是做什么的?都有哪些动态 sql?能简述一下动态 sql 的执行原理不?9. Mybatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式?10. Mybatis 能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别。11. Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?12. Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复?13. Mybatis 中如何执行批处理?14. Mybatis 都有哪些 Executor 执行器?它们之间的区别是什么?15. Mybatis 中如何指定使用哪一种 Executor 执行器?16. Mybatis 是否可以映射 Enum 枚举类?17. Mybatis 映射文件中,如果 A 标签通过 include 引用了 B 标签的内容,请问,B 标签能否定义在 A 标签的后面,还是说必须定义在 A 标签的前面?18. 简述 Mybatis 的 Xml 映射文件和 Mybatis 内部数据结构之间的映射关系?19. 为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?

MyBatis 的解析和运行原理

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

MyBatis 中的 Mapper 仅仅是一个接口,而不是一个包含逻辑的实现类,接口时没有办法去执行的,它是利用由 MyBatis 为 Mapper 接口创建的代理类来执行的。

代理模式

所谓的代理模式就是在原有的服务上多加一个占位,通过这个占位去控制服务的访问。

举例而言,假如你是一个公司的工程师,能提供一些技术服务。显然,客户只会找到你们公司的客服,和客服沟通,而不是找你沟通。客服会根据公司的规章制度和业务规则来决定找不找你服务。那么这个时候客服就等同于你的一个代理,她通过和客户的交流来控制对你的访问,当然她也可以提供一些你们公司对外的服务。而客户只能通过她的代理访问你。对客户而言,根本不需要认识你,只需要认识客服就可以了。事实上,站在客户的角度,客户会认为客服就代表你们公司,而不管真正为他服务的人是怎样的。

为什么要使用代理模式?

通过代理,一方面可以控制如何访问真正的服务对象,提供额外的服务。另外一方面有机会通过重写一些类来满足特定的需要,正如客服也可以根据公司的业务规则,提供一些服务,这个时候就不需要劳你大驾了。

一般而言,动态代理分为两种,一种是 JDK 反射机制提供的代理,另一种是 CGLIB 代理。在 JDK 提供的代理,我们必须要提供接口,而 CGLIB 则不需要提供接口,在 MyBatis 里面两种动态代理技术都已经使用了。

反射技术

在 Java 中,反射技术已经大行其道,并且通过不断优化,Java 的可配置性等性能得到了巨大的提高。下面来演示一个服务打印“hello + 姓名”,代码清单如下:

public class ReflectService {
    
    public void sayHello(String name) {
        System.out.println("hello" + name);
    }

    
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException,
            InvocationTargetException, IllegalAccessException, InstantiationException {
        // 通过反射创建 ReflectService 对象
        Object service = Class.forName(ReflectService.class.getName()).newInstance();
        // 获取服务方法
        Method method = service.getClass().getMethod("sayHello", String.class);
        // 反射调用方法
        method.invoke(service, "张三");
    }
}

结果:

hello 张三

进程完成,退出码 0

这段代码通过反射技术去创建 ReflectService 对象,获取方法后通过反射调用。

反射调用的最大好处是配置性大大提高,就如同 Spring IOC 容器一样,我们可以给很多配置设置参数,使得 Java 应用程序能够顺利运行起来,大大提高了 Java 的灵活性和可配置性,降低模块之间的耦合。

JDK 动态代理

JDK 的动态代理,是由 JDK 的 java.lang.reflect.* 包提供支持的,使用 JDK 的动态代理需要完成以下几个步骤:

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

JDK 的代理最大的缺点就是需要提供接口,而 MyBatis 的 Mapper 就是一个接口,它采用的就是 JDK 的动态代理。

编写一个服务接口及其实现类,代码清单如下:

HelloService.java

public interface HelloService {
    void sayHello(String name);
}

HelloServiceImpl.java

public class HelloServiceImpl implements HelloService {
    public void sayHello(String name) {
        System.out.println("Hello" + name);
    }
}

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

HelloServiceProxy.java

public class HelloServiceProxy implements InvocationHandler {

    
    private Object target;

    
    public Object bind(Object target) {
        this.target = target;
        // 取得代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), this); // jdk 代理需要提供接口
    }

    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("########## 执行 JDK 动态代理 ##########");
        Object result = null;
        // 反射方法前调用
        System.out.println(method.getName() + "执行之前");
        // 执行方法,相当于调用 HelloServiceImpl 类的 sayHello 方法
        result = method.invoke(target, args);
        // 反射方法后调用
        System.out.println(method.getName() + "执行之后");
        return result;
    }
}

上面这段代码让 JDK 产生一个代理对象。这个代理对象有三个参数:

1.target.getClass().getClassLoader():类加载器
2.target.getClass().getInterfaces():接口(代理对象挂在哪个接口下)
3.this:this 代表当前 HelloServiceProxy 类,换句话说是使用 HelloServiceProxy 的代理方法作为对象的代理执行者。

一旦绑定后,在进入代理对象方法调用的时候就会到 HelloServiceProxy 的代理方法上,代理方法有三个参数:

    proxy:代理对象当前调用的那个方法方法的参数

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

HelloServiceProxy 类的属性 target 保存了真实的服务对象,通过反射技术调度真实对象的方法。

result = method.invoke(target, args);

以上演示了 JDK 动态代理的实现,并且在调用方法前后都可以加入我们想要的东西。MyBatis 在使用 Mapper 的时候也是这样做的。

下面测试一下动态代理,代码清单如下:

TestHelloServiceProxy.java

public class TestHelloServiceProxy {
    public static void main(String[] args) {
        HelloServiceProxy handler = new HelloServiceProxy();
        HelloService proxy = (HelloService) handler.bind(new HelloServiceImpl());
        proxy.sayHello("张三");
    }
}

结果:

########## 执行 JDK 动态代理 ##########
sayHello 执行之前
Hello 张三
sayHello 执行之后

进程完成,退出码 0
CGLIB 动态代理

JDK 提供的动态代理存在一个缺陷,就是必须提供接口才可以使用,为了克服这个缺陷,可以使用开源框架——CGLIB,它是一种流行的动态代理。

下面来看看如何使用 CGLIB 动态代理。HelloService.java 和 HelloServiceImpl.java 都不需要改变,但是要实现 CGLIB 的代理类。它的实现 MethodInterceptor 的代理方法如下所示:

HelloServiceCglib.java

public class HelloServiceCglib implements MethodInterceptor {

    private Object target;

    
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        // 回调方法
        enhancer.setCallback(this);
        // 创建代理对象
        return enhancer.create();
    }

    
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("########## 执行 Cglib 动态代理 ##########");
        System.out.println(method.getName() + "执行之前");
        Object object = proxy.invokeSuper(obj, args);
        System.out.println(method.getName() + "执行之后");
        return object;
    }
}

下面测试一下动态代理,代码清单如下:

TestHelloServiceCglib.java

public class TestHelloServiceCglib {
    public static void main(String[] args) {
        HelloServiceCglib cglib = new HelloServiceCglib();
        HelloService proxy = (HelloService) cglib.getInstance(new HelloServiceImpl());
        proxy.sayHello("张三");
    }
}

结果:

########## 执行 Cglib 动态代理 ##########
sayHello 执行之前
Hello 张三
sayHello 执行之后

进程完成,退出码 0

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

构建 SqlSessionFactory 过程

SqlSessionFactory 时 MyBatis 的核心类之一,其最重要的功能就是提供创建 MyBatis 的核心接口 SqlSession,所以我们需要先创建 SqlSessionFactory,为此我们需要提供配置文件和相关的参数。MyBatis 采用构造模式去创建 SqlSessionFactory,我们可以通过 SqlSessionFactoryBuilder 去构建。构建分为以下两步:

XMLConfigBuilder.java➡读取配置的 XML 文件中的配置参数➡存入 Configuration.java➡创建 SqlSessionFactory。

1.通过 org.apache.ibatis.builder.xml.XMLConfigBuilder 解析配置的 XML 文件,读取配置参数,并将读取的数据存入这个 org.apache.ibatis.session.Configuration 类中。MyBatis 几乎所有的配置都是存在这里的。
2.使用 Configuration 对象去创建 SqlSessionFactory。MyBatis 中的 SqlSessionFactory 是一个接口,而不是一个实现类,为此 MyBatis 提供了一个默认的 SqlSessionFactory 实现类,一般都会使用它 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory。注意,在大部分情况下我们都没有必要自己去创建新的 SqlSessionFactory 的实现类。

这种创建方式就是一种 Builder 模式。这对于复杂的对象而言,直接使用构造方法构建是有困难的,这会导致大量的逻辑放在构造方法中,由于对象的复杂性,在构建的时候,我们更希望一步步有秩序的来构建它,从而降低其复杂性。这个时候使用一个参数类总领全局,例如,Configuration 类,然后分布构建,例如 DefaultSqlSessionFactory 类,就可以构建一个复杂的对象,例如,SqlSessionFactory,这种方式值得我们在工作中学习和使用。

构建 Configuration

在 SqlSessionFactory 构建中,Configuration 是最重要的,它的作用如下:

1.读入配置文件,包括基础配置的 XML 文件和映射器的 XML 文件。
2.初始化基础配置,比如 MyBatis 的别名等,一些重要的类对象,例如:插件、映射器、ObjectFactory 和 typeHandler 对象。
3.提供单例,为后续创建 SessionFactory 服务并提供配置的参数。
4.执行一些重要的对象方法,初始化配置信息。

MyBatis 会读出所有 XML 配置的信息,然后将这些信息保存到 Configuration 类的单例中。它会做如下初始化:

    properties 全局参数setting 设置typeAliases 别名typeHandler 类型处理器ObjectFactoy 对象plugin 插件environment 环境DatabaseIdProvider 数据库标识Mapper 映射器
映射器的内部组成

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

1.MappedStatement:它保存映射器的一个节点(select|insert|delete|update)。包括许多我们配置的 SQL、SQL 的 id、缓存信息、resultMap、parameterType、resultType、languageDriver 等重要配置内容。
2.SqlSource:它是提供 BoundSql 对象的地方,它是 MappedStatement 的一个属性。
3.BoundSql:它是建立 SQL 和参数的地方。它有 3 个常用的属性:SQL、parameterObject、parameterMappings。

这些都是映射器的重要内容,也是 MyBatis 的核心内容。映射器的内部组成如下图所示:

映射器的内部组成

这里只列举了主要的属性和方法,并没有将所有的方法和属性都列举出来。

MappedStatement 对象涉及的东西较多,我们一般都不去修改它,因为容易产生不必要的错误。SqlSource 是一个接口,它的主要作用是根据参数和其他的规则组装 SQL(包括动态 SQL),MyBatis 本身已经实现了它,一般也不需要去修改它。对于参数和 SQL 而言,主要的规则都反映在 BoundSql 类对象上,在插件中往往需要拿到它进而可以拿到当前运行的 SQL 和参数以及参数规则,做出适当的修改,来满足特殊的需求。

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

parameterObject 为参数本身,可以传递简单对象:POJO、Map 或者 @Param 注解的对象传递简单对象(包括 int、String、float、double 等),比如传递 int 类型时,MyBatis 会把参数变为 Integer 对象传递,类似的 long、String、float、double 也是如此。如果传递的是 POJO 或者 Map,那么这个 parameterObject 就是传入的 POJO 或者 Map 不变。也可以传递多个参数,如果没有 @Param 注解,MyBatis 就会把 parameterObject 变为一个 Map 对象,其键值的关系是按顺序来规划的,类似于这样的形式 {“1”:p1,”2“:p2,”3“:p3… ,”param1“:p1,“param2”:p2,“param3”:p3…},在编写的时候都可以使用 #{param1} 或者 #{1}去引用第一个参数。如果使用 @Param 注解,MyBatis 就会把 parameterObject 变为一个 Map 对象,类似于没有 @Param 注解,只是把其数字的键值对应置换为了 @Param 注解的键值。比如注解 @Param(“key1”)String p1, @Param(“key2”)int p2, @Param(“key3”)Role p3,那么这个 parameterObject 对象就是一个 Map,它的键值包含:{“key1”:p1,“key2”:p2,“key3”:p3}.parameterMappings,它是一个 List,每一个元素都是 ParameterMapping 的对象。这个对象会描述我们的参数。参数包括属性、名称、表达式、javaType、jdbcType、typeHandler 等重要信息,一般不需要去改变它。通过它可以实现参数和 SQL 的结合,以便 PreparedStatement 能够通过它找到 parameterObject 对象的属性并设置参数,使得程序准确运行。sql 属性就是我们书写在映射器里面的一条 SQL,在大多数时候无需修改它,只有在插件的情况下,可以根据需要进行改写。改写 SQL 将是一件危险的事情,务必慎重。 构建 SqlSessionFactory

有了 Configuration 对象构建 SqlSessionFactory 就很简单了,只要写很简短的代码便可以了。

sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

MyBatis 会根据 Configuration 的配置读取所配置的信息,构建 SqlSessionFactory 对象。

SqlSession 运行过程

SqlSession 是一个接口,通过构建 SqlSessionFactory 可以创建 SqlSession,SqlSession 给出了查询、插入、更新、删除的方法,在旧版本的 MyBatis 或 iBatis 中常常使用这些接口方法,而在新版的 MyBatis 中建议使用 Mapper,所以它就是 MyBatis 最为常用和重要的接口之一。

映射器的动态代理

Mapper 映射是通过动态代理来实现的。来看看源码 MapperProxyFactory.java:

从代码中可以看到动态代理对接口的绑定,它的作用就是生成动态代理对象(占位)。而代理的方法则被放到了 MapperProxy 类中。

来看看 MapperProxy 的部分源码:

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

来方法 execute 方法的源码:

MapperMethod 采用命令模式运行,根据上下文跳转,它可能跳转到许多方法中。executeForMany 方法实际上它最后就是通过 sqlSession 对象去运行对象的 SQL。

SqlSession 下的四大对象

映射器其实就是一个动态代理对象,进入到了 MapperMethod 的 execute 方法。它经过简单判断就进入了 SqlSession 的删除、更新、插入、选择等方法,这些方法的执行是我们需要关心的问题,也是正确编写插件的根本。

显然通过类名和方法名字就可以匹配到我们配置的 SQL,Mapper 执行的过程是通过 Executor、StatementHandler、ParameterHandler 和 ResultHandler 来完成数据库操作和结果返回的。

Executor 代表执行器,由它来调度 StatementHandler、ParameterHandler、ResultHandler 等来执行对应的 SQL。
StatementHandler 的作用是使用数据库的 Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用。
PreparedStatement 用于对 SQL 参数的处理。
ResultHandler 是进行最后数据集(ResultSet)的封装返回处理的。

执行器

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

SIMPLE,简易执行器,不配置它就是默认执行器。
REUSE,是一种执行器重用预处理语句。
BATCH,执行器重用语句和批量更新,它是针对批量专用的执行器。

它们都提供了查询和更新的方法,以及相关的事务方法。下面来看看 MyBatis 如何创建 Executor,代码清单如下:

MyBatis 将根据配置类型去确定你需要创建三种执行器中的哪一种,在创建对象后,它会去执行下面这样一行代码:

interceptorChain.pluginAll(executor);

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

显然MyBatis 根据 Configuration 来构建 StatementHandler,然后使用 prepareStatement 方法,对 SQL 编译并对参数进行初始化,再看它的实现过程,它调用了 StatementHandler 的 prepare()进行了预编译和基础设置,然后通过 StatementHandler 的 parameterize 来设置参数并执行,resultHandler 再组装查询结果返回给调用者来完成一次查询。

数据库会话器

顾名思义,数据库会话器(StatementHandler)就是专门处理数据库会话的,来看看 MyBatis 是如何创建 StatementHandler 的,再看 Configuration.java 生成会话器的地方,代码清单如下:

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

RoutingStatementHandler 不是我们真实的服务对象,它是通过适配器模式找到对应的 Statement 来执行的。在 MyBatis 中,StatementHandler 和 Executor 一样分为三种:SimpleStatementHandler、PrepareStatementHandler、CallableStatementHandler。它所对应的是上面一节所说的三种执行器。

在初始化 RoutingStatementHandler 对象的时候它会根据上下文环境决定创建哪个 StatementHandler 对象,来看看 RoutingStatementHandler 的源码,代码清单如下:

数据库会话器定义了一个对象的适配器 delegate,它是一个 StatementHandler 接口对象,构造方法根据配置类适配对应的 StatementHandler 对象。它的作用是给实现类对象的使用提供一个统一、简易的使用适配器。此为对象的适配模式,可以让我们使用现有的类和方法对外提供服务,也可以根据实际的需求对外屏蔽一些方法,甚至是加入新的服务。

现在以最常用的 PreparedStatementHandler 为例,看看 MyBatis 是怎么执行查询的。在了解执行器的时候可以看到它的三个主要的方法:prepare、parameterize 和 query,如代码清单所示:

baseStatementHandler 的 prepare 方法

instantiateStatement()方法是对 SQL 进行了预编译。首先,做一些基础配置,比如超时,获取的最大行数等的设置。然后,Executor 会调用 parameterize()方法去设置参数,它的方法如下所示:

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

由于在执行前参数和 SQL 都被 prepare()方法预编译,参数在 parameterize()方法上已经进行了设置。所以到这里已经很简单了。我们只要执行 SQL,然后返回结果就可以了。执行之后 ResultSetHandler 对结果的封装和返回。

一条查询 SQL 的执行过程如下:

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

参数处理器

MyBatis 是通过参数处理器(ParameterHandler)对预编译语句进行参数设置的。

ParameterHandler.java

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

MyBatis 为 ParameterHandler 提供了一个实现类 DefaultParameterHandler,来看看其 setParameters()方法的实现,代码清单如下:

从代码中可以看到它还是从 parameterObject 对象中取参数,然后使用 typeHandler 进行参数处理,这和 typeHandler 配置一样,如果有设置,那么它就会根据签名注册的 typeHandler 对参数进行处理。而 typeHandler 也是在 MyBatis 初始化的时候,注册在 Configuration 里面的,需要的时候可以直接拿来用。这样就完成了参数的设置。

结果处理器

有了 StatementHandler 的描述,我们知道它就是组装结果集返回的。再来看看结果处理器(ResultSetHandler)的接口定义,如代码清单所示:

其中,handleOutputParameters()方法是处理存储过程输出参数的,handlerResultSets()方法是包装结果集的。MyBatis 同样为我们提供了一个 DefaultResultSetHandler 类,在默认的情况下都是通过这个类进行处理的。

SqlSession 运行总结

SqlSession 内部运行图

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

1.prepared 预编译 SQL
2.parameterize 设置参数
3.query/update 执行 SQL

其中 parameterize 是调用 parameterHandler 的方法去设置的,而参数是根据类型处理器 typeHandler 去处理的。query/update 方法是通过 resultHandler 进行处理结果的封装,如果是 update 的语句,它就返回整数,否则它就通过 typeHandler 处理结果类型,然后用 ObjectFactory 提供的规则组装对象,返回给调用者。这便是 SqlSession 执行的过程。

MyBatis 插件 插件接口

在 MyBatis 中使用插件,就必须实现接口 Interceptor。

*intercept 方法:它将直接覆盖你所拦截对象原有的方法,因此它是插件的核心方法。intercept 里面有个参数 Invocation 对象,通过它可以反射调度原来对象的方法。
*plugin 方法:target 是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它。为了方便 MyBatis 使用 org.apache.ibatis.plugins.Plugin 中的 wrap 静态 (static) 方法提供生成代理对象,我们往往使用 plugin 方法便可以生成一个代理对象了。也可以自定义,自定义去实现的时候需要特别小心。
*setProperties 方法:允许在 plugin 元素中配置所需参数,方法在插件初始化的时候就被调用了 一次,然后把插件对象存入到配置中,以便后面再取出。

以上就是插件的骨架,这样的模式成为模板 (template) 模式,就是提供一个骨架,并且告知骨架中的方法是干什么用的,由开发者来完成它。在实际中,我们常常用到模板模式。

插件的初始化

插件的初始化是在 MyBatis 初始化的时候完成的,通过 XMLConfigBuilder 中的代码便可知道,代码清单如下:

在解析配置文件的时候,在 MyBatis 的上下文初始化过程中,就开始读入插件节点和我们配置的参数,同时使用反射技术生成对应的插件实例,然后调用方法中的 setProperties 方法,设置我们配置的参数,然后将插件实例保存到配置对象中,以便读取和使用它。所以插件的实例对象是一开始就被初始化的,而不是用到的时候才初始化的,需要使用它的时候,直接拿出来就可以了,这样有助于性能的提高。

再来看看插件在 Configuration 对象里是怎样保存的,如代码清单所示:

interceptorChain 在 Configuration 里面是一个属性,它里面有个 addInterceptor 方法,如代码清单所示:

显然,完成初始化的插件就保存在这个 List 对象里面等待将其取出使用。

插件的代理和反射设计

插件用的是责任链模式。

责任链模式:就是一个对象,在 MyBatis 中可能是四大对象中的一个,在多个角色中传递,处在传递链上的任何角色都有处理它的机会。打个比方,你在公司中是个重要人物,你需要请假 3 天。那么,请假流程是,首先你需要项目经理批准,然后部门经理批准,最后总裁批准才能完成。你的请假请求就是一个对象,它经过项目经理、部门经理、总裁多个角色审批处理,每个角色都可以对你的请求做出修改和批示。这就是责任链模式,它的作用是让每一个在责任链上的角色都有机会去拦截这个对象。在将来如果有新的角色也可以轻松拦截请求对象,进行处理。

MyBatis 的责任链是由 interceptorChain 去定义的,代码如下:

executor = (Executor) interceptorChain.pluginAll(executor);

来看看 pluginAll()方法是如何实现的,如代码清单所示:

plugin 方法是生成代理对象的方法,当它取出插件的时候是从 Configuration 对象中去取出的。从第一个对象(四大对象中的一个)开始,将对象传递给了 plugin 方法,然后返回一个代理;如果存在第二个插件,那么我们就拿到第一个代理对象,传递给 plugin 方法再返回第一个代理对象的代理…… 依此类推,有多少个拦截器就生成多少个代理对象。这样每一个插件都可以拦截到真实的对象了。这就好比每一个插件都可以一层层处理被拦截的对象。

如果要我们自己编写代理类,工作量会很大,为此 MyBatis 中提供了一个常用的工具类,用来生成代理对象,它便是 Plugin 类。Plugin 类实现了 InvocationHandler 接口,采用的是 JDK 的动态代理,来看看这个类的两个十分重要的方法,如代码清单所示:

MyBatis 提供生成代理对象的 Plugin 类

它是一个动态代理对象,其中 wrap 方法为我们生成这个对象的动态代理对象。

再看 invoke 方法,如果使用这个类为插件生成代理对象,那么代理对象在调用方法的时候就会进入到 invoke 方法中。在 invoke 方法中,如果存在签名的拦截方法,插件的 intercept 方法就会被我们在这里调用,然后就返回结果。如果不存在签名方法,那么将直接反射调度我们要执行的方法。

创建一个 Invocation 对象,其构造方法的参数包括被代理的对象、方法及其参数。Invocation 对象进行初始化,它有一个 proceed()方法,如代码清单所示:

这个方法就是调度被代理对象的真实方法。现在假设有 n 个插件,第一个传递的参数是四大对象的本身,然后调用一次 wrap 方法产生第一个代理对象,而这里的反射就是反射四大对象本身的真实方法。如果有第二个插件,则将第一个代理对象传递给 wrap 方法,生成第二个代理对象,这里的反射就是指第一个代理对象的 invoke 方法,依此类推直至最后一个代理对象。如果每一个代理对象都调用这个 proceed 方法,那么最后四大对象本身的方法也会被调用,只是它会从最后一个代理对象的 invoke 方法运行到第一个代理对象的 invoke 方法,直至四大对象的真实方法。

在初始化的时候,我们一个个的加载插件实例,并用 setProperties()方法进行初始化。我们可以使用 MyBatis 提供的 Plugin.wrap 方法去生成代理对象,再一层层地使用 Invocation 对象的 proceed()方法来推动代理对象运行。所以在多个插件的环境下,调度 proceed()方法时,MyBatis 总是从最后一个代理对象运行到第一个代理对象,最后是真实被拦截的对象方法被运行。大部分情况下,使用 MyBatis 的 Plugin 类生成代理对象足够我们使用,当然如果你觉得自己可以写规则,也可以不用这个类,使用这个方法时必须谨慎,因为它将覆盖底层的方法。

常用的工具类——metaObject

MyBatis 的一个工具类——metaObject,它可以有效读取或者修改一些重要对象的属性。在 MyBatis 中,四大对象提供的 public 设置参数的方法很少,难以通过其自身得到相关的属性信息,有了 metaObject 这个工具类就可以通过其他的技术手段来读取或者修改这些重要对象的属性。在 MyBatis 插件中它是一个十分常用的工具类。

它有 3 个方法常常被我们用到:

    metaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory):用于包装对象。Object getValue(String name):用于获取对象属性值,支持 OGNL。void setValue(String name, Object value):用于修改对象属性值,支持 OGNL。

在 MyBatis 对象中大量使用了这个类进行包装,包括四大对象,使得我们可以通过它来给四大对象的某些属性赋值从而满足我们的需要。

例如,拦截 StatementHandler 对象,我们需要先获取它要执行的 SQL 修改它的一些值。这时候可以使用 metaObject,它为我们提供了如代码清单所示的方法:

StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
metaObject metaStatementHandler = SystemmetaObject.forObject(statementHandler);
// 进行绑定
// 分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过循环可以分离出最原始的目标类
while (metaStatementHandler.hasGetter("h")) {
    Object object = metaStatementHandler.getValue("h");
    metaStatementHandler = SystemmetaObject.forObject(object);
}
// BoundSql 对象是处理 SQL 语句用的
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
// 判断 SQL 是否是 SELECT 语句,如果不是 SELECT 语句,那么就出错了
// 如果是,则修改它,最多返回 1000 行,这里用的是 MySQL 数据库,其他数据库要改写成其他
if (sql != null && sql.toLowerCase().trim().indexOf("select") == 0) {
    // 通过 SQL 重写来实现,这里我们起了一个奇怪的列名,避免与表名重复
    sql = "select * from (" + sql + ") $_$limit_$table_ limit 1000";
    metaStatementHandler.setValue("delegate.boundSql.sql", sql);
}

我们拦截的 StatementHandler 实际是 RoutingStatementHandler 对象,它的 delegate 属性才是真实服务的 StatementHandler,真实的 StatementHandler 有一个属性 BoundSql,它下面又有一个属性 sql。所以才有了路径 delegate.boundSql.sql。我们就可以通过这个路径去获取或者修改对应运行时的 SQL。通过这样的改写,就可以限制所有查询的 SQL 都只能至多返回 1000 行记录。

插件开发过程和实例 确定需要拦截的签名

正如 MyBatis 插件可以拦截四大对象中的任意一个一样。从 Plugin 源码中我们可以看到它需要注册签名才能够运行插件。签名需要确定一些要素。

1.确定需要拦截的对象

首先要根据功能来确定需要拦截的对象

Executor 是执行 SQL 的全过程,包括组装参数,组装结果集返回和执行 SQL 过程,都可以拦截,较为广泛,一般用的不算太多。StatementHandler 是执行 SQL 的过程,可以重写执行 SQL 的过程。这是最常用的拦截对象。ParameterHandler,很明显它主要是拦截执行 SQL 的参数组装,可以重写组装参数规则。ResultSetHandler 用于拦截执行结果的组装,可以重写组装结果的规则。

我们清楚需要拦截的是 StatementHandler 对象,应该在预编译 SQL 之前,修改 SQL 使得结果返回数量被限制。

2.拦截方法和参数

当确定了需要拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数。

查询的过程是通过 Executor 调度 StatementHandler 来完成的。调度 StatementHandler 的 prepare 方法预编译 SQL,于是我们需要拦截的方法便是 prepare 方法,在此之前完成 SQL 的重新编写。先看看 StatementHandler 接口的定义,如代码清单所示:

以上的任何方法都可以拦截。从接口定义而言,prepare 方法有一个参数 Connection 对象以及 Integer 对象,因此按以下方法来设计拦截器:

@Intercepts({
    @Signature(type = StatementHandler.class, // 要拦截的类
               method = "prepare", // 要拦截的方法
               args = {Connection.class, Integer.class}) // 要拦截的方法的参数
})
public class MyPlugin implements Interceptor {
}

其中,@Intercepts 说明它是一个拦截器。@Signature 是注册拦截器签名的地方,只有签名满足条件才能拦截,type 可以是四大对象中的一个,这里是 StatementHandler。method 代表要拦截四大对象的某一种接口方法,而 args 则表示该方法的参数(根据拦截对象的方法参数进行设置)。

实现插件方法
package com.mybatis.chap2.plugin;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;

import java.sql.Connection;
import java.util.Properties;

@Intercepts({@Signature(type = Executor.class,// 确定要拦截的对象
        method = "update",// 确定要拦截的方法
        args = {MappedStatement.class, Object.class}// 拦截方法的参数
)})
public class MyPlugin implements Interceptor {
    Properties properties = null;

    
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("before……");
        // 如果当前代理的是一个非代理对象,那么它就回调用真实拦截对象的方法,如果不是它会调度下个插件代理对象的 invoke 方法
        Object object = invocation.proceed();
        System.out.println("after……");
        return object;
    }

    
    public Object plugin(Object target) {
        // 使用 MyBatis 提供的 Plugin 类生成代理对象
        System.out.println("调用生成代理对象……");
        return Plugin.wrap(target, this);
    }

    
    public void setProperties(Properties properties) {
        System.out.println(properties.get("dbType"));
        this.properties = properties;
    }
}

这就是一个最简单的插件,实现了一些简单的打印顺序功能。

配置和运行

使用插件需要 MyBatis 配置文件里面进行配置,如代码清单所示:(注意 plugins 元素的配置顺序,配错了顺序系统就会报错)


    
        
    

显然,我们需要清楚配置的哪个类是插件。它会去解析注解,知道拦截哪个对象、方法和方法的参数,在初始化的时候就会调用 setProperties 方法,初始化参数。

运行一个插入数据的操作,日志打印如下:

package com.mybatis.chap2.main;

import java.io.IOException;

import org.apache.ibatis.session.SqlSession;

import com.mybatis.chap2.mapper.RoleMapper;
import com.mybatis.chap2.po.Role;
import com.mybatis.chap2.util.SqlSessionFactoryUtil;
public class Chapter2Main {

    public static void main(String[] args) throws IOException{
        SqlSession sqlSession = null;
        try {
            sqlSession = SqlSessionFactoryUtil.openSqlSession();
            RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
            Role role = new Role();
            role.setId(4L);
            role.setRoleName("AAA");
            role.setNote("AAAA");
            roleMapper.insertRole(role);
            sqlSession.commit();
        } catch (Exception ex) {
            System.err.println(ex.getMessage());
            sqlSession.rollback();
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
    }

}

通过日志可以清晰地看到 MyBatis 调度插件的顺序。

插件实例

通过插件来配置数据库查询返回的数据量

首先要确定需要拦截四大对象中的哪一个,根据功能我们需要修改 SQL 的执行。由 SqlSession 运行原理可知我们需要拦截 StatementHandler 对象,因为是由它的 prepare 方法来预编译 SQL 语句的,我们可以在预编译前修改语句来满足我们的需求。

代码清单如下:

package com.mybatis.chap2.plugin;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.metaObject;
import org.apache.ibatis.reflection.SystemmetaObject;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Properties;

@Intercepts({@Signature(type = StatementHandler.class, // 确定要拦截的对象
        method = "prepare", // 确定要拦截的方法
        args = {Connection.class, Integer.class}) // 确定要拦截的参数
 })
public class QueryLimitPlugin implements Interceptor {

    
    private int limit;

    private String dbType;

    
    public static final String LMT_TABLE_NAME = "limit_Table_Name_xxx";

    public Object intercept(Invocation invocation) throws Throwable {
        // 取出被拦截对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        metaObject metaStatementHandler = SystemmetaObject.forObject(statementHandler);
        // 分离代理对象,从而形成多次代理,通过两次循环最原始的被代理类,MyBatis 使用的是 JDK 代理
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemmetaObject.forObject(object);
        }
        // 分离最后一个代理对象的目标类
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = SystemmetaObject.forObject(object);
        }
        // 取出即将要执行的 SQL
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        String limitSql;
        // 判断参数是不是 MySQL 数据库且 SQL 有没有被插件重写过
        if ("mysql".equals(this.dbType) && sql.indexOf(LMT_TABLE_NAME) == -1) {
            // 去掉前后空格
            sql = sql.trim();
            // 将参数写入 SQL
            limitSql = "select * from (" + sql + ")" + LMT_TABLE_NAME + "limit" + limit;
            // 重写要执行的 SQL
            metaStatementHandler.setValue("delegate.boundSql.sql",limitSql);
        }
        // 调用原来对象的方法,进入责任链的下一层级
        return invocation.proceed();
    }

    public Object plugin(Object target) {
        // 使用默认的 MyBatis 提供的类生成代理对象
        return Plugin.wrap(target, this);
    }

    public void setProperties(Properties properties) {
        String strLimit = properties.getProperty("limit", "50");
        this.limit = Integer.parseInt(strLimit);
        // 这里读取配置的数据库类型
        this.dbType = properties.getProperty("dbType", "mysql");
    }
}

在 setProperties 方法中可以读入配置给插件的参数,一个是数据库的名称,另外一个是限制的记录数。从初始化代码可知,它在 MyBatis 初始化的时候就已经被设置进去了,在需要的时候我们可以直接使用它。

在 plugin 方法里,我们使用了 MyBatis 提供的类来生成代理对象。那么插件就会进入 plugin 的 invoke 方法,它最后会使用到拦截器的 intercept 方法。

在插件的 intercept 方法就会覆盖掉 StatementHandler 的 prepare 方法,我们先从代理对象分离出真实对象,然后根据需要修改 SQL,来达到限制返回行数的需求。最后使用 invocation.proceed()来调度真实 StatementHandler 的 prepare 方法完成 SQL 预编译,最后需要在 MyBatis 配置文件里面配置才能运行这个插件,如代码清单所示:

配置 log4j 日志,运行一个查询语句,可以得到下面的日志信息

log4j.properties 配置文件

log4j.rootLogger=DEBUG , stdout
log4j.logger.org.mybatis=DEBUG
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n

Maven 依赖


    log4j
    log4j
    1.2.17

在通过反射调度 prepare()方法之前,SQL 被我们的插件重写了,所以无论什么查询都只能返回至多 50 条数据,这样就可以限制一条语句的返回记录行数,插件运行成功。

MyBatis 插件总结

1.能不用插件尽量不要用插件,因为它将修改 MyBatis 的底层设计。
2.插件生成的是层层代理对象的责任链模式,通过反射方法运行,性能不高,所以减少插件就能减少代理,从而提高系统的性能。
3.编写插件需要了解 MyBatis 的运行原理,了解四大对象及其方法的作用,准确判断需要拦截什么对象,什么方法,参数是什么,才能确定签名如何编写。
4.在插件中往往需要读取和修改 MyBatis 映射器中的对象属性,要熟练掌握 MyBatis 映射器内部组成的知识。
5.插件的代码编写要考虑全面,特别是多个插件层层代理的时候,要保证逻辑的正确性。
6.尽量少改动 MyBatis 底层的东西,以减少错误的发送。

常见面试题 1. 为什么 MyBatis 只用 Mapper 接口便能够运行 SQL?

因为映射器的 XML 文件的命名空间对应的便是这个接口的全路径,那么它根据全路径和方法名便能够绑定起来,通过动态代理技术,让这个接口跑起来。而后采用命令模式,最后还是使用 SqlSession 接口的方法使得它能够执行查询,有了这层封装我们便可以使用接口编程,这样编程就更简单了。

2. #{}和 ${}的区别是什么?

${} 是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于静态文本替换,比如 ${driver}会被静态替换为 com.mysql.jdbc.Driver。#{} 是 sql 的参数占位符,Mybatis 会将 sql 中的 #{} 替换为? 号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的? 号占位符设置参数值,比如 ps.setInt(0, parameterValue),#{item.name} 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 param.getItem().getName()。 3. Xml 映射文件中,除了常见的 select|insert|updae|delete 标签之外,还有哪些标签?

还有很多其他的标签,,加上动态 sql 的 9 个标签,trim|where|set|foreach|if|choose|when|otherwise|bind 等,其中为 sql 片段标签,通过 标签引入 sql 片段, 为不支持自增的主键生成策略标签。

4. 最佳实践中,通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

Dao 接口,就是人们常说的 Mapper 接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement 的 id 值,接口方法内的参数,就是传递给 sql 的参数。Mapper 接口是没有实现类的,当调用接口方法时,接口全限名 + 方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id = findStudentById 的 MappedStatement。在 Mybatis 中,每一个 标签均会被解析为 MappedStatement 对象,标签内的 sql 会被解析为 BoundSql 对象。

19. 为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?

Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/776893.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号