插件原理回顾
在前面,我们通过 MyBatis插件机制介绍与原理 分析了 MyBatis 插件的基本原理,但是可能还只是理论上的分析,没有实战的锻炼可能理解的还是不够透彻。接下来,我们通过自定义插件实例来进一步深度理解 MyBatis 插件的插件机制。
插件接口
-
MyBatis 插件接口-Interceptor 有哪些方法?
-
intercept
方法,插件的核心方法 -
plugin
方法 -
setProperties
方法
-
自定义插件
现在,我们从零开始,设计实现一个自定义插件。
-
新建一个 Maven 项目,然后导入 Mybatis 对应 jar 包
<!--mybatis坐标--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>org.jboss</groupId> <artifactId>jboss-vfs</artifactId> <version>3.2.15.Final</version> </dependency> <!--mysql驱动坐标--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> <scope>runtime</scope> </dependency>
-
接下来,完善 sqlMapConfig.xml、jdbc.properties 等
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 加载外部的propeties文件 -->
<properties resource="jdbc.properties"/>
<settings>
<!-- 输出日志 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 为实体的全限定类名取别名 -->
<typeAliases>
<!-- 给单独的实体起别名 -->
<!-- <typeAlias type="space.terwer.pojo.User" alias="user"/> -->
<!-- 批量起别名:改包下所有类本身的类名,不区分大小写 -->
<package name="space.terwe.pojo"/>
</typeAliases>
<!-- environments:运行环境 -->
<environments default="development">
<environment id="development">
<!-- 当前事务交给JDBC管理 -->
<transactionManager type="JDBC"/>
<!-- 当前使用MyBatis提供的连接池 -->
<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>
<!--
<mapper class="space.terwer.mapper.IUserMapperr"/>
-->
<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();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!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>
<!-- 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 初稿