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的用法以及原理分析就到此爲止了。