原来MyBatis插件/拦截器(Plugin/Interceptyor)的实现原理这么简单

网友投稿 263 2022-09-17

原来MyBatis插件/拦截器(Plugin/Interceptyor)的实现原理这么简单

一、前言

前文(​​从JDK动态代理一步步推导到MyBatis Plugin插件实现原理​​)中,我们从JDK动态代理推导出了MyBatis Plugin的基本实现原理;本文我们就详细分析一下Mybatis Plugin的实现原理;

MyBatis Plugin是MyBatis高可扩展性的体现之一,其用来改变或扩展MyBatis的现有功能;例如:分析项目中存在那些慢SQL(统计SQL执行时间)、分页工具PageHelper也是基于该机制实现的分页;

MyBatis对外提供的扩展点有四个,分别对应MyBatis的四大接口。

二、MyBatis四大接口

我们都知道MyBatis的底层封装了JDBC,并依赖其实现数据库操作;所以MyBatis他的整体工作流程和JDBC类似,大致可分为:预编译SQL语句、SQL参数处理、执行SQL语句、封装结果集;对应这四步,MyBatis提供了四大接口,分别为:Executor、ParameterHandler、StatementHandler、ResultSetHandler。

针对四大接口的几乎所有核心方法都可以被MyBatis Plugin拦截;具体如下:

三、源码分析

我们这里关注的是最原生的MyBatis(不考虑SpringBoot),以MyBatis官网的一个Plugin例子为例(​​mybatis-config.xml -->

拦截类:

// ExamplePlugin.java@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})public class ExamplePlugin implements Interceptor { private Properties properties = new Properties(); public Object intercept(Invocation invocation) throws Throwable { // implement pre processing if need Object returnObject = invocation.proceed(); // implement post processing if need return returnObject; } public void setProperties(Properties properties) { this.properties = properties; }}

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。

1、拦截器何时加载?

我们自定义拦截器之后,是需要在全局配置文件(mybatis-config.xml)中引入的; 因此我们首先从配置文件的解析来看;

phase1 > 在实例化SqlSessionFactory时,我们通过将全局配置文件​​mybatis-config.xml​​​序列化成二进制流,进而构建​​SqlSessionFactory​​;

InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

phase2 > 进入​​SqlSessionFactoryBuilder​​类中,看其build()方法如何初始化配置类?

public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null);}public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 初始化配置类 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 这里会做配置文件的解析、拦截器的添加; return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } }}

phase2.1 > 进入到​​XMLConfigBuilder#parse()​​​方法看配置文件内容的解析, 解析出配置文件中的​​plugin节点​​;

public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; // 真正解析配置文件的地方 parseConfiguration(parser.evalNode("/configuration")); return configuration;}/** * 此处是真正解析配置文件的地方(省略部分代码,我们只看Plugin相关的) */private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode("properties")); // 省略部分代码 ..... // case1:解析出配置文件中的`plugin节点` pluginElement(root.evalNode("plugins")); // 省略部分代码 ..... mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); }}

phase2.2 > 根据配置的​​interceptor属性​​​实例化​​Interceptor对象​​​,然后添加到 ​​Configuration对象​​​中的​​InterceptorChain属性​​中;

private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // case2:根据配置的`interceptor属性`实例化`Interceptor对象` String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); // Interceptor对象添加到 `Configuration对象`中的`InterceptorChain属性`中; configuration.addInterceptor(interceptorInstance); } }}/** * Configuration#addInterceptor()方法 */public void addInterceptor(Interceptor interceptor) { // 进入到InterceptorChain#addInterceptor()方法 interceptorChain.addInterceptor(interceptor);}

我们发现拦截器链​​InterceptorChain​​​和​​从JDK动态代理一步步推导到MyBatis Plugin插件实现原理​​一文中最后的拦截器链一样耶(实际就是模仿的这里的写法);

public class InterceptorChain { private final List interceptors = 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); }}

总而言之,初始化配置文件的时候会把所有的拦截器添加到拦截器链中;下面来看一下什么时候把拦截器插入到需要拦截的接口中。

2、拦截器何时被使用?拦截哪些接口?

按住​​Command​​​快捷键,接着点​​InterceptorChain​​​,我们发现只有Configuration类中会使用到​​InterceptorChain​​;

再进入​​Configuration​​​类,全局搜索​​InterceptorChain​​;

从上述图片可以看出mybatis 在实例化Executor、ParameterHandler、ResultSetHandler、StatementHandler四大接口对象的时候会调用​​interceptorChain.pluginAll()​​ 方法将所有的拦截器插入进去;

public class Configuration { // 集成拦截器链 protected final InterceptorChain interceptorChain = new InterceptorChain(); // 1、创建参数处理器 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // 在此处插入插件/拦截器 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } // 2、创建结果集处理器 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; } // 3、创建SQL语句处理器 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) { return newExecutor(transaction, defaultExecutorType); } // 4、创建执行器 public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; // 防止粗心之人将defaultExecutorType设成null executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; // 判断产生3种执行器BatchExecutor/ReuseExecutor/SimpleExecutor 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); } // 如果要走缓存,则采用装饰者模式使用CachingExecutor(默认就是有缓存),默认都是返回CachingExecutor if (cacheEnabled) { executor = new CachingExecutor(executor); } // 在此处插入插件/拦截器,通过插件改变Executor行为 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }

其实本质上就是遍历拦截器链中所有拦截器的plugin() 方法;mybatis官方推荐的plugin方法是​​Plugin.wrap()​​​方法,Plugin类其实就是我们在动态代理一文中提到的TargetProxy;该类实现了JDK动态代理中的​​InvocationHandler​​接口:

/** * InvocationHandler JDK动态代理 * * @author Clinton Begin */public class Plugin implements InvocationHandler { private Object target; private Interceptor interceptor; /** * 方法签名,Key为接口Class,value为方法 */ private Map, Set> signatureMap; private Plugin(Object target, Interceptor interceptor, Map, Set> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } public static Object wrap(Object target, Interceptor interceptor) { // 从拦截器的注解中获取要拦截的类名和方法信息 Map, Set> signatureMap = getSignatureMap(interceptor); // 取得要改变行为的类(ParameterHandler|ResultSetHandler|StatementHandler|Executor) Class type = target.getClass(); // 获取接口,被Interceptor注解的接口 的实现类才会被代理 // 根据用户在@Intercepts中@Signature注解中配置的类型来和目标对象接口进行匹配 Class[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 使用JDK动态代理 生成代理对象 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 获取需要拦截的方法 Set methods = signatureMap.get(method.getDeclaringClass()); // 是Interceptor实现类上@Intercepts注解中的方法才会被拦截处理 if (methods != null && methods.contains(method)) { // 调用Interceptor.intercept,即:插入我们自己的逻辑 return interceptor.intercept(new Invocation(target, method, args)); } // 不需要拦截,则执行原来的逻辑 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } /** * 获取签名Map,即获取Interceptor实现类上面的注解: * 得知要拦截的是哪个类(Executor,ParameterHandler, ResultSetHandler,StatementHandler)的哪个方法 * @param interceptor * @return */ private static Map, Set> getSignatureMap(Interceptor interceptor) { // 获取Intercepts注解,例子见ExamplePlugin.java Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251,拦截器必须要有Intercepts注解,否则会报错 if (interceptsAnnotation == null) { throw new PluginException( "No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } // 获取@Intercepts注解的value,即:所有的@Signature数组 Signature[] sigs = interceptsAnnotation.value(); // 每个class里会有多个Method需要被拦截 Map, Set> signatureMap = new HashMap, Set>(); // 遍历每个@Signature for (Signature sig : sigs) { Set methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet(); signatureMap.put(sig.type(), methods); } try { // 以@Signature注解中配置的type来获取其method、args Method method = sig.type().getMethod(sig.method(), sig.args()); // // 将获取到的方法保存进methods集合中 methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException( "Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } /** * 获取可拦截的接口 * @param type * @param signatureMap * @return */ private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) { Set> interfaces = new HashSet>(); while (type != null) { // 获取目标类型的所有接口并遍历,注意:这里传递的实际类型(type)是接口实现类,并不是接口本身; // 如果是接口本身,那么type.getInterfaces()获取到的接口只有父接口了 for (Class c : type.getInterfaces()) { // 如果signatureMap中包含该接口,则将该接口添加到interfaces集合中 if (signatureMap.containsKey(c)) { interfaces.add(c); } } // 如果当前类存在父类,对其父类进行相同的查找 type = type.getSuperclass(); } // 最后返回合适的接口 return interfaces.toArray(new Class[interfaces.size()]); }}

Plugin类中对每个拦截器的@Intercepts中@Signature注解中配置的类型 1> 首先和目标对象所实现的接口进行匹配,如果匹配上了,则说明目标对象是当前拦截器所关注的。

2> 其次在代理对象执行的时候(​​invoke()方法​​),对目标方法进行判断:判断其是不是当前拦截器所关注的方法,如果是则执行拦截器的intercept方法,否则直接执行目标方法。

四、总结

MyBatis中的拦截器机制其实就是基于JDK的动态代理实现,所有可能被拦截的处理类都会生成一个代理类;如果有N个拦截器,就会有N个代理,而层层生成动态代理是比较耗性能的。

虽然我们能指定插件拦截的位置,但这个是在执行方法时利用反射动态判断的,初始化的时候只是简单的把拦截器插入到所有可以拦截的地方。

因此非必要尽量不要编写拦截器。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:《SpringBoot系列三》:自定义配置时@Value和@ConfigurationProperties孰优孰劣?
下一篇:营销最前线:中国四大忽悠天王!
相关文章

 发表评论

暂时没有评论,来抢沙发吧~