大纲 MyBatis 四大对象 四大对象介绍 MyBatis 的四大对象包括:Executor、StatementHandler、ParameterHandler、ResultSetHandler。四大对象的工作职责如下:
Executor(执行器)
:负责整个 SQL 执行过程的总体控制StatementHandler(语句处理器)
:负责和 JDBC 层交互,包括预编译 SQL 语句和执行 SQL 语句,以及调用 ParameterHandler
设置参数ParameterHandler(参数处理器)
:负责设置预编译参数ResultSetHandler(结果集处理器)
:负责将 JDBC 查询结果映射到 JavaBean 对象四大对象的工作流程
MyBatis 插件开发 本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-lesson-20
。
插件介绍 MyBatis 在四大对象的创建过程中,都允许有插件进行介入。插件可以利用动态代理机制一层层的包装目标对象,从而实现在目标对象指向目标方法之前进行拦截的效果。 MyBatis 自定义插件是非常简单的,只需实现 Interceptor
接口,并指定想要拦截哪个对象的哪个方法,即可介入四大对象的任何一个方法的执行。 MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用。 默认情况下,MyBatis 允许使用插件来拦截的方法包括:Executor
:update, query, flushStatements, commit, rollback, getTransaction, close, isClosedStatementHandler
:prepare, parameterize, batch, update, queryParameterHandler
:getParameterObject, setParametersResultSetHandler
:handlerResultSets, handlerOuutputParameters 特别注意
自定义 MyBatis 插件时,如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。这些都是更底层的类和方法,所以使用插件的时候需要特别小心。
插件原理 在 MyBatis 的全局配置文件里注册插件后,会按照插件的配置顺序,依次调用插件的 plugin()
方法来生成被拦截对象的动态代理对象 存在多个插件时,会依次生成目标对象的动态代理对象,层层包裹,先声明的先包裹,以此形成代理链 目标方法执行时,是按照插件配置信息的逆向顺序来执行 intercept()
方法,即先配置的插件会后执行 使用多个插件的情况下,往往需要在某个插件中分离出来目标对象,可以借助 MyBatis 提供的 SystemMateObject
类来获取最后一层的 h
以及 target
属性的值 插件开发案例 Interceptor 接口
Interceptor.intercept()
:拦截目标方法的执行Interceptor.plugin()
:创建动态代理对象,可以使用 MyBatis 提供的 Plugin
类的 wrap
方法Interceptor.setProperties()
:注入配置插件时设置的属性插件开发案例一 实现 Interceptor
接口,并通过插件签名指定要拦截哪个对象的哪个方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package com.clay.mybatis.plugin;import java.sql.Connection;import java.util.Properties;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Plugin;import org.apache.ibatis.plugin.Signature;@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) }) public class MyFirstPlugin implements Interceptor { private Properties properties = new Properties(); @Override public Object intercept (Invocation invocation) throws Throwable { System.out.println("MyFirstPlugin ==> " + invocation.getTarget().getClass().getName() + "." + invocation.getMethod().getName() + "() 方法准备执行" ); Object result = invocation.proceed(); System.out.println("MyFirstPlugin ==> " + invocation.getTarget().getClass().getName() + "." + invocation.getMethod().getName() + "() 方法执行完成" ); return result; } @Override public Object plugin (Object target) { System.out.println("MyFirstPlugin ==> 要包装的对象: " + target); Object wrap = Plugin.wrap(target, this ); return wrap; } @Override public void setProperties (Properties properties) { this .properties = properties; } }
1 2 3 4 5 6 7 8 9 10 <configuration > <plugins > <plugin interceptor ="com.clay.mybatis.plugin.MyFirstPlugin" > <property name ="name" value ="FirstPlugin" /> </plugin > </plugins > </configuration >
上面自定义的插件将会拦截在 StatementHandler
实例中所有的 prepare()
方法调用,MyBatis 执行普通的 SQL 查询语句后,控制台输出的日志信息如下: 1 2 3 4 5 6 7 8 9 10 11 12 MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.CachingExecutor@69fb6037 MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.scripting.defaults.DefaultParameterHandler@11bd0f3b MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.resultset.DefaultResultSetHandler@696da30b MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.statement.RoutingStatementHandler@4e7912d8 22:16:06,391 DEBUG JdbcTransaction:137 - Opening JDBC Connection 22:16:06,755 DEBUG PooledDataSource:434 - Created connection 208684473. 22:16:06,755 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c7045b9] MyFirstPlugin ==> org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare() 方法准备执行 22:16:06,761 DEBUG getEmpById:137 - ==> Preparing: select id, last_name as lastName, gender, email from t_employee where id = ? MyFirstPlugin ==> org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare() 方法执行完成 22:16:06,801 DEBUG getEmpById:137 - ==> Parameters: 1(Long) 22:16:06,831 DEBUG getEmpById:137 - <== Total: 0
插件开发案例二 在下面的案例代码里,演示了如何动态更改 SQL 语句运行时的参数,例如查询员工信息时,动态更改查询的员工 ID 为 11
。
1 2 3 4 5 public interface EmployeeMapper { public Employee getEmpById (Long id) ; }
1 2 3 4 5 6 7 8 9 <mapper namespace ="com.clay.mybatis.dao.EmployeeMapper" > <select id ="getEmpById" parameterType ="Long" resultType ="com.clay.mybatis.bean.Employee" > select id, last_name as lastName, gender, email from t_employee where id = #{id} </select > </mapper >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.clay.mybatis.plugin;import java.sql.Connection;import java.util.Properties;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Plugin;import org.apache.ibatis.plugin.Signature;import org.apache.ibatis.reflection.MetaObject;import org.apache.ibatis.reflection.SystemMetaObject;@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) }) public class MyThirdPlugin implements Interceptor { private Properties properties = new Properties(); @Override public Object intercept (Invocation invocation) throws Throwable { Object target = invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(target); metaObject.setValue("parameterHandler.parameterObject" , 11L ); Object proceed = invocation.proceed(); return proceed; } @Override public Object plugin (Object target) { Object wrap = Plugin.wrap(target, this ); return wrap; } @Override public void setProperties (Properties properties) { this .properties = properties; } }
1 2 3 4 5 6 7 8 9 10 <configuration > <plugins > <plugin interceptor ="com.clay.mybatis.plugin.MyThirdPlugin" > <property name ="name" value ="MyThirdPlugin" /> </plugin > </plugins > </configuration >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MyBatisApplication { public static void main (String[] args) throws IOException { String resource = "mybatis-config.xml" ; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = sqlSessionFactory.openSession(); try { EmployeeMapper mapper = session.getMapper(EmployeeMapper.class); mapper.getEmpById(5L ); } finally { if (session != null ) { session.close(); } } } }
1 2 3 22:36:57,053 DEBUG getEmpById:137 - ==> Preparing: select id, last_name as lastName, gender, email from t_employee where id = ? 22:36:57,089 DEBUG getEmpById:137 - ==> Parameters: 11(Long) 22:36:57,118 DEBUG getEmpById:137 - <== Total: 0
多个插件的执行顺序 MyBatis 在创建动态代理时,是按照插件的配置顺序依次调用 plugin()
方法创建层层的代理对象,但插件的 intercept()
方法是按照配置信息的逆向顺序来执行,即先配置的插件会后执行。
在上面案例代码的基础上,增加一个 MyBatis 插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.clay.mybatis.plugin;import java.sql.Connection;import java.util.Properties;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Plugin;import org.apache.ibatis.plugin.Signature;@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) }) public class MySecondPlugin implements Interceptor { private Properties properties = new Properties(); @Override public Object intercept (Invocation invocation) throws Throwable { System.out.println("MySecondPlugin ==> " + invocation.getTarget().getClass().getName() + "." + invocation.getMethod().getName() + "() 方法准备执行" ); Object result = invocation.proceed(); System.out.println("MySecondPlugin ==> " + invocation.getTarget().getClass().getName() + "." + invocation.getMethod().getName() + "() 方法执行完成" ); return result; } @Override public Object plugin (Object target) { System.out.println("MySecondPlugin ==> 要包装的对象: " + target); Object wrap = Plugin.wrap(target, this ); return wrap; } @Override public void setProperties (Properties properties) { this .properties = properties; } }
在 MyBatis 的全局配置文件中同时注册多个插件 1 2 3 4 5 6 7 8 9 10 11 12 13 <configuration > <plugins > <plugin interceptor ="com.clay.mybatis.plugin.MyFirstPlugin" > <property name ="name" value ="FirstPlugin" /> </plugin > <plugin interceptor ="com.clay.mybatis.plugin.MySecondPlugin" > <property name ="name" value ="SecondPlugin" /> </plugin > </plugins > </configuration >
MyBatis 执行普通的 SQL 查询语句后,控制台输出的日志信息如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.CachingExecutor@36d585c MySecondPlugin ==> 要包装的对象: org.apache.ibatis.executor.CachingExecutor@36d585c MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.scripting.defaults.DefaultParameterHandler@c333c60 MySecondPlugin ==> 要包装的对象: org.apache.ibatis.scripting.defaults.DefaultParameterHandler@c333c60 MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.resultset.DefaultResultSetHandler@4e7912d8 MySecondPlugin ==> 要包装的对象: org.apache.ibatis.executor.resultset.DefaultResultSetHandler@4e7912d8 MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.statement.RoutingStatementHandler@53976f5c MySecondPlugin ==> 要包装的对象: org.apache.ibatis.executor.statement.RoutingStatementHandler@53976f5c 22:57:50,780 DEBUG JdbcTransaction:137 - Opening JDBC Connection 22:57:51,071 DEBUG PooledDataSource:434 - Created connection 261748192. 22:57:51,071 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f99f5e0] MySecondPlugin ==> com.sun.proxy.$Proxy9.prepare() 方法准备执行 MyFirstPlugin ==> org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare() 方法准备执行 22:57:51,078 DEBUG getEmpById:137 - ==> Preparing: select id, last_name as lastName, gender, email from t_employee where id = ? MyFirstPlugin ==> org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare() 方法执行完成 MySecondPlugin ==> com.sun.proxy.$Proxy9.prepare() 方法执行完成 22:57:51,133 DEBUG getEmpById:137 - ==> Parameters: 1(Long) 22:57:51,173 DEBUG getEmpById:137 - <== Total: 0
MyBatis 代码生成器 MyBatis Generator(简称 MBG),是一个专门为 MyBatis 框架使用者定制的代码生成器(逆向工程),可以快速地根据数据库表生成对应的 SQL 映射文件、Mapper 接口以及 JavaBean 类。支持基本的增删改查,以及 QBC 风格的条件查询。但是像数据库表连接、存储过程等这些复杂 SQL 的定义需要开发者手工编写。更多的介绍内容,可查看 GitHub 仓库 和 MyBatis 官方文档 。
准备工作 下述的案例代码是基于以下的数据库表结构编写的,因此需要提前执行 SQL 语句来初始化数据库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 CREATE DATABASE `mybatis_lesson` DEFAULT CHARACTER SET utf8mb4;CREATE TABLE `t_department` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `name` varchar (255 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8; CREATE TABLE `t_employee` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `last_name` varchar (255 ) DEFAULT NULL , `gender` char (1 ) DEFAULT NULL , `email` varchar (255 ) DEFAULT NULL , `dept_id` int (11 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8; insert into t_department(id, name) values (1 , '开发部门' ), (2 , '测试部门' );insert into t_employee(id, last_name, gender, email, dept_id) values (1 , 'Jim' ,'1' , 'jim@gmail.com' , 1 );insert into t_employee(id, last_name, gender, email, dept_id) values (2 , 'Peter' ,'1' , 'peter@gmail.com' , 1 );
使用案例 本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-lesson-19
。
引入 Maven 依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.23</version > </dependency > <dependency > <groupId > org.mybatis.dynamic-sql</groupId > <artifactId > mybatis-dynamic-sql</artifactId > <version > 1.4.0</version > </dependency > <dependency > <groupId > org.mybatis.generator</groupId > <artifactId > mybatis-generator-core</artifactId > <version > 1.4.1</version > </dependency >
创建 XML 配置文件 在项目的 src/test/resources
目录下创建 mybatis-generator.xml
配置文件,其中的配置内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" > <generatorConfiguration > <context id ="MySQLTables" targetRuntime ="MyBatis3Simple" > <jdbcConnection driverClass ="com.mysql.cj.jdbc.Driver" connectionURL ="jdbc:mysql://127.0.0.1:3306/mybatis_lesson" userId ="root" password ="123456" /> <javaTypeResolver > <property name ="forceBigDecimals" value ="false" /> </javaTypeResolver > <javaModelGenerator targetPackage ="com.clay.mybatis.bean" targetProject ="./src/main/java" > <property name ="enableSubPackages" value ="true" /> <property name ="trimStrings" value ="true" /> </javaModelGenerator > <sqlMapGenerator targetPackage ="com.clay.mybatis.dao" targetProject ="./src/main/java" > <property name ="enableSubPackages" value ="true" /> </sqlMapGenerator > <javaClientGenerator type ="XMLMAPPER" targetPackage ="com.clay.mybatis.dao" targetProject ="./src/main/java" > <property name ="enableSubPackages" value ="true" /> </javaClientGenerator > <table tableName ="t_employee" domainObjectName ="Employee" /> <table tableName ="t_department" domainObjectName ="Department" /> </context > </generatorConfiguration >
上述 XML 配置文件里的 targetRuntime="MyBatis3Simple"
表示只生成基础的 CRUD 代码和少量动态 SQL 语句,可选值有 MyBatis3DynamicSql | MyBatis3Kotlin | MyBatis3 | MyBatis3Simple
。值得一提的是,在企业项目开发中,用得最多的是 targetRuntime="MyBatis3"
,它支持复杂条件查询(QBC 查询)和自动生成大量动态 SQL 语句。 MyBatis Generator 完整的 XML 标签介绍可看 这里 。
运行代码生成器 MyBatis Generator (MBG) 可以通过以下方式运行:
这里采用 Java 代码 + XML 配置文件的方式运行代码生成器,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import java.io.File;import java.net.URL;import java.util.ArrayList;import java.util.List;import org.mybatis.generator.api.MyBatisGenerator;import org.mybatis.generator.config.Configuration;import org.mybatis.generator.config.xml.ConfigurationParser;import org.mybatis.generator.internal.DefaultShellCallback;public class GeneratorTest { public static void main (String[] args) throws Exception { List<String> warnings = new ArrayList<String>(); boolean overwrite = true ; URL url = GeneratorTest.class.getClassLoader().getResource("mybatis-generator.xml" ); File configFile = new File(url.getFile()); ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration(configFile); DefaultShellCallback callback = new DefaultShellCallback(overwrite); MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings); myBatisGenerator.generate(null ); } }
最后会在项目的目录内自动生成 JavaBean 类、Mapper 接口和 SQL 映射文件,项目的目录结构图如下:
MyBatis 工作原理 工作原理图一
工作原理图二
工作原理图三
MyBatis 源码分析