mybatis拦截器学习
Posted tanyunlong_nice
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mybatis拦截器学习相关的知识,希望对你有一定的参考价值。
目录
最近项目中需要开发对指定规则的一组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>
以上是关于mybatis拦截器学习的主要内容,如果未能解决你的问题,请参考以下文章