MyBatis plugin的使用与源码解析
Posted 叶长风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis plugin的使用与源码解析相关的知识,希望对你有一定的参考价值。
MyBatis plugin的使用与源码解析
这一节来讲下Mybatis中的plugin的使用,plugin作为对执行期间对Executor、StatementHandler的一种增强等等,我见过用的最多的应该就是Mybatis的分页插件PageHelper,PageHelper因为简单易用被广泛用于各种大小工程中,虽说PageHelper使用起来确实挺舒服,但是在遇到一些查询性能上的问题时,PageHelper带来的弊端倒是挺大的,这个以后有空单独抽一个专题出来说下PageHelper,这一节就不再多说了,就直说Plugin用法与原理。
1. Plugin用法
Plugin用法说起来挺简单的,和写拦截器一样,在这也想好具体写什么Plugin,就写一个拦截query方法的Plugin吧,打印方法具体耗时,命名为ExecuteLogPlugin,如下:
@Intercepts(@Signature(
type = Executor.class,
method = "query",
args = MappedStatement.class,
Object.class,
RowBounds.class,
ResultHandler.class
))
public class ExecuteLogPlugin implements Interceptor
@Override
public Object intercept(Invocation invocation) throws Throwable
long start = System.currentTimeMillis();
Object target = invocation.proceed();
long end = System.currentTimeMillis();
System.out.println("intercept " + invocation.getMethod().getName() + " cost : " + (end - start));
return target;
@Override
public Object plugin(Object target)
return Plugin.wrap(target, this);
@Override
public void setProperties(Properties properties)
这段里面真正起作用的就是invocation.proceed()这段,就是执行代理方法,然后在前后埋点,最后输出对应method耗时,在这图简便,就没有单独引入log的包,就用System输出了,这个Plugin中最重要的就是@Intercepts注解,表明拦截的是什么类型,拦截方法。这个注解的解析在后面会提到。
除了编写Plugin之外,还需要做的事情就是在xml中需要配置Plugin,具体如下:
<plugins>
<plugin interceptor="cn.com.plugin.ExecuteLogPlugin"></plugin>
</plugins>
这一切做完后,在运行main程序时就能得到期望的效果,在这几乎每个query方法都进行了拦截,如果只想对某一个或者几个方法进行拦截的话,invocation中args字段中有对应的class类与方法名,可以在这做些文章。
用法就到此为止了,运行中的截图也没啥好贴出来的,运行时可以自己看下,下面开始对原理性的分析。
2. 原理分析
分析Plugin的加载仍然需要回到configuration的初始化,继续回到Main程序的第一行开始分析,我们的main程序中的SessionFactory的初始化如下:
String resource = "conf.xml";
//使用类加载器加载mybatis的配置文件(它也加载关联的映射文件)
InputStream is = Main.class.getClassLoader().getResourceAsStream(resource);
//构建sqlSession的工厂
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);
还是进入到build方法中,查看configuration的初始化过程.
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);
如同第一篇文章!Mybatis源码解析之配置加载(一)一样,我们回到解析configuration的源码处,此次其他均不讲,只说pluginElement(root.evalNode(“plugins”))这一行,这一行就是plugin的具体加载过程,进入到pluginElement()中。
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);
在这里解析出Plugin,并进行实例化操作,(Interceptor) resolveClass(interceptor).newInstance(),这一步追踪到最后就是调用了Class.forName()的过程,没有太多深究的东西。
在实例化完了后,将当前的plugin实例保存进InterceptorCahin中,等待Executor执行时调用,接下来我们再看session获取到mapper,mapper执行的过程。
UserMapper userMapper = session.getMapper(UserMapper.class);
//执行查询返回一个唯一user对象的sql
User user = userMapper.getUser(1);
从前几篇文章我们知这里的Mapper对象其实就是一个代理对象,真正在起作用的为MapperProxy,然后在执行调用方法时,是调用MapperProxy的invoke方法,然后对于select方法,最终都是调用selectList,selectList经过多层调用后最终还是调用doQuery方法,我们回到doQuery处。
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException
Statement stmt = null;
try
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
finally
closeStatement(stmt);
此处我们看获取StatementHandler的方法,进入到newStatementHandler。
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;
此处调用了interceptorChain.pluginAll(statementHandler)得到statementHandler对象,而pluginAll中做的操作为:
public Object pluginAll(Object target)
for (Interceptor interceptor : interceptors)
target = interceptor.plugin(target);
return target;
就是调用了每个plugin的plugin方法,返回一个包装对象,然后在statementHandler调用的时候发挥代理的作用,这最终解释了Plugin中intercept方法发挥作用的时机,但是仍然没有解释到@Intercepts注解的作用,以及何时进行拦截,这里就要说下Plugin中plugin方法的作用了。plugin方法中做的事情为:
@Override
public Object plugin(Object target)
return Plugin.wrap(target, this);
对target与当前对象进行了一层包装,进入到warp方法中。
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)
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
return target;
进行注解解析的一行代码为:
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
跳转到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();
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;
这个方法做的事情就是解析**@Signature**注解,将对应的type、method保存进Map中返回。
然后在wrap方法中对当前class进行判断,如果是当前class是@Signature中指定的type的子类,则进行代理操作。
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0)
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
对当前class的判断方法为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);
type = type.getSuperclass();
return interfaces.toArray(new Class<?>[interfaces.size()]);
这里判断了当前signatureMap中是否包含当前class,如果是则返回包含当前class的数组,如果不是则返回空数组,而在上一代码块中得知,只有当当前数组长度大于0时,才会返回代理对象,我们在@Signature注解中标注拦截的type为Executor,那么所有的方法基本都会被返回代理对象,但是我们选择拦截方法是query,不能拦截update、delete等操作,所以这里的玄机就在于实例化的对象传入的参数包括signatureMap。
new Plugin(target, interceptor, signatureMap))
代理对象最终执行的方法为invoke方法,我们看当前Plugin的invoke方法。
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))
return interceptor.intercept(new Invocation(target, method, args));
return method.invoke(target, args);
catch (Exception e)
throw ExceptionUtil.unwrapThrowable(e);
这里一切明了,只有signatureMap取出的methods中包含当前方法名才会调用代理对象的intercept方法,否则直接进行method.invoke操作,不会对方法进行拦截。
Plugin的用法以及原理分析就到此为止了。
以上是关于MyBatis plugin的使用与源码解析的主要内容,如果未能解决你的问题,请参考以下文章
MyBatis源码分析六MyBatis Plugins(拦截器)