mybatis 拦截器可以让程序员在不修改源码的情况下,执行自己的逻辑。
实现拦截器要继承Interceptor接口,并且使用
@Intercepts({@Signature(type=null,method="",args={null})})
注解,其中type是要拦截的类,method是拦截的方法,因为存在重载机制,所以要加上参数列表,args。
实现Interceptor接口必须实现三个方法,intercept方法,plugin方法,setProperties方法。
public class TestInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
// TODO Auto-generated method stub
return null;
}
public Object plugin(Object target) {
// TODO Auto-generated method stub
return null;
}
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
}
其中最重要的是前两个方法,因为mybatis会首先调用plugin方法,如果需要实现拦截则在调用intercept方法,实现相关的逻辑。
mybatis提供了一个实现类Plugin。
我们可以在plugin方法中直接使用:
public Object plugin(Object target) {
// TODO Auto-generated method stub
return Plugin.wrap(target, this);
}
Plugin.wrap的源码如下:
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) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
可以看到,如果目标对象有实现了我们想要拦截的结构的话,就返回相应的代理类,然后由代理类去执行相应的interceptor方法,否则就直接返回。
接下来我们就可以实现interceptor方法了。
我们的目的是实现分页,也就是说我们要在某一些sql(比如select标签中id 为 xxxByPage)语句中拼接上limit 语句,因此我们要拦截mybaits中所有执行sql语句的方法。
mybaits中,StatementHandler是专门处理sql的,因此我们要拦截的是它,以及它当中的prepare方法。
public interface StatementHandler {
Statement prepare(Connection connection)
throws SQLException;//其余代码略
}
所以注解这么写
@Intercepts(value = { @Signature(args = { Connection.class }, method = "prepare", type = StatementHandler.class) })
现在,我们就可以拦截了:
@Intercepts(value = { @Signature(args = { Connection.class }, method = "prepare", type = StatementHandler.class) })
public class TestInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
// TODO Auto-generated method stub
System.out.println("方法被拦截了");
return invocation.proceed();//交回主权
}
public Object plugin(Object target) {
// TODO Auto-generated method stub
return null;
}
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
}
接下来我们要拦截的是特定的sql语句,比如xxxByPage,因此我们要在intercept中加相应的逻辑。
下图是StatementHandler的继承关系:
mybatis的执行顺序是先调用RoutingStatementHandler,其中有StatementHandler delegate,delegate指向BaseStatementHandler,而BaseStatementHandler有参数mappedStatement,因此我们可以拿到mappedStatement去获取相应的id,由于mappedStatement是protected,因此我们可以通过反射获取到它。
代码如下:
RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
MetaObject metaObject=MetaObject.forObject(statementHandler,SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
MappedStatement mappedStatement=(MappedStatement) metaObject.getValue("delegate.mappedStatement");
接下来就可以直接获取id,并且做判断:
String id=mappedStatement.getId();
if(id.matches(".+ByPage$")){
System.out.println("方法已经拦截");
}
这样子我们就可以拦截id为xxxByPage的语句了。
剩下的就是拼接sql了:
BoundSql boundSql=statementHandler.getBoundSql();
String sql=boundSql.getSql();
String newSql=sql+" limit " + page.getLimitParameter()+"," +page.getPageNumber();
metaObject.setValue("delegate.boundSql.sql",newSql);
page是一个bean对象,这里不再多说。
完整的intercept方法如下:
public Object intercept(Invocation invocation) throws Throwable {
// TODO Auto-generated method stub
RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
MetaObject metaObject=MetaObject.forObject(statementHandler,SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
MappedStatement mappedStatement=(MappedStatement) metaObject.getValue("delegate.mappedStatement");
String id=mappedStatement.getId();
if(id.matches(".+ByPage$")){
System.out.println("方法已经拦截");
BoundSql boundSql=statementHandler.getBoundSql();
String sql=boundSql.getSql();
String newSql=sql+" limit " + page.getLimitParameter()+"," +page.getPageNumber();
System.out.println(newSql);
metaObject.setValue("delegate.boundSql.sql",newSql);
}
return invocation.proceed();
}
sql语句如下:
<select id="selectByPage" resultType="Ad" parameterType="Page">
select * from ad
</select>
配置如下:
<plugins>
<plugin interceptor="org.imooc.interceptor.PageInterceptor">
</plugin>
</plugins>
这样子就可以实现分页了,当然我们还可以加入计算查询总条数的代码,主要思路还是通过jdbc的connection以及statement直接去执行相应的sql语句,并且connection对象已经可以直接从invocation拿到了,注意我们拦截的时候已经把connection对象拦截了。