Mybatis 分页拦截器的实现与原理

707次阅读  |  发布于4年以前

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对象拦截了。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8