<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在很多業務場景下我們需要去攔截 SQL
,達到不入侵原有程式碼業務處理一些東西,比如:歷史記錄、分頁操作、資料許可權過濾操作、SQL
執行時間效能監控等等,這裡我們就可以用到 MyBatis
的外掛 Plugin
。下面我們來了解一下 Plugin
到底是如何工作的。
使用過 MyBatis
框架的朋友們肯定都聽說過 PageHelper
這個分頁神器吧,其實 PageHelper
的底層實現就是依靠 plugin
。下面我們來看一下 PageHelper
是如何利用 plugin
實現分頁的。
首先我們先看一下 MyBatis
的執行流程圖,對其執行流程有一個大體的認識。
從 MyBatis
程式碼實現的角度來看,MyBatis
的主要的核心部件有以下幾個:
Configuration
:初始化基礎設定,比如 MyBatis
的別名等,一些重要的型別物件,如,外掛,對映器,ObjectFactory
和 typeHandler
物件,MyBatis
所有的設定資訊都維持在 Configuration
物件之中。SqlSessionFactory
:SqlSession
工廠,用於生產 SqlSession
。SqlSession
: 作為 MyBatis
工作的主要頂層 API
,表示和資料庫互動的對談,完成必要資料庫增刪改查功能Executor
:MyBatis
執行器,是 MyBatis
排程的核心,負責 SQL
語句的生成和查詢快取的維護StatementHandler
:封裝了 JDBC Statement
操作,負責對 JDBC Statement
的操作,如設定引數、將 Statement
結果集轉換成List集合。ParameterHandler
:負責對使用者傳遞的引數轉換成 JDBC Statement
所需要的引數,ResultSetHandler
:負責將 JDBC
返回的 ResultSet
結果集物件轉換成 List
型別的集合;TypeHandler
:負責 java
資料型別和 jdbc
資料型別之間的對映和轉換MappedStatement
:MappedStatement
維護了一條 <select|update|delete|insert>
節點的封裝,SqlSource
:負責根據使用者傳遞的 parameterObject
,動態地生成 SQL
語句,將資訊封裝到 BoundSql
物件中,並返回BoundSql
:表示動態生成的 SQL
語句以及相應的引數資訊說了這麼多,怎麼還沒進入正題啊,別急,下面就開始講解 Plugin
的實現原理。
MyBatis
支援對 Executor、StatementHandler、PameterHandler和ResultSetHandler
介面進行攔截,也就是說會對這4種物件進行代理。
下面我們結合 PageHelper
來講解 Plugin
是怎樣實現的。
要使用自定義 Plugin
首先要實現 Interceptor
介面。可以通俗的理解為一個 Plugin
就是一個攔截器。
public interface Interceptor { // 實現攔截邏輯 Object intercept(Invocation invocation) throws Throwable; // 獲取代理類 Object plugin(Object target); // 初始化設定 void setProperties(Properties properties); }
現在我們來看一下 PageHelper
是如何通過 Plugin
實現分頁的。
@Intercepts( { @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), } ) public class PageInterceptor implements Interceptor { //快取count查詢的ms protected Cache<CacheKey, MappedStatement> msCountMap = null; private Dialect dialect; private String default_dialect_class = "com.github.pagehelper.PageHelper"; private Field additionalParametersField; @Override public Object intercept(Invocation invocation) throws Throwable { try { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; Executor executor = (Executor) invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; //由於邏輯關係,只會進入一次 if(args.length == 4){ //4 個引數時 boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { //6 個引數時 cacheKey = (CacheKey) args[4]; boundSql = (BoundSql) args[5]; } List resultList; //呼叫方法判斷是否需要進行分頁,如果不需要,直接返回結果 if (!dialect.skip(ms, parameter, rowBounds)) { //反射獲取動態引數 Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql); //判斷是否需要進行 count 查詢 if (dialect.beforeCount(ms, parameter, rowBounds)) { //建立 count 查詢的快取 key CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql); countKey.update(MSUtils.COUNT); MappedStatement countMs = msCountMap.get(countKey); if (countMs == null) { //根據當前的 ms 建立一個返回值為 Long 型別的 ms countMs = MSUtils.newCountMappedStatement(ms); msCountMap.put(countKey, countMs); } //呼叫方言獲取 count sql String countSql = dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey); countKey.update(countSql); BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter); //當使用動態 SQL 時,可能會產生臨時的引數,這些引數需要手動設定到新的 BoundSql 中 for (String key : additionalParameters.keySet()) { countBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //執行 count 查詢 Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql); Long count = (Long) ((List) countResultList).get(0); //處理查詢總數 //返回 true 時繼續分頁查詢,false 時直接返回 if (!dialect.afterCount(count, parameter, rowBounds)) { //當查詢總數為 0 時,直接返回空的結果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } //判斷是否需要進行分頁查詢 if (dialect.beforePage(ms, parameter, rowBounds)) { //生成分頁的快取 key CacheKey pageKey = cacheKey; //處理引數物件 parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey); //呼叫方言獲取分頁 sql String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey); BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter); //設定動態引數 for (String key : additionalParameters.keySet()) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //執行分頁查詢 resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); } else { //不執行分頁的情況下,也不執行記憶體分頁 resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } } else { //rowBounds用引數值,不使用分頁外掛處理時,仍然支援預設的記憶體分頁 resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { dialect.afterAll(); } } @Override public Object plugin(Object target) { //TODO Spring bean 方式設定時,如果沒有設定屬性就不會執行下面的 setProperties 方法,就不會初始化,因此考慮在這個方法中做一次判斷和初始化 //TODO https://github.com/pagehelper/Mybatis-PageHelper/issues/26 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { //快取 count ms msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties); String dialectClass = properties.getProperty("dialect"); if (StringUtil.isEmpty(dialectClass)) { dialectClass = default_dialect_class; } try { Class<?> aClass = Class.forName(dialectClass); dialect = (Dialect) aClass.newInstance(); } catch (Exception e) { throw new PageException(e); } dialect.setProperties(properties); try { //反射獲取 BoundSql 中的 additionalParameters 屬性 additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters"); additionalParametersField.setAccessible(true); } catch (NoSuchFieldException e) { throw new PageException(e); } } }
程式碼太長不看系列: 其實這段程式碼最主要的邏輯就是在執行 Executor
方法的時候,攔截 query
也就是查詢型別的 SQL
, 首先會判斷它是否需要分頁,如果需要分頁就會根據查詢引數在 SQL
末尾加上 limit pageNum, pageSize
來實現分頁。
SqlSessionFactoryBean
去構建 Configuration
新增攔截器並構建獲取 SqlSessionFactory
。public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> { // ... 此處省略部分原始碼 protected SqlSessionFactory buildSqlSessionFactory() throws IOException { // ... 此處省略部分原始碼 // 檢視是否注入攔截器,有的話新增到Interceptor集合裡面 if (!isEmpty(this.plugins)) { for (Interceptor plugin : this.plugins) { configuration.addInterceptor(plugin); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered plugin: '" + plugin + "'"); } } } // ... 此處省略部分原始碼 return this.sqlSessionFactoryBuilder.build(configuration); } // ... 此處省略部分原始碼 }
XMLConfigBuilder
構建 configuration
新增攔截器public class XMLConfigBuilder extends BaseBuilder { //解析設定 private void parseConfiguration(XNode root) { try { //省略部分程式碼 pluginElement(root.evalNode("plugins")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } 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); //呼叫InterceptorChain.addInterceptor configuration.addInterceptor(interceptorInstance); } } } }
上面是兩種不同的形式構建 configuration
並新增攔截器 interceptor
,上面第二種一般是以前 XML
設定的情況,這裡主要是解析組態檔的 plugin
節點,根據設定的 interceptor
屬性範例化 Interceptor
物件,然後新增到 Configuration
物件中的 InterceptorChain
屬性中。
如果定義多個攔截器就會它們鏈起來形成一個攔截器鏈,初始化組態檔的時候就把所有的攔截器新增到攔截器鏈中。
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { //迴圈呼叫每個Interceptor.plugin方法 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
在範例化 Executor、ParameterHandler、ResultSetHandler、StatementHandler
四大介面物件的時候呼叫 interceptorChain.pluginAll()
方法插入進去的。
其實就是迴圈執行攔截器鏈所有的攔截器的 plugin()
方法, MyBatis
官方推薦的 plugin
方法是 Plugin.wrap()
方法,這個就會生成代理類。
public class Configuration { protected final InterceptorChain interceptorChain = new InterceptorChain(); //建立引數處理器 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { //建立ParameterHandler 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) { //建立DefaultResultSetHandler 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; //這句再做一下保護,囧,防止粗心大意的人將defaultExecutorType設成null? executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; //然後就是簡單的3個分支,產生3種執行器BatchExecutor/ReuseExecutor/SimpleExecutor 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); } //如果要求快取,生成另一種CachingExecutor(預設就是有快取),裝飾者模式,所以預設都是返回CachingExecutor if (cacheEnabled) { executor = new CachingExecutor(executor); } //此處呼叫外掛,通過外掛可以改變Executor行為 executor = (Executor) interceptorChain.pluginAll(executor); return executor; } }
我們首先看一下Plugin.wrap()
方法,這個方法的作用是為實現Interceptor註解的介面實現類生成代理物件的。
// 如果是Interceptor註解的介面的實現類會產生代理類 public static Object wrap(Object target, Interceptor interceptor) { //從攔截器的註解中獲取攔截的類名和方法資訊 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); //取得要改變行為的類(ParameterHandler|ResultSetHandler|StatementHandler|Executor) Class<?> type = target.getClass(); //取得介面 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); //產生代理,是Interceptor註解的介面的實現類才會產生代理 if (interfaces.length > 0) { return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap)); } return target; }
Plugin
中的 getSignatureMap、 getAllInterfaces
兩個輔助方法,來幫助判斷是否為是否Interceptor註解的介面實現類。
//取得簽名Map,就是獲取Interceptor實現類上面的註解,要攔截的是那個類(Executor //,ParameterHandler, ResultSetHandler,StatementHandler)的那個方法 private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { //取Intercepts註解 Intercepts interceptsAnnotation =interceptor.getClass().getAnnotation(Intercepts.class); //必須得有Intercepts註解,沒有報錯 if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } //value是陣列型,Signature的陣列 Signature[] sigs = interceptsAnnotation.value(); //每個class裡有多個Method需要被攔截,所以這麼定義 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; } //取得介面 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()]); } }
我們來看一下代理類的 query
方法,其實就是呼叫了 Plugin.invoke()
方法。代理類遮蔽了 intercept
方法的呼叫。
public final List query(MappedStatement mappedStatement, Object object, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException { try { // 這裡的 h 就是一個 Plugin return (List)this.h.invoke(this, m5, new Object[]{mappedStatement, object, rowBounds, resultHandler, cacheKey, boundSql}); } catch (Error | RuntimeException | SQLException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } }
最後 Plugin.invoke()
就是判斷當前方法是否攔截,如果需要攔截則會呼叫 Interceptor.intercept()
對當前方法執行攔截邏輯。
public class Plugin implements InvocationHandler { ... @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //獲取需要攔截的方法 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); //是Interceptor實現類註解的方法才會攔截處理 if (methods != null && methods.contains(method)) { //呼叫Interceptor.intercept,即呼叫自己寫的邏輯 return interceptor.intercept(new Invocation(target, method, args)); } //最後執行原來邏輯 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } ...
我們以 PageHelper
為切入點講解了 MyBatis Plugin
的實現原理,其中 MyBatis 攔截器用到責任鏈模式+動態代理+反射機制。 通過上面的分析可以知道,所有可能被攔截的處理類都會生成一個代理類,如果有 N 個攔截器,就會有 N 個代理,層層生成動態代理是比較耗效能的。而且雖然能指定外掛攔截的位置,但這個是在執行方法時利用反射動態判斷的,初始化的時候就是簡單的把攔截器插入到了所有可以攔截的地方。所以儘量不要編寫不必要的攔截器,並且攔截器儘量不要寫複雜的邏輯。
以上就是從 PageHelper 到 MyBatis Plugin執行概要及實現原理的詳細內容,更多關於PageHelper MyBatis Plugin的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45