一、MyBatis外掛
外掛是一種常見的擴充套件方式,大多數開源框架也都支援使用者通過新增自定義外掛的方式來擴充套件或者改變原有的功能,MyBatis中也提供的有外掛,雖然叫外掛,但是實際上是通過攔截器(Interceptor)實現的,在MyBatis的外掛模組中涉及到責任鏈模式和JDK動態代理。
1. 自定義外掛
首先我們來看下一個自定義的外掛我們要如何來實現。https://mybatis.org/mybatis-3/zh/configuration.html#plugins
1.1 建立Interceptor實現類
建立的攔截器必須要實現Interceptor介面,Interceptor介面的定義為
public interface Interceptor { // 執行攔截邏輯的方法 111 Object intercept(Invocation invocation) throws Throwable; // 決定是否觸發 intercept()方法 default Object plugin(Object target) { return Plugin.wrap(target, this); } // 根據配置 初始化 Intercept 物件 default void setProperties(Properties properties) { // NOP } }
在MyBatis中Interceptor允許攔截的內容是
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
自己建立一個攔截Executor中的query和close的方法
/** * 自定義的攔截器 * @Signature 註解就可以表示一個方法簽名, 唯一確定一個方法 */ @Intercepts({ @Signature( type = Executor.class // 需要攔截的型別 ,method = "query" // 需要攔截的方法 // args 中指定 被攔截方法的 引數列表 ,args={MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class} ), @Signature( type = Executor.class ,method = "close" ,args = {boolean.class} ) }) public class FirstInterceptor implements Interceptor { private int testProp; /** * 執行攔截邏輯的方法 * @param invocation * @return * @throws Throwable */ public Object intercept(Invocation invocation) throws Throwable { System.out.println("FirtInterceptor 攔截之前 ...."); Object obj = invocation.proceed(); System.out.println("FirtInterceptor 攔截之後 ...."); return obj; } /** * 決定是否觸發 intercept方法 * @param target * @return */ public Object plugin(Object target) { return Plugin.wrap(target,this); } public void setProperties(Properties properties) { System.out.println("---->"+properties.get("testProp")); } public int getTestProp() { return testProp; } public void setTestProp(int testProp) { this.testProp = testProp; } }
1.2 配置攔截器
建立好自定義的攔截器後,需要在全域性配置檔案中新增自定義外掛的註冊
然後執行測試類中程式碼就 可以看到日誌資訊
2. 外掛實現原理
自定義外掛的步驟還是比較簡單的,接下來就是分析下外掛的實現原理
2.1 初始化操作
首先看下在全域性配置檔案載入解析的時候做了什麼操作。解析配置檔案的步驟前面原始碼跟了好多次就不再看了,直接從SqlSession sqlSession = factory.openSession();這裡開始
@Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { //事務物件 Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); // 獲取事務工廠 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 建立事務 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 根據事務工廠和預設的執行器型別,建立執行器 >>執行SQL語句操作 final Executor executor = configuration.newExecutor(tx, execType); 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(); } }
還是從他的執行器中找
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) {//針對Statement做快取 executor = new ReuseExecutor(this, transaction); } else { // 預設 SimpleExecutor,每一次只是SQL操作都建立一個新的Statement物件 executor = new SimpleExecutor(this, transaction); } // 二級快取開關,settings 中的 cacheEnabled 預設是 true if (cacheEnabled) { executor = new CachingExecutor(executor); } // 植入外掛的邏輯,至此,四大物件已經全部攔截完畢;這裡面是一個攔截器鏈 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
從上面程式碼可以看到外掛的入口
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); } }
從上面可以看到他是對interceptors進行解析迴圈,至於這個interceptors是怎麼來的那還是得看SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);這裡面的邏輯
public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); }
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 用於解析 mybatis-config.xml,同時建立了 Configuration 物件 >> XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 解析XML,最終返回一個 DefaultSqlSessionFactory >> 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. } } }
看他的解析步驟
public Configuration parse() { //檢查是否已經解析過了 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; // XPathParser,dom 和 SAX 都有用到 >> parseConfiguration(parser.evalNode("/configuration")); return configuration; }
private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 對於全域性配置檔案各種標籤的解析 propertiesElement(root.evalNode("properties")); // 解析 settings 標籤 Properties settings = settingsAsProperties(root.evalNode("settings")); // 讀取檔案 loadCustomVfs(settings); // 日誌設定 loadCustomLogImpl(settings); // 型別別名 typeAliasesElement(root.evalNode("typeAliases")); // 外掛 pluginElement(root.evalNode("plugins")); // 用於建立物件 objectFactoryElement(root.evalNode("objectFactory")); // 用於對物件進行加工 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 反射工具箱 reflectorFactoryElement(root.evalNode("reflectorFactory")); // settings 子標籤賦值,預設值就是在這裡提供的 >> settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 // 建立了資料來源 >> environmentsElement(root.evalNode("environments")); //解析databaseIdProvider標籤,生成DatabaseIdProvider物件(用來支援不同廠商的資料庫)。 databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); // 解析引用的Mapper對映器 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
可以看到pluginElement(root.evalNode("plugins"));的外掛解析操作
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 獲取<plugin> 節點的 interceptor 屬性的值 String interceptor = child.getStringAttribute("interceptor"); // 獲取<plugin> 下的所有的properties子節點 Properties properties = child.getChildrenAsProperties(); // 獲取 Interceptor 物件 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); // 設定 interceptor的 屬性 interceptorInstance.setProperties(properties); // Configuration中記錄 Interceptor configuration.addInterceptor(interceptorInstance); } } }
可以看到對interceptor的解析操作,至於這interceptor的位置就是前面配置的配置
<plugin interceptor="com.ghy.interceptor.FirstInterceptor"> <property name="testProp" value="1000"/> </plugin>
這樣一來就明白了自定義的外掛是怎麼在原始碼中讀取的,pluginElement就是這樣通過迴圈讀取到配置資訊然後通過反射獲取Interceptor物件最後加到configuration中去
public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
而他儲存的addInterceptor就到了下面的程式碼中去了
public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); }
// 儲存所有的 Interceptor 也就我所有的外掛是儲存在 Interceptors 這個List集合中的 private final List<Interceptor> interceptors = new ArrayList<>();
這樣一來,整個鏈路就通了;這個過程其實就是系統啟動時把我們自定義的外掛配置新增到了集合中去;
2.2 如何建立代理物件
初始化過程看完了接下來就是看代理物件的建立了,在解析的時候建立了對應的Interceptor物件,並儲存在了InterceptorChain中,那麼這個攔截器是如何和對應的目標物件進行關聯的呢?首先攔截器可以攔截的物件是Executor,ParameterHandler,ResultSetHandler,StatementHandler.那麼我們來看下這四個物件在建立的時候又什麼要注意的
2.2.1 Executor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) {//針對Statement做快取 executor = new ReuseExecutor(this, transaction); } else { // 預設 SimpleExecutor,每一次只是SQL操作都建立一個新的Statement物件 executor = new SimpleExecutor(this, transaction); } // 二級快取開關,settings 中的 cacheEnabled 預設是 true if (cacheEnabled) { executor = new CachingExecutor(executor); } // 植入外掛的邏輯,至此,四大物件已經全部攔截完畢;這裡面是一個攔截器鏈 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
可以看到Executor在裝飾完二級快取後會通過pluginAll來建立Executor的代理物件
public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { // 獲取攔截器鏈中的所有攔截器 target = interceptor.plugin(target); // 建立對應的攔截器的代理物件 } return target; }
進入plugin方法中,我們會進入到
// 決定是否觸發 intercept()方法 default Object plugin(Object target) { return Plugin.wrap(target, this); }
然後進入到MyBatis給我們提供的Plugin工具類的實現 wrap方法中。
/** * 建立目標物件的代理物件 * 目標物件 Executor ParameterHandler ResultSetHandler StatementHandler * @param target 目標物件 * @param interceptor 攔截器 * @return */ public static Object wrap(Object target, Interceptor interceptor) { // 獲取使用者自定義 Interceptor中@Signature註解的資訊 // getSignatureMap 負責處理@Signature 註解 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中的各個方法的作用
public class Plugin implements InvocationHandler { private final Object target; // 目標物件 private final Interceptor interceptor; // 攔截器 private final Map<Class<?>, Set<Method>> signatureMap; // 記錄 @Signature 註解的資訊 private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } /** * 建立目標物件的代理物件 * 目標物件 Executor ParameterHandler ResultSetHandler StatementHandler * @param target 目標物件 * @param interceptor 攔截器 * @return */ public static Object wrap(Object target, Interceptor interceptor) { // 獲取使用者自定義 Interceptor中@Signature註解的資訊 // getSignatureMap 負責處理@Signature 註解 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; } /** * 代理物件方法被呼叫時執行的程式碼 * @param proxy * @param method * @param args * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 獲取當前方法所在類或介面中,可被當前Interceptor攔截的方法 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); } } 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<>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>()); 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; } private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<>(); 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()]); } }
這一段搞通後其實就可以看攔截的邏輯了,也就是執行sqlSession.selectList("com.ghy.mapper.UserMapper.selectUserList");時做了啥
@Override public <E> List<E> selectList(String statement) { return this.selectList(statement, null); }
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); // 如果 cacheEnabled = true(預設),Executor會被 CachingExecutor裝飾 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
當呼叫executor.query方法時其實就滿足了攔截的條件了,這個物件中的引數完全滿足在自定義攔截中定義的引數型別,所以在呼叫這個方法時就會被攔截呼叫如下方法
就走進了我們自己寫的邏輯中來了
/** * 執行攔截邏輯的方法 * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("FirtInterceptor 攔截之前 ...."); Object obj = invocation.proceed(); System.out.println("FirtInterceptor 攔截之後 ...."); return obj; }
2.2.2 StatementHandler
在定義搞明白後,接下來要搞的就是官網上說的另外三個外掛的植入在哪;回退到
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); // 如果 cacheEnabled = true(預設),Executor會被 CachingExecutor裝飾 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // 一級快取和二級快取的CacheKey是同一個 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
@SuppressWarnings("unchecked") @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 異常體系之 ErrorContext ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { // flushCache="true"時,即使是查詢,也清空一級快取 clearLocalCache(); } List<E> list; try { // 防止遞迴查詢重複處理快取 queryStack++; // 查詢一級快取 // ResultHandler 和 ResultSetHandler的區別 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { //快取中有資料 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 真正的查詢流程 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; // 先佔位 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 三種 Executor 的區別,看doUpdate // 預設Simple list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { // 移除佔位符 localCache.removeObject(key); } // 寫入一級快取 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
進入資料庫查詢
@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(); // 注意,已經來到SQL處理的關鍵物件 StatementHandler >> StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 獲取一個 Statement物件 stmt = prepareStatement(handler, ms.getStatementLog()); // 執行查詢 return handler.query(stmt, resultHandler); } finally { // 用完就關閉 closeStatement(stmt); } }
在進入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; }
可以看到statementHandler的代理物件
2.2.3 ParameterHandler
這個在上面步驟的RoutingStatementHandler方法中
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // StatementType 是怎麼來的? 增刪改查標籤中的 statementType="PREPARED",預設值 PREPARED switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case PREPARED: // 建立 StatementHandler 的時候做了什麼? >> delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case CALLABLE: delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } }
然後隨便選擇一個分支進入,比如PreparedStatementHandler
public class PreparedStatementHandler extends BaseStatementHandler { public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql); } @Override public int update(Statement statement) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); int rows = ps.getUpdateCount(); Object parameterObject = boundSql.getParameterObject(); KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject); return rows; } @Override public void batch(Statement statement) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.addBatch(); } @Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // 到了JDBC的流程 ps.execute(); // 處理結果集 return resultSetHandler.handleResultSets(ps); } @Override public <E> Cursor<E> queryCursor(Statement statement) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.handleCursorResultSets(ps); } @Override protected Statement instantiateStatement(Connection connection) throws SQLException { String sql = boundSql.getSql(); if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { String[] keyColumnNames = mappedStatement.getKeyColumns(); if (keyColumnNames == null) { return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); } else { // 在執行 prepareStatement 方法的時候會進入進入到ConnectionLogger的invoker方法中 return connection.prepareStatement(sql, keyColumnNames); } } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) { return connection.prepareStatement(sql); } else { return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); } } @Override public void parameterize(Statement statement) throws SQLException { parameterHandler.setParameters((PreparedStatement) statement); } }
public abstract class BaseStatementHandler implements StatementHandler { protected final Configuration configuration; protected final ObjectFactory objectFactory; protected final TypeHandlerRegistry typeHandlerRegistry; protected final ResultSetHandler resultSetHandler; protected final ParameterHandler parameterHandler; protected final Executor executor; protected final MappedStatement mappedStatement; protected final RowBounds rowBounds; protected BoundSql boundSql; 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); } @Override public BoundSql getBoundSql() { return boundSql; } @Override public ParameterHandler getParameterHandler() { return parameterHandler; } @Override public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { ErrorContext.instance().sql(boundSql.getSql()); Statement statement = null; try { statement = instantiateStatement(connection); setStatementTimeout(statement, transactionTimeout); setFetchSize(statement); return statement; } catch (SQLException e) { closeStatement(statement); throw e; } catch (Exception e) { closeStatement(statement); throw new ExecutorException("Error preparing statement. Cause: " + e, e); } } protected abstract Statement instantiateStatement(Connection connection) throws SQLException; protected void setStatementTimeout(Statement stmt, Integer transactionTimeout) throws SQLException { Integer queryTimeout = null; if (mappedStatement.getTimeout() != null) { queryTimeout = mappedStatement.getTimeout(); } else if (configuration.getDefaultStatementTimeout() != null) { queryTimeout = configuration.getDefaultStatementTimeout(); } if (queryTimeout != null) { stmt.setQueryTimeout(queryTimeout); } StatementUtil.applyTransactionTimeout(stmt, queryTimeout, transactionTimeout); } protected void setFetchSize(Statement stmt) throws SQLException { Integer fetchSize = mappedStatement.getFetchSize(); if (fetchSize != null) { stmt.setFetchSize(fetchSize); return; } Integer defaultFetchSize = configuration.getDefaultFetchSize(); if (defaultFetchSize != null) { stmt.setFetchSize(defaultFetchSize); } } protected void closeStatement(Statement statement) { try { if (statement != null) { statement.close(); } } catch (SQLException e) { //ignore } } protected void generateKeys(Object parameter) { KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); ErrorContext.instance().store(); keyGenerator.processBefore(executor, mappedStatement, null, parameter); ErrorContext.instance().recall(); }
在newParameterHandler的步驟可以發現代理物件的建立
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.2.4 ResultSetHandler
在上面的newResultSetHandler()方法中,也可以看到ResultSetHander的代理物件
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; }
2.3 多攔截器
如果我們有多個自定義的攔截器,那麼他的執行流程是怎麼樣的呢?比如我們建立了兩個Interceptor 都是用來攔截 Executor 的query方法,一個是用來執行邏輯A 一個是用來執行邏輯B的
如果說物件被代理了多次,這裡會繼續呼叫下一個外掛的邏輯,再走一次Plugin的invoke()方法。這裡需要關注一下有多個外掛的時候的執行順序。配置的順序和執行的順序是相反的。InterceptorChain的List是按照外掛從上往下的順序解析、新增的。而建立代理的時候也是按照list的順序代理。執行的時候當然是從最後代理的物件開始。
物件 | 作用 |
Interceptor | 自定義外掛需要實現介面,實現4個方法 |
InterceptChain | 配置的外掛解析後會儲存在Configuration的InterceptChain中 |
Plugin | 觸發管理類,還可以用來建立代理物件 |
Invocation | 對被代理類進行包裝,可以呼叫proceed()呼叫到被攔截的方法 |
3. PageHelper分析
Mybatis的外掛使用中分頁外掛PageHelper應該是我們使用到的比較多的外掛應用。先來看下PageHelper的應用
3.1 PageHelper的應用
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>4.1.6</version> </dependency>
在全域性配置檔案中註冊
<plugin interceptor="com.github.pagehelper.PageHelper"> <property name="dialect" value="mysql" /> <!-- 該引數預設為false --> <!-- 設定為true時,會將RowBounds第一個引數offset當成pageNum頁碼使用 --> <!-- 和startPage中的pageNum效果一樣 --> <property name="offsetAsPageNum" value="true" /> <!-- 該引數預設為false --> <!-- 設定為true時,使用RowBounds分頁會進行count查詢 --> <property name="rowBoundsWithCount" value="true" /> <!-- 設定為true時,如果pageSize=0或者RowBounds.limit = 0就會查詢出全部的結果 --> <!-- (相當於沒有執行分頁查詢,但是返回結果仍然是Page型別) --> <property name="pageSizeZero" value="true" /> <!-- 3.3.0版本可用 - 分頁引數合理化,預設false禁用 --> <!-- 啟用合理化時,如果pageNum<1會查詢第一頁,如果pageNum>pages會查詢最後一頁 --> <!-- 禁用合理化時,如果pageNum<1或pageNum>pages會返回空資料 --> <property name="reasonable" value="false" /> <!-- 3.5.0版本可用 - 為了支援startPage(Object params)方法 --> <!-- 增加了一個`params`引數來配置引數對映,用於從Map或ServletRequest中取值 --> <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置對映的用預設值 --> <!-- 不理解該含義的前提下,不要隨便複製該配置 --> <property name="params" value="pageNum=start;pageSize=limit;" /> <!-- always總是返回PageInfo型別,check檢查返回型別是否為PageInfo,none返回Page --> <property name="returnPageInfo" value="check" /> </plugin>
然後就是分頁查詢操作
通過MyBatis的分頁外掛的使用,在執行操作之前設定了一句PageHelper.startPage(1,5); 並沒有做其他操作,也就是沒有改變任何其他的業務程式碼。這就是它的優點,那麼再來看下他的實現原理
3.2 實現原理剖析
在PageHelper中,肯定有提供Interceptor的實現類,通過原始碼可以發現是PageHelper,而且我們也可以看到在該方法頭部新增的註解,宣告瞭該攔截器攔截的是Executor的query方法
然後當我們要執行查詢操作的時候,我們知道 Executor.query() 方法的執行本質上是執行 Executor的代理物件的方法,前面有說過。先來看下Plugin中的invoke方法
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 獲取當前方法所在類或介面中,可被當前Interceptor攔截的方法 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); } }
interceptor.intercept(new Invocation(target, method, args));方法的執行會進入到 PageHelper的intercept方法中
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 丟擲異常 */ public Object intercept(Invocation invocation) throws Throwable { if (autoRuntimeDialect) { SqlUtil sqlUtil = getSqlUtil(invocation); return sqlUtil.processPage(invocation); } else { if (autoDialect) { initSqlUtil(invocation); } return sqlUtil.processPage(invocation); } }
在interceptor方法中首先會獲取一個SqlUtils物件;SqlUtil:資料庫型別專用sql工具類,一個資料庫url對應一個SqlUtil例項,SqlUtil內有一個Pars物件,如果是mysql,它是MysqlParser,如果是oracle,它是OracleParser,這個Parser物件是SqlUtil不同例項的主要存在價值。執行count查詢、設定Parser物件、執行分頁查詢、儲存Page分頁物件等功能,均由SqlUtil來完成。
/** * 初始化sqlUtil * * @param invocation */ public synchronized void initSqlUtil(Invocation invocation) { if (this.sqlUtil == null) { this.sqlUtil = getSqlUtil(invocation); if (!autoRuntimeDialect) { properties = null; sqlUtilConfig = null; } autoDialect = false; } }
/** * 根據datasource建立對應的sqlUtil * * @param invocation */ public SqlUtil getSqlUtil(Invocation invocation) { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; //改為對dataSource做快取 DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource(); String url = getUrl(dataSource); if (urlSqlUtilMap.containsKey(url)) { return urlSqlUtilMap.get(url); } try { lock.lock(); if (urlSqlUtilMap.containsKey(url)) { return urlSqlUtilMap.get(url); } if (StringUtil.isEmpty(url)) { throw new RuntimeException("無法自動獲取jdbcUrl,請在分頁外掛中配置dialect引數!"); } String dialect = Dialect.fromJdbcUrl(url); if (dialect == null) { throw new RuntimeException("無法自動獲取資料庫型別,請通過dialect引數指定!"); } SqlUtil sqlUtil = new SqlUtil(dialect); if (this.properties != null) { sqlUtil.setProperties(properties); } else if (this.sqlUtilConfig != null) { sqlUtil.setSqlUtilConfig(this.sqlUtilConfig); } urlSqlUtilMap.put(url, sqlUtil); return sqlUtil; } finally { lock.unlock(); } }
/** * 構造方法 * * @param strDialect */ public SqlUtil(String strDialect) { if (strDialect == null || "".equals(strDialect)) { throw new IllegalArgumentException("Mybatis分頁外掛無法獲取dialect引數!"); } Exception exception = null; try { Dialect dialect = Dialect.of(strDialect); parser = AbstractParser.newParser(dialect); } catch (Exception e) { exception = e; //異常的時候嘗試反射,允許自己寫實現類傳遞進來 try { Class<?> parserClass = Class.forName(strDialect); if (Parser.class.isAssignableFrom(parserClass)) { parser = (Parser) parserClass.newInstance(); } } catch (ClassNotFoundException ex) { exception = ex; } catch (InstantiationException ex) { exception = ex; } catch (IllegalAccessException ex) { exception = ex; } } if (parser == null) { throw new RuntimeException(exception); } }
public static Parser newParser(Dialect dialect) { Parser parser = null; switch (dialect) { case mysql: case mariadb: case sqlite: parser = new MysqlParser(); break; case oracle: parser = new OracleParser(); break; case hsqldb: parser = new HsqldbParser(); break; case sqlserver: parser = new SqlServerParser(); break; case sqlserver2012: parser = new SqlServer2012Dialect(); break; case db2: parser = new Db2Parser(); break; case postgresql: parser = new PostgreSQLParser(); break; case informix: parser = new InformixParser(); break; case h2: parser = new H2Parser(); break; default: throw new RuntimeException("分頁外掛" + dialect + "方言錯誤!"); } return parser; }
我們可以看到不同的資料庫方言,建立了對應的解析器。然後再回到前面的interceptor方法中繼續,檢視sqlUtil.processPage(invocation);方法
/** * Mybatis攔截器方法,這一步巢狀為了在出現異常時也可以清空Threadlocal * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 丟擲異常 */ public Object processPage(Invocation invocation) throws Throwable { try { Object result = _processPage(invocation); return result; } finally { clearLocalPage(); } }
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 丟擲異常 */ private Object _processPage(Invocation invocation) throws Throwable { final Object[] args = invocation.getArgs(); Page page = null; //支援方法引數時,會先嚐試獲取Page if (supportMethodsArguments) { page = getPage(args); } //分頁資訊 RowBounds rowBounds = (RowBounds) args[2]; //支援方法引數時,如果page == null就說明沒有分頁條件,不需要分頁查詢 if ((supportMethodsArguments && page == null) //當不支援分頁引數時,判斷LocalPage和RowBounds判斷是否需要分頁 || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) { return invocation.proceed(); } else { //不支援分頁引數時,page==null,這裡需要獲取 if (!supportMethodsArguments && page == null) { page = getPage(args); } return doProcessPage(invocation, page, args); } }
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 丟擲異常 */ private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable { //儲存RowBounds狀態 RowBounds rowBounds = (RowBounds) args[2]; //獲取原始的ms MappedStatement ms = (MappedStatement) args[0]; //判斷並處理為PageSqlSource if (!isPageSqlSource(ms)) { processMappedStatement(ms); } //設定當前的parser,後面每次使用前都會set,ThreadLocal的值不會產生不良影響 ((PageSqlSource)ms.getSqlSource()).setParser(parser); try { //忽略RowBounds-否則會進行Mybatis自帶的記憶體分頁 args[2] = RowBounds.DEFAULT; //如果只進行排序 或 pageSizeZero的判斷 if (isQueryOnly(page)) { return doQueryOnly(page, invocation); } //簡單的通過total的值來判斷是否進行count查詢 if (page.isCount()) { page.setCountSignal(Boolean.TRUE); //替換MS args[0] = msCountMap.get(ms.getId()); //查詢總數 Object result = invocation.proceed(); //還原ms args[0] = ms; //設定總數 page.setTotal((Integer) ((List) result).get(0)); if (page.getTotal() == 0) { return page; } } else { page.setTotal(-1l); } //pageSize>0的時候執行分頁查詢,pageSize<=0的時候不執行相當於可能只返回了一個count if (page.getPageSize() > 0 && ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0) || rowBounds != RowBounds.DEFAULT)) { //將引數中的MappedStatement替換為新的qs page.setCountSignal(null); BoundSql boundSql = ms.getBoundSql(args[1]); args[1] = parser.setPageParameter(ms, args[1], boundSql, page); page.setCountSignal(Boolean.FALSE); //執行分頁查詢 Object result = invocation.proceed(); //得到處理結果 page.addAll((List) result); } } finally { ((PageSqlSource)ms.getSqlSource()).removeParser(); } //返回結果 return page; }
invocation.proceed();方法會進入 CachingExecutor中的query方法
@Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 獲取SQL BoundSql boundSql = ms.getBoundSql(parameterObject); // 建立CacheKey:什麼樣的SQL是同一條SQL? >> CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
ms.getBoundSql方法中會完成分頁SQL的繫結
@Override public BoundSql getBoundSql(Object parameterObject) { Boolean count = getCount(); if (count == null) { return getDefaultBoundSql(parameterObject); } else if (count) { return getCountBoundSql(parameterObject); } else { return getPageBoundSql(parameterObject); } } }
然後進入getPageBoundSql獲取分頁的SQL語句,在這個方法中也可以發現查詢總的記錄數的SQL生成;想看可以debugger看一下就明白了
@Override protected BoundSql getPageBoundSql(Object parameterObject) { String tempSql = sql; String orderBy = PageHelper.getOrderBy(); if (orderBy != null) { tempSql = OrderByParser.converToOrderBySql(sql, orderBy); } tempSql = localParser.get().getPageSql(tempSql); return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject); }
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
// 如果 cacheEnabled = true(預設),Executor會被 CachingExecutor裝飾
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}