Plugins
简单理解为拦截器,既然是拦截器说白了一般都是动态代理来实现对目标方法的拦截,在前后做一些操作。
在mybatis将这种东西,称之为plugin,配置在mybatis-config.xml配置文件中,通过 标签配置。在mybatis中,可以被拦截的目标主要是:
1. StatementHandler;
2. ParameterHandler;
3. ResultSetHandler;
4. Executor;
这里实现 ByPage后缀的查询,自动执行分页操作;不需要显性的limit SQL操作;
interceptor
通过实现Interceptor接口,来自定义plugin
public interface Interceptor {
// 拦截逻辑,参数是代理类
Object intercept(Invocation invocation) throws Throwable;
// 加载插件,一般使用Plugin.wrap(target, this);加载当前插件
Object plugin(Object target);
// 初始化属性
void setProperties(Properties properties);
}
可以通过implements Interceptor来自定义plugin,但是仅仅这样是不行的,额外需要通过@Inteceptors和@Signature源注解来指定拦截器需要拦截的目标(类、方法、参数);
@Intercepts(value = {@Signature(
type = StatementHandler.class,
method = "prepare",
args = {
Connection.class,
Integer.class
}
)})
public class ThreadLocalPagePlugin implements Interceptor {
/**
* 这个方法是实际的拦截逻辑,我们的目的是在这里来实现分页,需要达到什么程度的使用。
* 假设从ThreadLocal获取分页信息,来进行分页操作;
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取目标对象,注意StatementHandler中的属性都是protected
// 不能直接访问,因此需要通过其他的方式来获取,就是MetaObject
// 其基本实现是BaseStatementHandler其中最重要的属性是MappedStatment
// 包含了SQL相关信息
// 实际返回的是RoutingStatementHandler
StatementHandler handler = (StatementHandler) invocation.getTarget();
// 获取指定对象的元信息
MetaObject metaObject = MetaObject.forObject(
handler,
SystemMetaObject.DEFAULT_OBJECT_FACTORY,
SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
new DefaultReflectorFactory()
);
// 然后就可以通过MetaObject获取对象的属性
// 获取RoutingStatementHandler->PrepareStatementHandler->BaseStatementHandler中的mappedStatement
// mappedStatement 包含了Sql的信息
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 获取statement id
String statementId = mappedStatement.getId();
// 会拦截每个属性
if (statementId.endsWith("ByPage")){
// ByPage 表示的是分页查询
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 获取当前线程分页信息
Page<?> pager = ThreadLocalUtil.threadLocal.get();
String countSql = "SELECT COUNT(*) " + sql.substring(sql.indexOf("from"));
Connection conn = (Connection) invocation.getArgs()[0];
PreparedStatement ps = conn.prepareStatement(countSql);
// 获取参数处理器来处理参数
ParameterHandler ph = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
ph.setParameters(ps);
// 执行查询
ResultSet rs = ps.executeQuery();
if(rs.next()){
pager.setTotalCount(rs.getInt(1));
}
String pageSql = sql + " LIMIT " + pager.getStartPos() + ", " + pager.getPageSize();
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
return invocation.proceed();
}
// 指定需要拦截的对象
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 初始化属性
@Override
public void setProperties(Properties properties) {
}
}
增加配置:
<plugins>
<plugin interceptor="com.test.plugin.ThreadLocalPagePlugin" >
</plugin>
</plugins>
测试代码:
Page<User> pager = new Page<User>(1, 2);
ThreadLocalUtil.threadLocal.set(pager);
List<User> list = getSqlSession().getMapper(UserMapper.class).selectUserByPage();
Page page = ThreadLocalUtil.threadLocal.get();
System.out.println(list.size()+page.toString());
打印出分页相关信息。这里没有将查询结果设置到 page 对象中。
----------- intercept query end… ---------testPro
2Pager{startPos=0, curPage=1, pageSize=2, datas=null, totalPage=3, totalCount=5}
我们再增加一个处理查询结果的Plugin代码如下,该代码拦截结果处理的方法,并将处理后的结果设置到我们的 Page对象中
@Intercepts(value = {@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {
Statement.class
}
)})
public class ThreadLocalPagePluginAfter implements Interceptor {
/**
* 这个方法是实际的拦截逻辑,我们的目的是在这里来实现分页,需要达到什么程度的使用。
* 假设从ThreadLocal获取分页信息,来进行分页操作;
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object rr = invocation.proceed();
Page page =ThreadLocalUtil.threadLocal.get();
page.setDatas((List) rr);
return rr;
}
// 指定需要拦截的对象
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 初始化属性
@Override
public void setProperties(Properties properties) {
}
}
配置plugin
执行测试代码:
public static void main(String[] args) throws Exception {
SqlSession sesion = getSqlSession();
UserMapper mapper= sesion.getMapper(UserMapper.class);
Page<User> page = new Page<>(1,2);
ThreadLocalUtil.threadLocal.set(page);
mapper.getUsersByPage();
System.out.println(page);
}
可以看到 我们拿到了分页的完整信息
11:22:20.796 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - ==> Preparing: SELECT COUNT(*) from user
11:22:20.822 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - ==> Parameters:
11:22:20.857 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - ==> Preparing: select * from user LIMIT 0, 2
11:22:20.858 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - ==> Parameters:
11:22:20.863 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - <== Total: 2
Pager{startPos=0, curPage=1, pageSize=2, datas=[com.test.mybatis.beans.User@5b8dfcc1, com.test.mybatis.beans.User@2f9f7dcf], totalPage=7502, totalCount=15003}
Plugin实现原理
1.我们我们在配置文件中声明定义了plugin 如:
<plugin interceptor="com.test.plugin.ThreadLocalPagePluginAfter" > </plugin>
2.当Mybaitis 启动加载配置信息时将配置的plugin加载到 Configuration 属性 中
3、随后在创建 一下四个接口的实现类之后会调用 InterceptorChain 的 pluginAll方法,这里以 Exexutor 为例如截图:
1. StatementHandler;
2. ParameterHandler;
3. ResultSetHandler;
4. Executor;
4、pluginAll方法 调用所有plugin 对象的 plugin方法 如下图:
以我们实现的plugin 为例 可以看到 plugin 方法只有 一行 调用 Plugin.wrap参数target是我们们的 Executor实例或代理对象,
return Plugin.wrap(target, this);
wrap 方法中判断我们定义的plugin 是否对目标对象( Executor实例或代理对象)的接口做了拦截,如果做了拦截 就用当前我们定义的Interceptor 构造一个plugin 对 目标对象做代理,否则不做代理。
经过上述过程如果 我们定义的 多个plugin 对某个接口做了拦截,就会依次生成对象的代理。我们这里的两个plugin 分别 对 Executor接口和resultSetHandler接口做了代理,所以创建 executor对象后就会有一个plugin 对 这个对象做代理。resultSetHandler同理。所以如果定义了 plugin 那么我们得到的 对象其实是代理对象。