目录
最近项目中需要开发对指定规则的一组sql进行拦截操作,如果添加在业务层中会增加耦合性,后期的扩展和维护也会很麻烦,因此想到之前用过的mybatis 拦截器,这里学习记录一番。
MyBatis拦截器介绍官方介绍
从官网我们介绍我们可知:
MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。
MyBatis允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
可以拦截Executor接口的部分方法,比如update,query,commit,rollback等方法,还有其他接口的一些方法等。
总体概括为:
拦截执行器的方法
拦截参数的处理
拦截结果集的处理
拦截Sql语法构建的处理
mybatis拦截器的使用步骤是比较简单的
step1 在mybatis-config.xml中添加如下配置step2 实现org.apache.ibatis.plugin.Interceptor 接口 并重写三个方法
// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
}
}
三个方法分别为:
intercept(Invocation invocation)
此方法是拦截目标对象的目标方法执行
plugin(Object target)
包装目标对象,为目标对象创建一个代理对象
setProperties(Properties properties)
将插件注册时的property属性设置进来
MyBatis的插件机制,实际就是Java动态代理实现的责任链模式实现。
我们先来看一张图了解下整个的流程
图中红色圈住的地方是可以被代理拦截的点
从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个
红色部分为Mapper初始化,语法解析阶段的产出物.
紫色部分为sql语句执行时的关键类.
SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,
ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
MappedStatement MappedStatement维护了一条
SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息
- SqlsessionFactoryBuilder
public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }XMLConfigBuilder 读取xml 配置文件中的配置
- XMLConfigBuilder
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } }
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}这个interceptorChain是Configuration的内部属性,类型为InterceptorChain,也就是一个拦截器链,我们来看下它的定义:
- interceptorChain
public class InterceptorChain { private final Listinterceptors = new ArrayList (); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List getInterceptors() { return Collections.unmodifiableList(interceptors); } } - Configuration
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor, autoCommit); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }以上4个方法都是Configuration的方法。这些方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler(其中ParameterHandler和ResultSetHandler的创建是在创建StatementHandler[3个可用的实现类CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler]的时候,其构造函数调用的[这3个实现类的构造函数其实都调用了父类baseStatementHandler的构造函数])。
这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,InterceptorChain的pluginAll刚才已经介绍过了,就是遍历所有的拦截器,然后调用各个拦截器的plugin方法。注意:拦截器的plugin方法的返回值会直接被赋值给原先的对象
由于可以拦截StatementHandler,这个接口主要处理sql语法的构建,因此比如分页的功能,可以用拦截器实现,只需要在拦截器的plugin方法中处理StatementHandler接口实现类中的sql即可,可使用反射实现。
最后我们再看下plugin 这个核心方法吧
- plugin
1 package org.apache.ibatis.plugin; 2 3 import java.lang.reflect.InvocationHandler; 4 import java.lang.reflect.Method; 5 import java.lang.reflect.Proxy; 6 import java.util.HashMap; 7 import java.util.HashSet; 8 import java.util.Map; 9 import java.util.Set; 10 11 import org.apache.ibatis.reflection.ExceptionUtil; 12 13 //这个类是Mybatis拦截器的核心,大家可以看到该类继承了InvocationHandler 14 //又是JDK动态代理机制 15 public class Plugin implements InvocationHandler { 16 17 //目标对象 18 private Object target; 19 //拦截器 20 private Interceptor interceptor; 21 //记录需要被拦截的类与方法 22 private Map, Set > signatureMap; 23 24 private Plugin(Object target, Interceptor interceptor, Map , Set > signatureMap) { 25 this.target = target; 26 this.interceptor = interceptor; 27 this.signatureMap = signatureMap; 28 } 29 30 //一个静态方法,对一个目标对象进行包装,生成代理类。 31 public static Object wrap(Object target, Interceptor interceptor) { 32 //首先根据interceptor上面定义的注解 获取需要拦截的信息 33 Map , Set > signatureMap = getSignatureMap(interceptor); 34 //目标对象的Class 35 Class> type = target.getClass(); 36 //返回需要拦截的接口信息 37 Class>[] interfaces = getAllInterfaces(type, signatureMap); 38 //如果长度为>0 则返回代理类 否则不做处理 39 if (interfaces.length > 0) { 40 return Proxy.newProxyInstance( 41 type.getClassLoader(), 42 interfaces, 43 new Plugin(target, interceptor, signatureMap)); 44 } 45 return target; 46 } 47 48 //代理对象每次调用的方法 49 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 50 try { 51 //通过method参数定义的类 去signatureMap当中查询需要拦截的方法集合 52 Set methods = signatureMap.get(method.getDeclaringClass()); 53 //判断是否需要拦截 54 if (methods != null && methods.contains(method)) { 55 return interceptor.intercept(new Invocation(target, method, args)); 56 } 57 //不拦截 直接通过目标对象调用方法 58 return method.invoke(target, args); 59 } catch (Exception e) { 60 throw ExceptionUtil.unwrapThrowable(e); 61 } 62 } 63 64 //根据拦截器接口(Interceptor)实现类上面的注解获取相关信息 65 private static Map , Set > getSignatureMap(Interceptor interceptor) { 66 //获取注解信息 67 Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); 68 //为空则抛出异常 69 if (interceptsAnnotation == null) { // issue #251 70 throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); 71 } 72 //获得Signature注解信息 73 Signature[] sigs = interceptsAnnotation.value(); 74 Map , Set > signatureMap = new HashMap , Set >(); 75 //循环注解信息 76 for (Signature sig : sigs) { 77 //根据Signature注解定义的type信息去signatureMap当中查询需要拦截方法的集合 78 Set methods = signatureMap.get(sig.type()); 79 //第一次肯定为null 就创建一个并放入signatureMap 80 if (methods == null) { 81 methods = new HashSet (); 82 signatureMap.put(sig.type(), methods); 83 } 84 try { 85 //找到sig.type当中定义的方法 并加入到集合 86 Method method = sig.type().getMethod(sig.method(), sig.args()); 87 methods.add(method); 88 } catch (NoSuchMethodException e) { 89 throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); 90 } 91 } 92 return signatureMap; 93 } 94 95 //根据对象类型与signatureMap获取接口信息 96 private static Class>[] getAllInterfaces(Class> type, Map , Set > signatureMap) { 97 Set > interfaces = new HashSet >(); 98 //循环type类型的接口信息 如果该类型存在与signatureMap当中则加入到set当中去 99 while (type != null) { 100 for (Class> c : type.getInterfaces()) { 101 if (signatureMap.containsKey(c)) { 102 interfaces.add(c); 103 } 104 } 105 type = type.getSuperclass(); 106 } 107 //转换为数组返回 108 return interfaces.toArray(new Class>[interfaces.size()]); 109 } 110 111 } 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = null;
if (invocation.getArgs().length > 1) {
parameter = invocation.getArgs()[1];
}
String sqlId = mappedStatement.getId();
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
Configuration configuration = mappedStatement.getConfiguration();
StopWatch clock = new StopWatch();
clock.start();
Object result = null;
try {
result = invocation.proceed();
} catch (RuntimeException re) {
log.error("invoke mybatis intercepts RuntimeException(can rollback),process target method. class and method:{},parameter:{}", new Object[]{sqlId, parameter}, re);
throw re;
} catch (Exception e) {
log.error("invoke mybatis intercepts Exception(not rollback),process target method. class and method:{},parameter:{}", new Object[]{sqlId, parameter}, e);
throw e;
} catch (Error error) {
log.error("invoke mybatis intercepts Error(can rollback),process target method. class and method:{},parameter:{}", new Object[]{sqlId, parameter}, error);
throw error;
} finally {
clock.stop();
}
//TODO someThing
return result;
}1.mybatis 拦截器并不会交由spring 容器管理 而是由其自身管理 因此需要在拦截时注入spring 容器中的bean 需要添加如下而外配置:



