MyBatis插件原理分析,看完感觉自己better了
本文主要内容:
大多数框架都支持插件,用户可通过编写插件来自行扩展功能,Mybatis也不例外。
在Mybatis中最出名的就是PageHelper 分页插件,下面我们先来使用一下这个分页插件。
如何集成分页插件
Spring-Boot+Mybatis+PageHelper 。
引入pom依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
配置分页插件配置项
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
service接口代码中
PageInfo selectUsersByName(int pageIndex, int pageSize);
service实现类代码中
@Override
public PageInfo selectUsersByName(int pageIndex, int pageSize) {
PageHelper.startPage(pageIndex, pageSize);
List<User> users = userMapper.selectUsersByName(null);
return new PageInfo(users);
}
Mapper代码代码
<select id="selectUsersByName" resultMap="User">
select * from m_user
<where>
<if test="userName != null and userName != ''">
`name` = #{userName}
</if>
</where>
</select>List<User> selectUsersByName(@Param("userName") String userName);
controller中代码
@GetMapping("/user/name")
public PageInfo selectUsersByName(int pageIndex, int pageSize) {
return userService.selectUsersByName(pageIndex, pageSize);
}
然后我们访问
http://localhost:9002/user/name?pageIndex=1&pageSize=10
输出结果:
输出重要项说明:
pageNum:当前页码。 pageSize:每页数。 list:就是我们返回的业务数据。 total:总数据。 hasNextPage:是否存在下一页。
我们在看看输出SQL:
发现其实执行了两条SQL
:count和limit。
猜测分页插件实现
1.这个分页插件无非就是在我们的查询条件上拼接了个limit和做了一个count查询。
2.我们这里使用的是Mysql作为数据库,如果是Oracle的话那就不是limit了,所以这里有多重数据库对应的方案。
3.在没有此插件的前面拦截并做了sql和相关处理。
根据官网快速入门插件
下面是来自官网的一段话:
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler (getParameterObject, setParameters) ResultSetHandler (handleResultSets, handleOutputParameters) StatementHandler (prepare, parameterize, batch, update, query) 这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。这些都是更底层的类和方法,所以使用插件的时候要特别当心。
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
那我们就尝试着按照官方来写一个插件。
自定义插件
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class TianPlugin implements Interceptor {
private Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("老田写的一个Mybatis插件--start");
Object returnObject = invocation.proceed();
System.out.println("老田写的一个Mybatis插件---end");
return returnObject;
}
}
然后把插件类注入到容器中。
这里的自定义完全是官网给出的案例。从自定义的插件类中看到有个update,我们猜测肯定是需要执行update才会被拦截到。
访问前面的代码:http://localhost:9002/updateUser
成功了。
这是大家肯定会联想到我们刚刚开始学动态代理的时候,不就是在要调用的方法的前面和后面做点小东东吗?
Mybatis
的插件确实就是这样的。
我们来分析一下官方的那段话和我们自定义的插件。
分析
首先,我们自定义的插件必须是针对下面这四个类以及方法。
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler (getParameterObject, setParameters) ResultSetHandler (handleResultSets, handleOutputParameters) StatementHandler (prepare, parameterize, batch, update, query)
其次,我们必须实现Mybatis的Interceptor。
Interceptor中三个方法的作用:
intercept():执行拦截内容的地方,比如:在调用某类方法前后做一些自己的处理,简单就是打印日志。 plugin():决定是否触发intercept()方法。 setProperties():给自定义的拦截器传递我们配置的属性参数(这个可以暂时不管他,后面我们写一个相对完整点的插件,你就明白是干啥的了)。
plugin方法
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
默认实现方法,里面调用了Plugin.wrap()方法。
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 创建JDK动态代理对象
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@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);
}
}
//...省略其他不相关代码
}
这不就是一个JDK动态代理吗?
Map<Class<?>, Set> signatureMap:缓存需拦截对象的反射结果,避免多次反射,即target的反射结果。
所以,我们不要动不动就说反射性能很差,那是因为你没有像Mybatis一样去缓存一个对象的反射结果。
判断是否是需要拦截的方法,这句注释很重要,一旦忽略了,都不知道Mybatis是怎么判断是否执行拦截内容的,要记住。
Plugin.wrap(target, this)是干什么的?
使用JDK的动态代理,给target对象创建一个delegate代理对象,以此来实现方法拦截和增强功能,它会回调intercept()方法。
为什么要写注解?注解都是什么含义?
在我们自定义的插件上有一堆注解,别害怕。
Mybatis规定插件必须编写Annotation注解,是必须,而不是可选。
@Intercepts({@Signature( type= Executor.class, method = "update",
args = {MappedStatement.class,Object.class})}
)
public class TianPlugin implements Interceptor {
@Intercepts注解:装载一个@Signature列表,一个@Signature其实就是一个需要拦截的方法封装。那么,一个拦截器要拦截多个方法,自然就是一个@Signature列表。
type= Executor.class, method = "update",args = {MappedStatement.class,Object.class}
解释:要拦截Executor接口内的query()方法,参数类型为args列表。
那如果想拦截多个方法呢?
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
这就简单了吧,我们在@Intercepts注解中可以存放多个@Signature注解。
比如说前面分页插件中就是拦截多个方法的。
为什么拦截两个都是query方法呢?因为在Executor中有两个query方法。
总结下:
Mybatis规定必须使用@Intercepts注解。
@Intercepts注解内可以添加多个类多个方法,注意方法名和参数类型个数一定要对应起来。
推荐阅读