Mybatis的自定义插件原理
1.插件简介
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例, 我们可基于MyBatis插件机制实现分页、分表, 监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。
2.Mybatis插件介绍
Mybatis作为一个应用广泛的优秀的ORM开源框架, 这个框架具有强大的灵活性, 在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler) 处提供了简单易用的插件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进行拦截, 对mybatis而言所谓的插件就是拦截器, 用来增强核心对象的功能, 增强功能本质上是借助于底层的动态代理实现的, 换句话说, MyBatis中的四大对象都是代理对象。代理对象当调用执行方法时候都会调用getMapper中的invoke方法。然后返回的都是经过层层代理的代理对象。
换句话说:mybatis允许自定义插件对上图的四大核心对象进行拦截。
MyBatis所允许拦截的方法如下:
所允许拦截的方法,也就是这四大核心对象里面的方法。
- 执行器Executor(update、query、commit、rollback等方法) ;
- SQL语法构建器StatementHandler(prepare、parameterize、batch、update、query等方法);
- 参数处理器ParameterHandler(getParameterObject、setParameters方法) ;
- 结果集处理器ResultSetHandler(handleResultSets、handleOutputParameters等方法) ;
3.Mybatis插件原理
上面说到四大核心对象返回的都是代理对象,那么这些代理对象如何产生的呢?如何来完成方法的动态增强呢?
在四大对象创建的时候
1、每个创建出来的对象不是直接返回的, 而是经过interceptorChain.pluginAll(parameterHandler);来对你创建出来的对象进行处理。
2、获取到所有的Interceptor(拦截器)(插件需要实现的接口);调用interceptor.plugin(target) ; 返回target包装后的对象
3、插件机制, 我们可以使用插件为目标对象创建一个代理对象; AOP(面向切面) 我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;
拦截 : 插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来举例说明:
// 1.这个newParameterHandler方法是借助createParameterHandler来生成ParameterHandler的原生对象
// 但是返回的不是创建出来的parameterHandler,而是经过了interceptorChain.pluginAll后返回的。
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object object, BoundSql sql, InterceptorChain interceptorChain){
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
parameterHandler = (ParameterHandler)interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
//pluginAll里面就是对所有的interceptor进行遍历,取出每一个interceptor的target(也就是上面产生的
//parameterHandler对象),经过interceptor.plugin()方法进行处理。那么如何处理的呢?
//就是通过jdk动态代理,来为当前的target对象产生一个代理对象,返回的就是代理对象
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
具体的拦截过程分析
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链 中的拦截器依次的对目标进行拦截或增强。
interceptor.plugin(target)中的target就可以理解为mybatis 中的四大对象。返回的target是被重重代理后的对象。底层用的是动态代理,最终拿到的四大核心对象也是代理对象。
底层是动态代理,最终拿到的是代理对象,那么代理对象调用接口中的任意方法,则都会调用代理对象中的InvocationHandler接口中的invoke方法,那么在invoke方法中,可以在原方法进行调用的前/后都可以进行逻辑的增强。
自定义拦截Executor的query方法
1、先创建一个自定义类,继承拦截器
2、通过intercepts和signature两个注解
3、指定拦截的类型,需要拦截的方法,以及这个被拦截方法需要的参数
4、还需要将自定义的配置类添加到数据库配置文件中
@Intercepts({
@Signature(
type = Executor.class,// 要拦截的类型是四大对象的那个
method = "query",// 具体要拦截的方法
args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
// 这个要拦截方法的参数,也就是入参,因为可能有多个方法,所以通过参数来识别
)
})
public class ExeunplePlugin implements Interceptor { // 自定义拦截器类继承Interceptor接口
//省略逻辑
}
具体的解析如下:
当将自定义的插件配置到数据库配置文件中,这样MyBatis在启动时可以加载插件, 并保存插件实例到相关对象(InterceptorChain, 拦截器链)中。待准备工作做完后, MyBatis处于就绪状态。
我们在执行SQL时, 需要先通过DefaultSqlSessionFactory创建SqlSession。Executor实例会在创建SqlSession的过程中被创建,Executor实例创建完毕后, MyBatis会通过JDK动态代理为实例生成代理类。这样, 插件逻辑即可在Executor相关方法被调用前执行。
调用代理对象中的InvocationHandler接口中的invoke方法,那么在invoke方法中,可以在原方法进行调用的前/后都可以进行逻辑的增强。
以上就是MyBatis插件机制的基本原理,就是通过JDK动态代理,为原生对象来生成一个代理对象。
其实也就是对四大核心对象里面的方法进行增强
Mybatis的自定义插件
4.1插件接口
Mybatis插件接口-Interceptor
Intercept方法, 插件的核心方法
plugin方法, 生成target的代理对象
setProperties方法, 传递插件所需参数
4.2自定义插件
设计实现一个自定义插件
- 1、创建一个MyPlugin类实现Interceptor接口,并重写里面的方法。
- 2、借助@Intercepts、@Signature注解来明确当前自定义插件要拦截那个核心对象里面的那个方法
- 3、将自定义的插件在sqlMapConfig.xml进行注册
StatementHandler是sql的预处理,因为当SQL语句执行的时候,肯定要进行sql语句的预处理。
//注意看这个大花括号,在这个拦截器中,可以订阅多个@Signature对多处进行拦截
@Intercepts({
@Signature(type = StatementHandler.class,// 具体拦截那个核心对象的class
method = "prepare",// 指定要拦截接口内的方法
args = { Connection.class, Integer.class}), // 这是拦截方法的需要的入参
})
public class MyPlugin implements Interceptor {
private final Logger logger = (Logger) LoggerFactory.getLogger(this.getClass());
// 1、拦截方法:只要被拦截的目标对象的目标方法被执行时,都会执行这个intercept方法
// 每次执行prepare方法操作时都会进到intercept这个拦截器的方法内
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 其实这两还可以实现分页、监控等
System.out.println("增强逻辑");
return invocation.proceed();//执行原方法
}
// 2、把当前拦截器生成的代理对象存到拦截链中
// 这是三个方法中第一个执行的方法,因为它把拦截器生成一个代理对象注册到拦截器链中
@Override
public Object plugin(Object target) {
System.out.println( "将要包装的目标对象"+target);
// 通过Plugin.wrap()方法来吧原目标方法生成代理对象,然后存入到拦截器中
// target就是要处理的对象,this就是当前的拦截器MyPlugin
Object wrap = Plugin.wrap(target, this);
return wrap;
}
// 3、获取配置文件的参数
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数"+properties);
}
}
4.3给插件配置文件设置参数
plugins一定要放在typeAliases的下面。
<typeAliases>
<package name="com.lagou.pojo"/>
</typeAliases>
<plugins>
<plugin interceptor="com.lagou.plugin.MyPlugin">
<property name="name" value="tom"/>
</plugin>
</plugins>
Mybatis插件的源码分析
执行插件逻辑
Plugin实现了InvocationHandler接口, 实现接口肯定会实现接口里面的invoke方法,在invoke方法里会拦截所有的方法调用。invoke方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
1、先看自定义框架中getMapper的动态代理方法
2、Mybatis的插件也是实现InvocationHandler接口
3、通过signatureMap获取被拦截的方法列表
4、插件的方法是在注解时候就明确了
5、就是调用intercept方法进行增强
Mybatis可引入第三方插件
page Helper分页插件
1、添加依赖
<!-- 分页助手 -->
<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、可以查看分页助手的源码
3、在Mybatis的核心配置文件sqlMapConfig.xml中配置PageHelper
<plugins>
<!--注意:分页助手的插件配置在通用mapper之前-->
<plugin interceptor="com.github.pagehelper.PageHelper">
<!--指定方言-->
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
4、测试代码
@Test
public void testPageHelper(){
IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
// 从第一页开始,每页两条记录
PageHelper.startPage(1,2);
List<User> users = mapper.selectUser();
for(User user : users){
System.out.println(user);
}
//其他分页数据
PageInfo<User> pageInfo = new PageInfo<User>(users);
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());
}