Mybatis系列之Mybatis插件
插件简介
一般情况下,开源框架都会提供插件或其他形式的扩展点,供开发者自行扩展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯ 作。以MyBatis为例,我们可基于MyBati s插件机制实现分⻚、分表,监控等功能。由于插件和业务 ⽆ 关,业务也⽆法感知插件的存在。因此可以⽆感植⼊插件,在⽆形中增强功能。
Mybatis插件介绍
在 MyBatis 中,四大核心对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)负责处理数据库操作的不同阶段,如执行 SQL 语句、处理参数、处理结果集等。MyBatis 通过动态代理来生成这些核心对象的代理对象,从而实现对其进行拦截和增强的能力。
当我们使用 MyBatis 插件时,实际上就是通过动态代理来生成这些核心对象的代理对象,并在代理对象中嵌入我们自定义的逻辑。当调用代理对象的方法时,拦截器会在核心对象执行前后插入自定义的逻辑,实现对核心对象功能的增强。
具体而言,拦截器在拦截器链中的位置是在核心对象前后,它可以拦截核心对象方法的调用,对方法的参数、执行过程、返回结果进行拦截和修改。拦截器通过实现 MyBatis 的 Interceptor 接口,并重写 intercept 方法来实现拦截逻辑。在 intercept 方法中,可以通过调用核心对象的相应方法,以及自定义的逻辑来完成拦截和增强的操作。
通过拦截器的动态代理机制,MyBatis 可以灵活地将我们自定义的逻辑嵌入到核心对象的执行过程中,从而实现对数据库操作的定制化和增强。这使得我们可以在不修改 MyBatis 核心代码的情况下,对核心对象的行为进行拦截和修改,满足各种特定的需求,如日志记录、性能监控、缓存优化等。
MyBatis 允许使用插件来拦截的方法:
- 执行器 Executor (update, query, commit, rollback, flushStatements, getTransaction, close, isClosed等方法)
- SQL语法构建器 ParameterHandler (getParameterObject, setParameters等方法)
- 参数处理器 ResultSetHandler (handleResultSets, handleOutputParameters等方法)
- 结果集处理器 StatementHandler (prepare, parameterize, batch, update, query等方法)
Mybatis 插件原理
在四大对象创建的时候:
- 在对象创建过程中,每个对象不是直接返回,而是经过
interceptorChain.pluginAll(parameterHandler)的处理。 这个方法会遍历所有的插件(实现了插件接口Interceptor的类),并调用每个插件的plugin方法。 interceptor.plugin(target)方法会返回一个包装后的对象。在该方法中,插件可以使用动态代理的方式为目标对象创建代理对象。 代理对象具备拦截和增强的能力,可以拦截目标对象的方法调用,并在方法执行前后插入自定义的逻辑。- 插件机制可以通过为四大对象创建代理对象来实现 AOP(面向切面)的效果。 通过插件,我们可以在四大对象的每个执行点上插入自定义的逻辑,从而实现对这些对象行为的拦截、修改和增强。
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
在拦截器链(InterceptorChain)中,保存了所有的插件(interceptors)。当调用 interceptorChain.pluginAll(target) 时,会依次调用拦截器链中的每个插件的 plugin 方法,并传入目标对象。每个插件可以在 plugin 方法中对目标对象进行拦截和增强操作,最后返回经过拦截和增强后的代理对象。
如果我们想要拦截Executor的query⽅法,那么可以这样定义插件:
com.amber.plugin.ExamplePlugin
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args=
{MappedStatement.class,Object.class,RowBounds.class, ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
//省略逻辑
}
以上代码是一个示例的 MyBatis 插件类。使用 @Intercepts 注解标注了要拦截的目标方法,即 Executor 接口中的 query 方法。@Signature 注解指定了拦截方法的签名,包括方法的类型、方法名和参数类型列表。lk
除此之外,我们还需将插件配置到sqlMapConfig.xm l中:
<plugins>
<plugin interceptor="com.amber.plugin.ExamplePlugin">
</plugin>
</plugins>
这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备⼯作做完后,MyBatis处于就绪状态。我们在执⾏SQL时,需要先通过DefaultSqlSessionFactory 创建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后, MyBatis会通过JDK动态代理为实例⽣成代理类。这样,插件逻辑即可在 Executor相关⽅法被调⽤前执⾏。 以上就是MyBatis插件机制的基本原理.
自定义插件
插件接口
Mybatis 插件接口 Interceptor
- intercept 方法,插件的核心方法
- plugin 方法,生成 target 的代理对象
- setProperties 方法,传递插件所需参数
自定义插件
设计实现一个自定义插件
MyPlugin
// 在四大对象创建的时候,通过插件机制对其进行拦截和增强
// 1. 每个创建出来的对象不是直接返回的,而是通过 InterceptorChain.pluginAll(parameterHandler) 进行拦截
// 2. 获取到所有的 Interceptor(拦截器),调用 interceptor.plugin(target) 返回 target 包装后的对象
// 3. 插件机制使用动态代理为目标对象创建代理对象,可以拦截目标对象的每一个执行
// 实质上就是 AOP(面向切面)的应用,插件可以为四大对象创建代理对象,代理对象可以拦截每个执行的细节
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//在目标方法执行前进行拦截和增强的逻辑
System.out.println("方法前增强");
//调用原方法
Object proceed = invocation.proceed();
//在目标方法执行后进行拦截和增强的逻辑
System.out.println("方法后增强");
return proceed;
}
/**
*把当前的拦截器生成的代理存到拦截器链中 也就是InterceptorChain
*/
@Override
public Object plugin(Object target) {
// 使用 Plugin.wrap 方法对目标对象进行包装,返回代理对象
Object wrap = Plugin.wrap(target, this);
return wrap;
}
/**获取配置⽂件的属性**/
//插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来
@Override
public void setProperties(Properties properties) {
this.properties = properties;
// 获取配置的属性值
String name = properties.getProperty("name");
System.out.println("插件配置的name属性值为:" + name);
}
}
sqlMapConfig.xml
<plugins>
<plugin interceptor="com.amber.plugin.MyPlugin">
<property name="name" value="tom"/>
</plugin>
</plugins>
IUserDao
public interface IUserDao {
//查询所有用户
public List<User> findAll() throws IOException;
}
UserMapper.xml
<!--查询用户-->
<select id="findAll" resultType="user">
select * from user
</select>
PluginTest
public class PluginTest {
@Test
public void test() throws IOException {
InputStream resourceAsStream =
Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
List<User> userList = userDao.findAll();
for (User user : userList) {
System.out.println(user);
}
}
}
运行结果
插件配置的初始化参数:{name=tom}
方法前增强
15:13:22,018 DEBUG findAll:159 - ==> Preparing: select * from user
方法后增强
15:13:22,042 DEBUG findAll:159 - ==> Parameters:
15:13:22,095 DEBUG findAll:159 - <== Total: 1
User(id=1, username=张三)
源码分析
执行插件逻辑
Plugin实现了InvocationHandler接口,因此他的invoke方法会拦截所有的方法调用。invoke方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取拦截器中与方法所在类对应的拦截目标方法集合。
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//判断当前方法是否在拦截器目标方法集合中
if (methods != null && methods.contains(method)) {
//如果当前方法是拦截目标方法,则通过拦截器的 intercept 方法进行拦截处理。
return interceptor.intercept(new Invocation(target, method, args));
}
//如果当前方法不是拦截目标方法,则直接调用原始目标对象的方法。
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
pageHelper分页插件
MyBati s可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿PageHelper是将分⻚的复杂操作进⾏封 装,使⽤简单的⽅式即可获得分⻚的相关数据
1.导入通用PageHelper坐标
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>3.7.5</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>0.9.1</version>
</dependency>
2.在mybatis核⼼配置⽂件中配置PageHelper插件
<plugins>
<!--注意:分⻚助⼿的插件 配置在通⽤mapper之前-->
<plugin interceptor="com.github.pagehelper.PageHelper">
<!--指定方言-->
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
3.测试
@Test
public void testPageHelper() throws IOException {
//设置分⻚参数
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
PageHelper.startPage(1, 2);
List<User> userList = userDao.findAll();
for (User user : userList) {
System.out.println(user);
}
PageInfo<User> pageInfo = new PageInfo<>(userList);
System.out.println("总条数:"+pageInfo.getTotal());
System.out.println("总⻚数:"+pageInfo. getPages ());
System.out.println("当前⻚:"+pageInfo. getPageNum());
System.out.println("每⻚显示⻓度:"+pageInfo.getPageSize());
System.out.println("是否第⼀⻚:"+pageInfo.isIsFirstPage());
System.out.println("是否最后⼀⻚:"+pageInfo.isIsLastPage());
}
4.结果
16:31:39,019 DEBUG PooledDataSource:406 - Created connection 1997357673.
16:31:39,020 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@770d4269]
16:31:39,022 DEBUG findAll_PageHelper_Count:159 - ==> Preparing: SELECT count(*) FROM user
16:31:39,049 DEBUG findAll_PageHelper_Count:159 - ==> Parameters:
16:31:39,100 DEBUG findAll_PageHelper_Count:159 - <== Total: 1
16:31:39,101 DEBUG findAll_PageHelper:159 - ==> Preparing: select * from user limit ?,?
16:31:39,102 DEBUG findAll_PageHelper:159 - ==> Parameters: 0(Integer), 2(Integer)
16:31:39,104 DEBUG findAll_PageHelper:159 - <== Total: 2
User(id=1, username=张三)
User(id=2, username=李四)
总条数:4
总⻚数:2
当前⻚:1
每⻚显示⻓度:2
是否第⼀⻚:true
是否最后⼀⻚:false