浅析Mybatis拦截器
一、背景
最近针对项目中出现的慢sql,我们使用自定义Mybatis拦截器,结合配置中心动态配置慢sql阈值,来监控慢sql并报警,提前发现风险点。借着这个契机,浅析下Mybatis拦截器原理,个人理解,不足之处请指正。
二、Mybatis拦截器
Mybatis使用plugin来拦截方法调用,所以MyBatis plugin也称为:Mybatis拦截器。Mybatis采用责任链模式,通过代理组织多个plugin,对Executor、ParameterHandler、StatementHandler、ResultSetHandler中的方法进行拦截。
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) - ParameterHandler (getParameterObject, setParameters) - ResultSetHandler (handleResultSets, handleOutputParameters) - StatementHandler (prepare, parameterize, batch, update, query)
本文使用mybatis版本为3.4.5。
2.1 使用Mybatis拦截器
2.1.1 自定义Plugin
Signature定义具体要拦截的方法信息,type为拦截的对象,method为拦截对象的方法名,考虑到重载的方法,args为拦截对象的方法参数。
@Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), @Signature( type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) public class ExamplePlugin implements Interceptor { /** * 具体拦截逻辑 * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { // implement pre-processing if needed Object result = invocation.proceed(); // implement post-processing if needed return result; } /** * 生成代理对象 * @param target * @return */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 注册插件时xml配置的property转化,设置一些自定义属性 * @param properties */ @Override public void setProperties(Properties properties) { } }
2.1.2 注册Plugin
在mybatis-config.xml中注册插件。
<plugins> <plugin interceptor="xx.ExamplePlugin"> <property name="xxxProperty" value="xxx"/> </plugin> </plugins>
2.1.3 绑定SqlSessionFactory
<bean id="xxxSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="xxxDataSource"/> <property name="configLocation" value="classpath:xxx/mybatis-config.xml"/> </bean>
2.2 源码分析
在Spring容器对SqlSessionFactory初始化时,会解析mybatis-config.xml,注册Interceptor。
/** * org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration * 解析mybatis-config.xml */ private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
/** * org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement * 解析mybatis-config.xml中的plugins元素 */ 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); } } }
/** * org.apache.ibatis.session.Configuration#addInterceptor * 将Interceptor添加到interceptorChain中 */ public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
2.2.1 生成代理流程
这里以ExamplePlugin为例,对Executor阶段的方法进行代理,跟踪一下流程。
第①步
/** * org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource */ private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); //创建Executor对象 final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
第②步
/** * org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType) */ public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor 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); } //处理注册的plugin executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
第③步
/** * org.apache.ibatis.plugin.InterceptorChain#pluginAll * 如果同一个拦截的方法存在多个拦截器,会按照声明的顺序,循环进行代理。 */ public Object pluginAll(Object target) { //循环处理每个注册的plugin,构成一条代理链。 for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
前面说了会对Executor、ParameterHandler、StatementHandler、ResultSetHandler中的方法进行拦截,从这一步的引用可以论证,这4个阶段都会通过plugin生成代理对象。
第④步
/** * ExamplePlugin#plugin * 调用自定义拦截器的plugin方法 * @param target * @return */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); }
第⑤⑥⑦步
/** * org.apache.ibatis.plugin.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) { //生成代理对象,Plugin对象为该代理对象的InvocationHandler return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } /** * org.apache.ibatis.plugin.Plugin#getSignatureMap */ private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } //获取配置的注解信息 Signature[] sigs = interceptsAnnotation.value(); //key是拦截的类,value是拦截的方法集合 Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { //反射获取对应的方法 Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } /** * org.apache.ibatis.plugin.Plugin#getAllInterfaces */ private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { //遍历原始对象类的接口信息 for (Class<?> c : type.getInterfaces()) { //判断拦截配置是否存在的匹配的类型 if (signatureMap.containsKey(c)) { interfaces.add(c); } } //以父类进行匹配,一直匹配到java.lang.Object的父类,为null结束循环 type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); }
2.2.2 拦截流程
这里以一个单条查询sql跟踪一下拦截流程。
第①步
/** * org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds) */ @Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); //此时的executor是代理后的executor return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
第②步
/** * org.apache.ibatis.plugin.Plugin#invoke */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //获取生成代理时构建好的拦截方法信息 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); //判断是否需要拦截 if (methods != null && methods.contains(method)) { //调用自定义plugin实现的intercept方法 return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
第③步
/** * ExamplePlugin#intercept * 具体拦截逻辑 * 如果同一个拦截的方法存在多个拦截器,由于生成层层代理时是顺序生成,拦截时正好相反。 * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { // implement pre-processing if needed //调用代理对象的目标类 Object result = invocation.proceed(); // implement post-processing if needed return result; }
2.3 应用场景
1.慢sql监控(拦截Executor对象的query、update方法)
2.分库分表场景主键ID替换(拦截StatementHandler对象的prepare方法)
3.分页查询(拦截Executor对象的query方法)
2.4 优化
在生成代理流程时序图中的第⑤⑥步会进行反射判断是否需要生成代理,这一步可以前置到第④步进行判断,减少反射。
/** * 生成代理对象 * @param target * @return */ @Override public Object plugin(Object target) { //提前判断是否要生成代理对象 if (target != null && target instanceof Executor) { return Plugin.wrap(target, this); } return target; }
三、总结思考
通过自定义plugin,自己也对mybatis拦截器的实现机制有了清晰的认识,希望这篇文章也能帮助到读者。