目录
最近项目中需要开发对指定规则的一组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中添加如下配置
<plugins> <plugin interceptor="org.mybatis.example.ExamplePlugin"> <property name="someProperty" value="100"/> </plugin> </plugins>
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拦截器源码解析
MyBatis的插件机制,实际就是Java动态代理实现的责任链模式实现。
我们先来看一张图了解下整个的流程
一个QUERY的执行流程:
图中红色圈住的地方是可以被代理拦截的点
从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维护了一条<select|update|delete|insert>节点的封装, 包含sqlSource , resultMap
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 List<Interceptor> interceptors = new ArrayList<Interceptor>(); 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<Interceptor> 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<Class<?>, Set<Method>> signatureMap; 23 24 private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> 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<Class<?>, Set<Method>> 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<Method> 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<Class<?>, Set<Method>> 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<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); 75 //循环注解信息 76 for (Signature sig : sigs) { 77 //根据Signature注解定义的type信息去signatureMap当中查询需要拦截方法的集合 78 Set<Method> methods = signatureMap.get(sig.type()); 79 //第一次肯定为null 就创建一个并放入signatureMap 80 if (methods == null) { 81 methods = new HashSet<Method>(); 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<Class<?>, Set<Method>> signatureMap) { 97 Set<Class<?>> interfaces = new HashSet<Class<?>>(); 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 { /** step_1 解析拦截器,获取目标对象,方法,参数 */ 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(); /** step_2 执行目标方法,记录不同异常,用以判断Spring声明式回滚策略(https://blog.csdn.net/abc19900828/article/details/39497631) */ 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 需要添加如下而外配置:
<bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="dataSource" ref="DataSource"/> <property name="plugins"> <ref bean="mybatisInterceptor"/> </property> </bean> <!-- MyBatis sql拦截器--> <bean id="mybatisInterceptor" class="xx.xx.xx.MybatisInterceptor"> <property name="properties"> <map> <entry key="insert" value="insert"/> <entry key="update" value="update"/> <entry key="delete" value="delete"/> </map> </property> </bean>