一、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();
}
}