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的继承关系:

mysql 拦截器 拦截顺序 mybatis拦截器原理_sql

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