mybatis原始碼(十一) mybatis外掛原理及其應用
mybatis原始碼(十一) mybatis外掛原理及其應用
mybatis外掛:MyBatis提供了擴充套件機制,能夠在執行Mapper時改變SQL的執行行為。這種擴充套件機制是通過攔截器來實現的,使用者自定義的攔截器也被稱為MyBatis 外掛。MyBatis框架支援對Executor、ParameterHandler、ResultSetHandler、 StatementHandler四種元件的方法進行攔截。
Executor 的建立是在SqlSession建立的時候建立的
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, booleanDefaultSqlSessionFactoryautoCommit) { Transaction tx = null; try { // 獲取Mybatis主配置檔案配置的環境資訊 final Environment environment = configuration.getEnvironment(); // 建立事務管理器工廠 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 建立事務管理器 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 根據Mybatis主配置檔案中指定的Executor型別建立對應的Executor例項 final Executor executor = configuration.newExecutor(tx, execType); // 建立DefaultSqlSession例項 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(); } }
StatementHandler 的建立是在執行sql語句前建立的,例如:
public class SimpleExecutor extends BaseExecutor { public SimpleExecutor(Configuration configuration, Transaction transaction) { super(configuration, transaction); } @Override public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.update(stmt); } finally { closeStatement(stmt); } }SimpleExecutor
ParameterHandler、ResultSetHandler是在SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler的構造方法中,呼叫父類的構造方法實現的
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { this.configuration = mappedStatement.getConfiguration(); this.executor = executor; this.mappedStatement = mappedStatement; this.rowBounds = rowBounds; this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); this.objectFactory = configuration.getObjectFactory(); if (boundSql == null) { // issue #435, get the key before calculating the statement generateKeys(parameterObject); boundSql = mappedStatement.getBoundSql(parameterObject); } this.boundSql = boundSql; this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); }View Code
總之,都是在執行sql前,就建立的物件
1.為什麼只能攔截這四種外掛呢?
1.mybatis可以根據使用者配置的引數建立不同的例項,根據Configuration物件。(這4個元件)
2.mybatis通過工廠方式建立4種元件,在工廠方法中,可以執行攔截邏輯
Configuration類部分程式碼如下:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // 執行攔截器鏈的攔截邏輯 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return 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 = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return 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 = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } public Executor newExecutor(Transaction transaction) { return newExecutor(transaction, defaultExecutorType); } public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor 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); } // 如果cacheEnabled屬性為ture,這使用CachingExecutor對上面建立的Executor進行裝飾 if (cacheEnabled) { executor = new CachingExecutor(executor); } // 執行攔截器鏈的攔截邏輯 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
可以看到這四個方法裡面,都有一個interceptorChain.pluginAll 的呼叫過程
pluginAll 返回ParameterHandler、ResultSetHandler、StatementHandler、Executor的代理物件
可以看下InterceptorChain 的原始碼
public class InterceptorChain { // 通過List物件維護所有攔截器例項 private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); // 呼叫所有攔截器物件的plugin()方法執行攔截邏輯 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); } }
由上程式碼可以看到,mybatis的攔截器執行鏈中維護了一個所有攔截器的List集合。攔截器鏈執行的方法呼叫是interceptor.plugin(targer),
看下Interceptor介面,裡面只有三個方法
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
Interceptor介面中定義了3個方法:
intercept()方法用於定義攔截邏輯,該方法會在目標方法呼叫時執行。
plugin()方法用於建立Executor、ParameterHandler、 ResultSetHandler 或Statementh Handler的代理物件,該方法的引數即為Executor、ParameterHandler、ResultSetHandler或StatementHandler元件的例項。
setProperties()方法用於設定外掛的屬性值。
需要注意的是,intercept()接 收一一個Invocation物件作為引數,Invocation物件中封裝了目標物件的方法及引數資訊。
Invocation類的原始碼如下:
Invocation類中提供了一個proceed()方法,該方法用於執行目標方法的邏輯。所以在自定義外掛類中,攔截邏輯執行完畢後一般都需要呼叫proceed()方法執行目標方法的原有邏輯。
public class Invocation { // 目標物件,即ParameterHandler、ResultSetHandler、StatementHandler或者Executor例項 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; } public Object getTarget() { return target; } public Method getMethod() { return method; } public Object[] getArgs() { return args; } /** * 執行目標方法 * @return 目標方法執行結果 * @throws InvocationTargetException * @throws IllegalAccessException */ public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } }View Code
當我們自定義攔截器的時候,需要實現該介面。mybatis中提供了ExamplePlugin類,供我們參考,可以看一下原始碼
@Intercepts({}) public class ExamplePlugin implements Interceptor { private Properties properties; @Override public Object intercept(Invocation invocation) throws Throwable { // TODO:自定義攔截邏輯 return invocation.proceed(); } @Override public Object plugin(Object target) { // 呼叫Plugin類的wrap()方法返回一個動態代理物件 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 設定外掛的屬性資訊 this.properties = properties; } public Properties getProperties() { return properties; } }
我們深入interceptor.plugin(target);的方法,發現他呼叫的是Plugin.wrap(target, this); 繼續深入,檢視PluginPlugin的原始碼如下:
public class Plugin implements InvocationHandler { //目標物件,即Executor、ParameterHandler、ResultSetHandler、StatementHandler物件 private final Object target; // 使用者自定義攔截器例項 private final Interceptor interceptor; // Intercepts註解指定的方法 private final Map<Class<?>, Set<Method>> signatureMap; private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } /** * wrap()方法的第一個引數為目標物件,即Executor、ParameterHandler、 ResultSetHandler、 StatementHandler類的例項; * 第二個引數為攔截器例項。在wrap()方法中首先呼叫getSignatureMap()方法獲取Intercepts註解指定的要攔截的元件及方法, * 然後呼叫getAllInterfaces()方法獲取當前Intercepts註解指定要攔截的元件的介面資訊,接著呼叫Proxy類的靜態方法 * newProxyInstance()建立一個動態代理物件。 * 該方法用於建立Executor、ParameterHandler、ResultSetHandler、StatementHandler的代理物件 * * @param target * @param interceptor * @return */ public static Object wrap(Object target, Interceptor interceptor) { // 呼叫getSignatureMap()方法獲取自定義外掛中,通過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; } /** * Plugin類的invoke()方法會在呼叫目標物件的方法時執行,在invoke()方法中首先判斷該方法是否被Intercepts註解指定為被攔截的方法,如果是,則呼叫使用者自定義 * 攔截器的intercept()方法,並把目標方法資訊封裝成Invocation物件作為intercept()方法的引數。 * * @param proxy * @param method * @param args * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果該方法是Intercepts註解指定的方法,則呼叫攔截器例項的intercept()方法執行攔截邏輯 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); } } /** * Plugin類的getSignatureMap()方法中,首先獲取Intercepts註解,然後獲取Intercepts註解中配置的所有Signature註解,接著對所有的Signature註解資訊進行遍歷, * 將Signature註解中指定要攔截的元件及方法新增到Map物件中,其中 * Key為Executor、 ParameterHandler、ResultSetHandler或StatementHandler對應的Class物件,Value為攔截的所有方法對應的Method物件陣列。 * * @param interceptor * @return */ private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { // 獲取Intercepts註解資訊 Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } // 獲取所有Signature註解資訊 Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); // 對所有Signature註解進行遍歷,把Signature註解指定攔截的元件及方法新增到Map中 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; } /** * 獲取目標型別的介面資訊 * * @param type * @param signatureMap * @return */ 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()]); } }
由上述程式碼,發現Plugin,實現了InvocationHandler 即JDK內建的動態代理方式建立代理物件,同時使用Proxy.newProxyInstance返回了代理物件。
如上面的程式碼所示,wrap()方法的第一個引數為目標物件,即Executor、ParameterHandler、 ResultSetHandler、 StatementHandler類的例項;第二個引數為攔截器例項。在wrap()方法中首先呼叫
getSignatureMap()方法獲取Intercepts註解指定的要攔截的元件及方法,然後呼叫getAllInterfaces()方法獲取當前Intercepts註解指定要攔截的元件的介面資訊,接著呼叫Proxy類的靜態方法
newProxyInstance()建立一個動態代理物件。
2.自定義mybatis外掛
2.1mybatis自定義外掛,都必須實現Interceptor介面,並在intercept()中編寫攔截邏輯
2.2 攔截器上面要新增@Intercepts註解,標註攔截器的型別和攔截的方法
例如:
@Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class}) }) public class PageInterceptor implements Interceptor {
2.3 通過plugin()方法返回一個動態代理物件
例如:
/** * 攔截器對應的封裝原始物件的方法 */ public Object plugin(Object target) { return Plugin.wrap(target, this); }
2.4通過setProperties方法設定Plugin標籤中配置的屬性值
例如:
/** * 設定註冊攔截器時設定的屬性 */ public void setProperties(Properties properties) { this.databaseType = properties.getProperty("databaseType"); }
2.5 mybaits提供了ExamplePlugin的例子供參考
@Intercepts({}) public class ExamplePlugin implements Interceptor { private Properties properties; @Override public Object intercept(Invocation invocation) throws Throwable { // TODO:自定義攔截邏輯 return invocation.proceed(); } @Override public Object plugin(Object target) { // 呼叫Plugin類的wrap()方法返回一個動態代理物件 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 設定外掛的屬性資訊 this.properties = properties; } public Properties getProperties() { return properties; } }ExamplePlugin
3.mybatis外掛載入的地方
3.1 在mybatis的plugins標籤下面,配置要載入的外掛
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE configuration 3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-config.dtd"> 5 6 <configuration> 7 <settings> 8 <setting name="useGeneratedKeys" value="true"/> 9 </settings> 10 11 // 配置外掛 12 <plugins> 13 <plugin interceptor="com.blog4java.plugin.pager.PageInterceptor"> 14 <property name="databaseType" value="hsqldb"/> 15 </plugin> 16 17 <plugin interceptor="com.blog4java.plugin.slowsql.SlowSqlInterceptor"> 18 <property name="limitSecond" value="0"/> 19 </plugin> 20 </plugins> 21 22 <environments default="dev" > 23 <environment id="dev"> 24 <transactionManager type="JDBC"> 25 <property name="" value="" /> 26 </transactionManager> 27 <dataSource type="UNPOOLED"> 28 <property name="driver" value="org.hsqldb.jdbcDriver" /> 29 <property name="url" value="jdbc:hsqldb:mem:mybatis" /> 30 <property name="username" value="sa" /> 31 <property name="password" value="" /> 32 </dataSource> 33 </environment> 34 <environment id="qa"> 35 <transactionManager type="JDBC"> 36 <property name="" value="" /> 37 </transactionManager> 38 <dataSource type="UNPOOLED"> 39 <property name="driver" value="org.hsqldb.jdbcDriver" /> 40 <property name="url" value="jdbc:hsqldb:mem:mybatis_qa" /> 41 <property name="username" value="admin" /> 42 <property name="password" value="admin" /> 43 </dataSource> 44 </environment> 45 </environments> 46 47 <mappers> 48 <mapper resource="com/blog4java/mybatis/example/mapper/UserMapper.xml"/> 49 </mappers> 50 </configuration>mybatis-config
3.2 從XmlConfigBuilder類開始解析mybaits-config的主配置檔案
首先呼叫XmlConfigBuilder.parse()方法
public Configuration parse() { // 防止parse()方法被同一個例項多次呼叫 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; // 呼叫XPathParser.evalNode()方法,建立表示configuration節點的XNode物件。 // 呼叫parseConfiguration()方法對XNode進行處理 parseConfiguration(parser.evalNode("/configuration")); return configuration; }
然後呼叫parseConfiguration 解析<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); } }
然後呼叫pluginElement(root.evalNode("plugins")); 解析mybatis主配置檔案下面的plugins標籤
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 獲取<plugin>標籤的interceptor屬性 String interceptor = child.getStringAttribute("interceptor"); // 獲取攔截器屬性,轉換為Properties物件 Properties properties = child.getChildrenAsProperties(); // 建立攔截器例項 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); // 設定攔截器例項屬性資訊 interceptorInstance.setProperties(properties); // 將攔截器例項新增到攔截器鏈中 configuration.addInterceptor(interceptorInstance); } } }
由上程式碼可以看出,迴圈解析<plugins>標籤下面的每一個<plugin> ,然後建立攔截器新增到集合中
Configuration.addInterceptor(interceptorInstance) 的原始碼如下:
public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
剛才說過,interceptorChain內部維護了一個List集合,用來儲存攔截器
4.Mybatis外掛建立動態代理物件的工作原理
以執行一個查詢操作為例,SqlSession是MyBatis中提供的面向使用者的操作資料庫的介面,而真正執行SQL操作的是Executor元件。MyBatis通過工廠模式建立Executor例項,
Configuration類中提供了一個newExecutor()工廠 方法,該方法返回的實際上是一個Executor的動態代理物件。
SqlSession獲取Executor例項的過程如下:
(1) SqlSession中會呼叫Configuration類提供的newExecutor()工廠方法建立Executor物件。
(2) Configuration類中通過一個InterceptorChain物件維護了使用者自定義的攔截器鏈。newExecutor()工廠方法中呼叫InterceptorChain物件的pluginAll()方法。
(3) InterceptorChain物件 的pluginAll()方法中會呼叫自定義攔截器的plugin()方法。
(4)自定義攔截器的plugin()方法是由我們來編寫的,通常會呼叫Plugin類的wrap()靜態方法建立一個代理物件。
mybatis動態代理物件建立過程
Sq|Session獲取到的Executor例項實際上已經是一個動態代理物件了。當我們呼叫SqlSession物件的selectOne()方法執行查詢操作時,大致會經歷下面幾個過程:
(1)SqlSession操作資料庫需要依賴於Executor元件,SqlSession會呼叫Configuration物件的newExecutor()方法獲取Executor的例項。
(2) SqlSession獲取到的是Executor元件的代理物件,執行查詢操作時會呼叫代理物件的query()方法。
(3)按照JDK動態代理機制,呼叫Executor代理物件的query()方法時,會呼叫Plugin類的invoke()方法。
(4) Plugin類的invoke()方法中會呼叫自定義攔截器物件的intercept()方法執行攔截邏輯。
(5)自定義攔截器物件的intercept()方法呼叫完畢後,呼叫目標Executor物件的query()方法。
(6)所有操作執行完畢後,會將查詢結果返回給SqlSession物件。
mybatis外掛攔截邏輯執行過程