1. 插件简介
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。一是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。
以 MyBatis
为例,我们可基于 MyBatis
插件机制实现分页、分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。
2. Mybatis 插件介绍
Mybatis
作为⼀个应用广泛的优秀的 ORM
开源框架,这个框架具有强大的灵活性,在四大组件 (Executor、StatementHandler、ParameterHandler、ResultSetHandler)
处提供了简单易用的插件扩展机制。Mybatis
对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进行拦截,对 mybatis
来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象
。
MyBatis
所允许拦截的方法如下:
- 执行器
Executor
(update
、query
、commi
t、rollback
等方法); -
SQL
语法构建器StatementHandler
(prepare
、parameterize
、batch
、updates
、query
等方法); - 参数处理器
ParameterHandler
(getParameterObject
、setParameters
方法); - 结果集处理器
ResultSetHandler
(handleResultSets``handleOutputParameters
等方法);
3. Mybatis 插件原理
3.1 在四大对象创建的时候
- 每个创建出来的对象不是直接返回的,而是
interceptorChain.pluginAll(parameterHandler);
- 获取到所有的
Interceptor
(拦截器),插件需要实现的接口;调用interceptor.plugin(target)
;返回target
包装后的对象 - 插件机制,我们可以使用插件为目标对象创建⼀个代理对象;
AOP
(面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每⼀个执行;
3.2 拦截
插件具体是如何拦截并附加额外的功能的呢?以 ParameterHandler
来说
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
),是mybatis
初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)
中的 target
就可以理解为mybatis
中的四大对象。返回的 target
是被重重代理后的对象
如果我们想要拦截 Executor
的 query
方法,那么可以这样定义插件:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args= {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
//省略逻辑
}
除此之外,我们还需将插件配置到 sqlMapConfig.xml
中。
这样 MyBatis
在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain
,拦截器链) 中。待准备工作做完后,MyBatis
处于就绪状态。我们在执行 SQL
时,需要先通过 DefaultSqlSessionFactory
创建 SqlSession
。Executor
实例会在创建 SqlSession
的过程中被创建, Executor
实例创建完毕后,MyBatis
会通过 JDK
动态代理为实例生成代理类。这样,插件逻辑即可在 Executor
相关方法被调用前执行。
以上就是MyBatis插件机制的基本原理
4. 自定义插件
4.1 插件接口
Mybatis
插件接口 Interceptor
-
Intercept
方法,插件的核心方法 -
plugin
方法,生成target
的代理对象 -
setProperties
方法,传递插件所需参数
4.2 自定义插件
- 设计实现一个自定义插件
package com.study.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
/**
* 自定义插件
* @author xiaosong
* @since 2021/4/14
*/
@Intercepts({ //注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
@Signature(
type = StatementHandler.class,//这是指拦截哪个接口
method = "prepare", //这个接口内的哪个方法名,不要拼错了
args = {Connection.class,Integer.class} // 这是拦截的方法的⼊参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
)
})
public class MyPlugin implements Interceptor {
/**
* //这里是每次执⾏操作的时候,都会进行这个拦截器的方法内
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
//增强逻辑
System.out.println("对方法进⾏了增强....");
//执行原方法
return invocation.proceed();
}
/**
* 主要是为了把这个拦截器⽣成一个代理放到拦截器链中,包装⽬标对象 为⽬标对象创建代理对象
* @param target 为要拦截的对象
* @return 代理对象
*/
@Override
public Object plugin(Object target) {
System.out.println("将要包装的目标对象:"+target);
return Plugin.wrap(target,this);
}
/**
* 获取配置文件的属性
* 插件初始化的时候调用,也只调用⼀次,插件配置的属性从这里设置进来
* @param properties
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:"+properties);
}
}
sqlMapConfig.xml
-
mapper
接口
public interface UserMapper {
List selectUser();
}
- 测试类
package com.study.mapper;
import com.study.pojo.User;
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 java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* 缓存的测试
* @author xiaosong
* @since 2021/4/12
*/
public class PluginTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void before() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testSelect(){
//根据 sqlSessionFactory 产生 session
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
}
}
-
执行顺序
5. 源码分析
- 执行插件逻辑
Plugin
实现了InvocationHandler
接口,因此它的invoke
方法会拦截所有的方法调用。invoke
方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获取被拦截方法列表,比如:signatureMap.get(Executor.class), 可能返回 [query, update,commit]
Set methods = signatureMap.get(method.getDeclaringClass());
//检测方法列表是否包含被拦截的方法
if (methods != null && methods.contains(method)) {
//执行插件逻辑
return interceptor.intercept(new Invocation(target, method, args));
}
//执行被拦截的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
invoke
方法的代码比较少,逻辑不难理解。首先, invoke
方法会检测被拦截方法是否配置在插件的 @Signature
注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在 intercept
中,该方法的参数类型为 Invocation
,Invocation
主要用于存储目标类,方法以及方法参数列表。下面简单看⼀下该类的定义
package org.apache.ibatis.plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author Clinton Begin
*/
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
// 调用被拦截的方法
return method.invoke(target, args);
}
}
6. pageHelper 分页插件
MyBatis
可以使用第三方的插件来对功能进行扩展,分页助手
PageHelper
是将分的复杂操作进行封装,使用简单的方式即可获得分页的相关数据。
开发步骤:
① 导入通用 PageHelper
的坐标
② 在 mybatis
核心配置文件中配置 PageHelper
插件
③ 测试分页数据获取
代码实现:
- 导入通用
PageHelper
坐标
com.github.pagehelper
pagehelper
4.2.1
- 在
mybatis
核心配置文件中配置PageHelper
插件
- 测试分页代码实现
@Test
public void testPageHelper(){
PageHelper.startPage(1,1);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
}
- 获得分页相关的其他参数
@Test
public void testPageHelper(){
PageHelper.startPage(1,1);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
// 其它分页数据
PageInfo 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());
}
7. 通用 Mapper
7.1 什么是通用 Mapper
通用 Mapper
就是为了解决单表增删改查,基于 Mybatis
的插件机制。开发人员不需要编写 SQL
,不需要在 DAO
中增加方法,只要写好实体类,就能支持相应的增删改查方法
7.2 如何使用
- 首先在
maven
项目,在pom.xml
中引入mapper
的依赖
tk.mybatis
mapper
3.1.2
-
Mybatis
配置文件中完成配置
- 实体类设置主键
package com.study.pojo;
import lombok.Data;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.List;
/**
* 用户的实体类
* @author xiaosong
* @since 2021/4/1
*/
@Data
@Table(name = "user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
private String birthday;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", birthday='" + birthday + '\'' +
'}';
}
}
- 定义通用
mapper
package com.study.mapper;
import com.study.pojo.User;
import tk.mybatis.mapper.common.Mapper;
import java.util.List;
/**
* 用户的持久层
* @author xiaosong
* @since 2021/4/12
*/
public interface UserMapper extends Mapper {
/**
* 查询用户信息
* @return List
*/
List findUserList();
}
- 测试
@Test
public void testMapper(){
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setId(1);
// mapper 基础接口, select
// 根据实体中的属性进行查询,只能有一个返回值
User user1 = userMapper.selectOne(user);
System.out.println(user1);
// 参数为 null 时,查询全部
List userList = userMapper.select(null);
for (User user2 : userList) {
System.out.println(user2);
}
// 根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号
User user2 = userMapper.selectByPrimaryKey(1);
System.out.println(user2);
//根据实体中的属性查询总数,查询条件使用等号
int count = userMapper.selectCount(user);
System.out.println("总数" + count);
// mapper 基础接口, insert
//保存一个实体,null值也会保存,不会使用数据库默认值
user.setId(3);
int insertCount = userMapper.insert(user);
System.out.println("插入条数" + insertCount);
//保存实体,null的属性不会保存,会使用数据库默认值
user.setId(4);
int insertCount1 = userMapper.insertSelective(user);
System.out.println("插入条数" + insertCount1);
// mapper 基础接口, update
//根据主键更新实体全部字段, null值会被更新
user.setId(4);
int updateCount = userMapper.updateByPrimaryKey(user);
System.out.println("更新条数" + updateCount);
// mapper 基础接口, delete
//根据实体属性作为条件进行删除,查询条件使用等号
int deleteCount = userMapper.delete(user);
System.out.println("删除条数" + deleteCount);
// example方法
Example example = new Example(User.class);
example.createCriteria().andEqualTo("id",1)
.andLike("username","E%");
List userList1 = userMapper.selectByExample(example);
for (User user3 : userList1) {
System.out.println(user3);
}
}
8. 案例代码
详情参考
:https://gitee.com/xiaosonglab/mybatis-plugins.git