文章目录
- 前言
- 第九章 MyBatis拦截器的原理及应用
- 9.1 拦截器的实现原理
- 9.1.1 拦截器的注册
- 9.1.2 自定义拦截器
- 9.1.3 拦截器的实现原理
- 9.1.3.1 拦截器支持的类和方法
- 9.1.3.2 Interceptor
- 9.1.3.3 Invocation
- 9.1.3.4 Plugin
- 9.1.3.4.1 getSignatureMap()
- 9.1.3.4.2 getAllInterfaces()
- 9.1.3.4.3 Plugin#invoke()
- 9.1.4 拦截器的执行过程
前言
MyBatis框架支持用户通过自定义插件的方式改变SQL的执行行为,例如在SQL执行时追加SQL分页语法,从而达到简化分页查询的目的。用户自定义的插件也被称为MyBatis拦截器。
本章研究MyBatis拦截器的实现原理及其应用。为方便阅读,本文均采用“拦截器”这一话语,而不使用“插件”这一话语。
第九章 MyBatis拦截器的原理及应用
9.1 拦截器的实现原理
9.1.1 拦截器的注册
在【MyBatis3源码深度解析(十二)MyBatis的核心组件(一)Configuration 4.3.6 插件】中已经知道,在MyBatis的配置文件中,拦截器(插件)使用<plugins>标签进行配置。
在Configuration组件中,组合了一个拦截器链对象(InterceptorChain),用于存放通过<plugins>标签定义的拦截器。 调用Configuration对象的addInterceptor()
方法就可以向InterceptorChain对象中注册一个拦截器:
源码1:org.apache.ibatis.session.Configurationprotected final InterceptorChain interceptorChain = new InterceptorChain();public void addInterceptor(Interceptor interceptor) {interceptorChain.addInterceptor(interceptor);
}
源码2:org.apache.ibatis.plugin.InterceptorChainpublic class InterceptorChain {// 内部维护了一个List集合,用于保存自定义的拦截器private final List<Interceptor> 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<Interceptor> getInterceptors() {return Collections.unmodifiableList(interceptors);}}
由 源码1-2 可知,InterceptorChain对象内部维护了一个List集合,用于保存自定义的拦截器,通过其addInterceptor()
方法即可往集合中添加一个新的拦截器。
值得注意的是pluginAll()
方法,该方法会遍历List集合中的所有拦截器,逐一调用其plugin()
方法创建一个目标对象的动态代理对象。(分析到这里可能还无法理解,没关系,继续往下读)
在【MyBatis3源码深度解析(十四)SqlSession的创建与执行(一)Configuration与SqlSession的创建过程 5.2 Configuration实例创建过程】中已经知道,在XMLConfigBuilder类的parseConfiguration()
方法中,会对MyBatis配置文件中的<plugins>标签进行解析。
源码3:org.apache.ibatis.builder.xml.XMLConfigBuilderprivate void parseConfiguration(XNode root) {try {// ......// 处理<plugins>标签pluginsElement(root.evalNode("plugins"));// ......} // catch ......
}
源码4:org.apache.ibatis.builder.xml.XMLConfigBuilderprivate void pluginsElement(XNode context) throws Exception {if (context != null) {// 遍历<plugins>标签的子标签<plugin>标签对应的XNode对象for (XNode child : context.getChildren()) {// 获取<plugin>标签的interceptor属性String interceptor = child.getStringAttribute("interceptor");// 获取拦截器属性,转换为Properties对象Properties properties = child.getChildrenAsProperties();// 创建拦截器实例Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();// 设置拦截器实例的属性interceptorInstance.setProperties(properties);// 将拦截器实例添加到Configuration对象的拦截器链中configuration.addInterceptor(interceptorInstance);}}
}
由 源码3-4 可知,在XMLConfigBuilder类的pluginsElement()
方法中,会遍历<plugins>标签的子标签<plugin>所对应的XNode对象,逐一获取<plugin>标签的属性并转换为Properties对象,然后通过Java的反射机制实例化拦截器对象,设置完拦截器对象的属性后将其添加到Configuration对象的拦截器链中。
至此,拦截器的注册过程完成。
9.1.2 自定义拦截器
想要自定义一个MyBatis拦截器,只需要创建一个类实现Interceptor接口,重写Interceptor接口的方法,并标注@Intercepts注解配置拦截规则,最后在配置文件中的<plugins>标签中配置即可。
例如:
@Intercepts({@Signature(type= Executor.class,method = "update",args = {MappedStatement.class, Object.class})})
public class ExamplePlugin implements Interceptor {private Properties properties;@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("在这里执行自定义的拦截逻辑...");return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {this.properties = properties;}}
<!--mybatis-config.xml-->
<plugins><plugin interceptor="com.star.mybatis.plugin.ExamplePlugin"><property name="name" value="ExamplePlugin"/></plugin>
</plugins>
关于这个自定义拦截器的解释,可以在下文的分析中慢慢展开。
借助Debug工具,可以查看到Configuration对象创建完毕后,内部的InterceptorChain对象已经封装了自定义的拦截器ExamplePlugin:
9.1.3 拦截器的实现原理
9.1.3.1 拦截器支持的类和方法
在 MyBatis官方文档-插件 部分,由如下内容:
意思是,允许被拦截的类或接口只有4个:Executor、ParameterHandler、ResultSetHandler、StatementHandler,允许被拦截的方法是这4个类或接口中定义的方法。
可以利用IDE的查找功能来理解这个规定。利用IDEA的 Find Usages 功能,查找InterceptorChain类的pluginAll
方法都在哪些地方被调用了,结果如下:
由图可知,只有4处地方调用,恰好是Configuration对象中的newExecutor()
、newParameterHandler()
、newResultSetHandler()
、newStatementHandler()
方法。这就和MyBatis官方文档的说法对应起来了。
源码5:org.apache.ibatis.session.Configurationpublic Executor newExecutor(Transaction transaction, ExecutorType executorType) {// ......// 创建Executor代理对象return (Executor) interceptorChain.pluginAll(executor);
}public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,parameterObject, boundSql);// 创建ParameterHandler代理对象return (ParameterHandler) interceptorChain.pluginAll(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代理对象return (ResultSetHandler) interceptorChain.pluginAll(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代理对象return (StatementHandler) interceptorChain.pluginAll(statementHandler);
}
由 源码5 可知,Configuration对象的这4个工厂方法,都调用了InterceptorChain对象的pluginAll()
方法,创建对应的动态代理对象。有了动态代理对象,就可以在执行目标方法时执行额外的逻辑。
9.1.3.2 Interceptor
在自定义的拦截器ExamplePlugin中,实现了Interceptor接口。该接口的定义如下:
源码6:org.apache.ibatis.plugin.Interceptorpublic interface Interceptor {// 用于定义拦截器逻辑,在目标方法被调用时执行Object intercept(Invocation invocation) throws Throwable;// 用于创建代理对象default Object plugin(Object target) {return Plugin.wrap(target, this);}// 用于设置插件的属性值default void setProperties(Properties properties) {// NOP}}
由 源码6 可知,Interceptor接口中定义了3个方法,它们的作用如代码中的注释所示。需要注意的有以下两点:
(1)intercept()
方法接收一个Invocation对象作为参数,该对象封装了目标对象的方法及参数信息;
(2)plugin()
方法中使用Plugin类的wrap()
方法创建动态代理对象。
9.1.3.3 Invocation
源码7:org.apache.ibatis.plugin.Invocationpublic class Invocation {// 目标对象,即要拦截的类private final Object target;// 目标方法,即要拦截的方法private final Method method;// 目标方法对应的参数private final Object[] args;public Invocation(Object target, Method method, Object[] args) {this.target = target;this.method = method;this.args = args;}// getter ......// 执行目标方法public Object proceed() throws InvocationTargetException, IllegalAccessException {return method.invoke(target, args);}}
由 源码7 可知,Invocation类中封装了目标对象、目标方法以及参数信息,还提供了一个proceed()
方法,用于执行目标方法。
因此,在自定义的拦截器中,执行完拦截逻辑后,一般都要调用invocation.proceed()
执行目标方法的原有逻辑。
9.1.3.4 Plugin
MyBatis提供了一个Plugin工具类,用于创建动态代理对象。
源码8:org.apache.ibatis.plugin.Pluginpublic class Plugin implements InvocationHandler {// 目标对象private final Object target;// 自定义的拦截器实例private final Interceptor interceptor;// @Intercepts注解制定的参数private final Map<Class<?>, Set<Method>> signatureMap;public static Object wrap(Object target, Interceptor interceptor) {// 获取自定义插件中,通过@Intercepts注解制定的方法Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);// 获取目标对象的类型Class<?> type = target.getClass();// Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));}return target;}
}
由 源码8 可知,Plugin类实现了InvocationHandler接口,可见它是采用JDK内置的动态代理方式创建代理对象的。 它的wrap()
方法的核心步骤有三步:
(1)调用getSignatureMap()
方法获取自定义拦截器中通过@Intercepts注解指定的拦截类和方法等信息;
(2)调用getAllInterfaces()
方法获取@Signature注解的type属性所指定的拦截类所实现的接口信息;
(3)调用Proxy类的newProxyInstance()
方法创建一个动态代理对象。
9.1.3.4.1 getSignatureMap()
在解读getSignatureMap()
方法之前,先研究一下@Intercepts注解和@Signature注解:
源码9:org.apache.ibatis.plugin.Intercepts@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {Signature[] value();
}
由 源码9 可知,@Intercepts注解的value属性又指定了一个由@Signature注解组成的数组。
@Signature注解的定义如下:
源码10:@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {// 定义要拦截的类Class<?> type();// 定义要拦截的方法,即要拦截的类中的方法String method();// 定义要拦截的方法所对应的参数Class<?>[] args();
}
由 源码10 可知,@Signature注解包含3个属性,分别是:type属性用于定义要拦截的类或接口,即前面提到的4种;method属性定义要拦截的方法,即要拦截的类或接口中的方法;args属性用于定义要拦截的方法所对应的参数。
下面再来解读一下getSignatureMap()
方法:
源码11:org.apache.ibatis.plugin.Pluginprivate static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {// 获取自定义拦截器的@Intercepts注解Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);// @Intercepts注解为空时抛出异常if (interceptsAnnotation == null) {throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());}// 获取@Intercepts注解的值,即由@Signature注解组成的数组Signature[] sigs = interceptsAnnotation.value();Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();// 遍历@Signature注解组成的数组for (Signature sig : sigs) {Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());try {// 获取@Signature注解的method属性所应对的Method对象,并添加到Set集合中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);}}// 最终返回的Map对象,Key值是@Signature注解的type属性// Value值是一个Set<Method>集合,Method对象封装了@Signature注解的method属性与args属性return signatureMap;
}
由 源码11 可知,getSignatureMap()
方法的解读如代码种的注释所示。该方法的作用是解析@Intercepts注解中的@Signature注解,将@Signature注解配置的要拦截的类、方法及参数信息提取出来,并保存到一个Map集合中。
最终返回的Map对象,Key值是@Signature注解的type属性,Value值是一个Set<Method>集合,Method对象封装了@Signature注解的method属性与args属性.
借助Debug工具,可以查看自定义的拦截器ExamplePlugin被解析时得到的信息:
9.1.3.4.2 getAllInterfaces()
源码12:org.apache.ibatis.plugin.Pluginprivate static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {Set<Class<?>> interfaces = new HashSet<>();// 从@Signature注解的type属性所指定的类开始// 一路向父级查找接口while (type != null) {// 遍历拦截类for (Class<?> c : type.getInterfaces()) {if (signatureMap.containsKey(c)) {interfaces.add(c);}}type = type.getSuperclass();}return interfaces.toArray(new Class<?>[0]);
}
由 源码12 可知,getAllInterfaces()
方法会遍历拦截类实现的所有接口,如果该接口在从@Signature注解的type属性中配置了,则保存起来并返回。
借助Debug工具,可以查看自定义的拦截器ExamplePlugin被解析时执行getAllInterfaces()
方法的结果:
以上信息收集完后,调用Proxy类的newProxyInstance()
方法创建一个动态代理对象。
借助Debug工具,可以查看自定义的拦截器ExamplePlugin被解析时创建的动态代理对象:
9.1.3.4.3 Plugin#invoke()
Plugin类实现了InvocationHandler接口,因此它是采用JDK内置的动态代理方式创建代理对象的。那在执行代理对象的方法时,会调用Plugin类的invoke()
方法。
源码13:org.apache.ibatis.plugin.Plugin@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 根据当前执行的方法所在类,取出signatureMap集合中封装的拦截方法信息Set<Method> methods = signatureMap.get(method.getDeclaringClass());// 如果当前执行的方法,是注解配置好的拦截方法// 则直接调用拦截器的intercept()方法,以执行拦截器逻辑if (methods != null && methods.contains(method)) {return interceptor.intercept(new Invocation(target, method, args));}// 否则直接执行目标方法return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}
}
由 源码13 可知,Plugin类的invoke()
方法会判断当前要执行的目标方法,是否是被@Intercepts注解指定的要被拦截的方法,如果是,则调用自定义拦截器的intercept()
方法,并把目标方法的信息封装为一个Invocation对象作为参数。如果不是,则直接执行目标方法。
9.1.4 拦截器的执行过程
经过以上分析,再来解释自定义拦截器ExamplePlugin就很清晰了:
intercept()
方法用于编写自定义的拦截逻辑;plugin()
方法用于创建动态代理对象;setProperties()
方法用于设置自定义拦截器的属性;- @Signature注解的type属性指定要拦截的类或接口,即Executor;method属性指定要拦截的方法,即
update()
方法;args属性指定拦截方法的参数。
最后,以上文的案例为例,梳理一下拦截器的执行过程:
(1)SqlSession对象创建完毕后,调用Configuration对象的newExecutor()
方法创建Executor对象。
(2)在newExecutor()
方法中,会调用InterceptorChain对象的pluginAll()
方法,该方法会调用自定义拦截器ExamplePlugin类的plugin()
方法。
(3)在plugin()
方法中,调用Plugin类的wrap()
方法创建一个Executor的动态代理对象。
(4)根据JDK动态代理机制,在Executor执行动态代理对象的update()
方法时,会执行Plugin类的invoke()
方法。
(5)Plugin类的invoke()
方法会调用自定义拦截器对象的intercept()
方法执行拦截逻辑。
(6)在自定义拦截器对象的intercept()
方法最后,会执行invocation.proceed()
方法继续执行目标对象的方法,最终返回结果。
编写一个单元测试如下:
@Test
public void testPlugin() throws IOException, NoSuchMethodException {Reader reader = Resources.getResourceAsReader("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession = sqlSessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);User user = new User();user.setId(1);user.setName("testPlugin");Long row = userMapper.updateById(user);System.out.println("row = " + row);
}
控制台打印执行结果:
在这里执行自定义的拦截逻辑...
row = 1
可见,自定义的拦截器确实生效了。
······
本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析