在前面,我们通过 MyBatis插件机制介绍与原理 分析了 MyBatis 插件的基本原理,但是可能还只是理论上的分析,没有实战的锻炼可能理解的还是不够透彻。接下来,我们通过自定义插件实例来进一步深度理解 MyBatis 插件的插件机制。
MyBatis 插件接口-Interceptor 有哪些方法?
intercept
方法,插件的核心方法plugin
方法setProperties
方法现在,我们从零开始,设计实现一个自定义插件。
新建一个 Maven 项目,然后导入 Mybatis 对应 jar 包
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.6version>
dependency>
<dependency>
<groupId>org.jbossgroupId>
<artifactId>jboss-vfsartifactId>
<version>3.2.15.Finalversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.16version>
<scope>runtimescope>
dependency>
接下来,完善 sqlMapConfig.xml、jdbc.properties 等
DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"/>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
settings>
<typeAliases>
<package name="space.terwe.pojo"/>
typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
dataSource>
environment>
<environment id="production">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
dataSource>
environment>
environments>
<mappers>
<package name="space.terwer.mapper"/>
mappers>
configuration>
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false
jdbc.username=terwer
jdbc.password=123456
pojo 和 mapper
package space.terwer.pojo;
import java.io.Serializable;
/**
* @author terwer on 2024/6/13
*/
public class User implements Serializable {
private Integer id;
private String username;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
}
package space.terwer.mapper;
import space.terwer.pojo.User;
import java.util.List;
/**
* @author terwer on 2024/6/13
*/
public interface IUserMapper {
/**
* 查询用户
*/
List<User> findAll();
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="space.terwer.mapper.IUserMapper">
<resultMap id="userMap" type="space.terwer.pojo.User">
<result property="id" column="id">result>
<result property="username" column="username">result>
resultMap>
<select id="findAll" resultMap="userMap">
select id, username from user
select>
mapper>
编写测试用例,让 mybatis 先跑起来
package space.terwer;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import space.terwer.mapper.IUserMapper;
import space.terwer.pojo.User;
import java.io.InputStream;
import java.util.List;
import static org.junit.Assert.assertTrue;
/**
* @author terwer on 2024/6/13
*/
public class MainTest {
private IUserMapper userMapper;
private SqlSession sqlSession;
@Before
public void before() throws Exception {
System.out.println("before...");
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = sqlSessionFactory.openSession();
// 这样也是可以的,这样的话后面就不用每次都设置了
// sqlSession = sqlSessionFactory.openSession(true);
userMapper = sqlSession.getMapper(IUserMapper.class);
}
@Test
public void testFindAll() {
List<User> all = userMapper.findAll();
for (User user : all) {
System.out.println(user);
}
}
}
效果如下:
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1ed1993a]
==> Preparing: select id, username from user
==> Parameters:
<== Columns: id, username
<== Row: 1, lisi
<== Row: 2, tom
<== Row: 8, 测试2
<== Row: 9, 测试3
<== Total: 4
User{id=1, username='lisi'}
User{id=2, username='tom'}
User{id=8, username='测试2'}
User{id=9, username='测试3'}
此时,整个项目结构如下:
编写插件 MyPlugin
package space.terwer.plugin;
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.Signature;
import java.sql.Connection;
import java.util.Properties;
/**
* @author terwer on 2024/6/13
*/
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 增强逻辑
System.out.println("这里是插件的增强方法....");
// 执行原方法
return invocation.proceed();
}
/**
* 主要是为了把这个拦截器生成一个代理放到拦截器链中 * ^Description包装目标对象 为目标对象创建代理对象 * @Param target为要拦截的对象
*/
@Override
public Object plugin(Object target) {
System.out.println("将要包装的目标对象:" + target);
return Interceptor.super.plugin(target);
}
/**
* 获取配置文件的属性,插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
**/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:" + properties);
Interceptor.super.setProperties(properties);
}
}
将插件配置到 sqlMapConfig.xm l 中。
<plugins>
<plugin interceptor="space.terwer.plugin.MyPlugin">
<property name="param1" value="value1"/>
plugin>
plugins>
查看效果
Using VFS adapter org.apache.ibatis.io.JBoss6VFS
插件配置的初始化参数:{param1=value1}
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Checking to see if class space.terwer.mapper.IUserMapper matches criteria [is assignable to Object]
将要包装的目标对象:org.apache.ibatis.executor.CachingExecutor@262b2c86
将要包装的目标对象:org.apache.ibatis.scripting.defaults.DefaultParameterHandler@c81cdd1
将要包装的目标对象:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@289d1c02
将要包装的目标对象:org.apache.ibatis.executor.statement.RoutingStatementHandler@17d0685f
Opening JDBC Connection
Created connection 1183888521.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
这里是插件的增强方法....
==> Preparing: select id, username from user
==> Parameters:
<== Columns: id, username
<== Row: 1, lisi
<== Row: 2, tom
<== Row: 8, 测试2
<== Row: 9, 测试3
<== Total: 4
User{id=1, username='lisi'}
User{id=2, username='tom'}
User{id=8, username='测试2'}
User{id=9, username='测试3'}
可以看到,插件确实生效了。
通过上面的自动插件实例,我再来进一步分析一下:
在四大对象创建的时候
1、每个创建出来的对象不是直接返回的,而是 interceptorChain.pluginAll(parameterHandler)
;
2、获取到所有的 Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target)
,返回 target 包装后的对象;
3、插件机制:我们可以使用插件为目标对象创建一个代理对象 AOP (面向切面);我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;
那么,插件具体是如何拦截并附加额外的功能的呢?以 ParameterHandler 来说:
// org.apache.ibatis.session.Configuration
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
interceptorChain
保存了所有的拦截器(interceptors),是 mybatis 初始化的时候创建的。调用拦截器链 中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)
中的 target 就可以理解为 mybatis 中的四大对象。返回 的 target 是被重重代理后的对象。
// org.apache.ibatis.plugin.InterceptorChain
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
例如:如果我们想要拦截 Executor 的 query 方法,那么可以稍微修改一下,这样定义插件:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
// TODO
}
这样 MyBatis 在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis 处于就绪状态。我们在执行 SQL 时,需要先通过 DefaultSqlSessionFactory 创建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor 实例创建完毕后,MyBatis 会通过 JDK 动态代理为 实例生成代理类。这样,插件逻辑即可在 Executor 相关方法被调用前执行。
-- show databases;
-- select version();
-- drop user 'terwer'@'%';
-- CREATE USER 'terwer'@'%' IDENTIFIED BY '123456';
-- GRANT ALL PRIVILEGES ON *.* TO 'terwer'@'%' WITH GRANT OPTION;
-- flush privileges;
-- create database test default character set utf8 collate utf8_general_ci;
-- user
create table if not exists user
(
id int auto_increment
primary key,
username varchar(50) null,
password varchar(50) null,
birthday varchar(50) null
)
charset = utf8;
-- user data
INSERT INTO test.user (id, username, password, birthday) VALUES (1, 'lisi', '123', '2019-12-12');
INSERT INTO test.user (id, username, password, birthday) VALUES (2, 'tom', '123', '2019-12-12');
INSERT INTO test.user (id, username, password, birthday) VALUES (8, '测试2', null, null);
INSERT INTO test.user (id, username, password, birthday) VALUES (9, '测试3', null, null);
mybatis-plugin
文章更新历史
2024/06/13 初稿