MyBatis 經過提供插件機制,讓咱們能夠根據本身的須要去加強MyBatis 的功能。須要注意的是,若是沒有徹底理解MyBatis 的運行原理和插件的工做方式,最好不要使用插件,由於它會改變系底層的工做邏輯,給系統帶來很大的影響。html
MyBatis 的插件能夠在不修改原來的代碼的狀況下,經過攔截的方式,改變四大核心對象的行爲,好比處理參數,處理SQL,處理結果。git
第一個問題:github
不修改對象的代碼,怎麼對對象的行爲進行修改,好比說在原來的方法前面作一點事情,在原來的方法後面作一點事情?spring
答案:你們很容易能想到用代理模式,這個也確實是MyBatis 插件的原理。sql
第二個問題:apache
咱們能夠定義不少的插件,那麼這種全部的插件會造成一個鏈路,好比咱們提交一個休假申請,先是項目經理審批,而後是部門經理審批,再是HR 審批,再到總經理審批,怎麼實現層層的攔截?設計模式
答案:插件是層層攔截的,咱們又須要用到另外一種設計模式——責任鏈模式。session
在以前的源碼中咱們也發現了,mybatis內部對於插件的處理確實使用的代理模式,既然是代理模式,咱們應該瞭解MyBatis 容許哪些對象的哪些方法容許被攔截,並非每個運行的節點都是能夠被修改的。只有清楚了這些對象的方法的做用,當咱們本身編寫插件的時候才知道從哪裏去攔截。在MyBatis 官網有答案,咱們來看一下:http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins。mybatis
Executor 會攔截到CachingExcecutor 或者BaseExecutor。由於建立Executor 時是先建立CachingExcecutor,再包裝攔截。從代碼順序上能看到。咱們能夠經過mybatis的分頁插件來看看整個插件從包裝攔截器鏈到執行攔截器鏈的過程。app
在查看插件原理的前提上,咱們須要來看看官網對於自定義插件是怎麼來作的,官網上有介紹:經過 MyBatis 提供的強大機制,使用插件是很是簡單的,只需實現 Interceptor 接口,並指定想要攔截的方法簽名便可。這裏本人踩了一個坑,在Springboot中集成,同時引入了pagehelper-spring-boot-starter 致使RowBounds參數的值被刷掉了,也就是走到了個人攔截其中沒有被設置值,這裏須要注意,攔截器出了問題,能夠Debug看一下Configuration配置類中攔截器鏈的包裝狀況。
@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 MyPageInterceptor implements Interceptor { // 用於覆蓋被攔截對象的原有方法(在調用代理對象Plugin 的invoke()方法時被調用) @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("將邏輯分頁改成物理分頁"); Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; // MappedStatement BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter RowBounds rb = (RowBounds) args[2]; // RowBounds // RowBounds爲空,無需分頁 if (rb == RowBounds.DEFAULT) { return invocation.proceed(); }// 在SQL後加上limit語句 String sql = boundSql.getSql(); String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit()); sql = sql + " " + limit; // 自定義sqlSource SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings()); // 修改原來的sqlSource Field field = MappedStatement.class.getDeclaredField("sqlSource"); field.setAccessible(true); field.set(ms, sqlSource); // 執行被攔截方法 return invocation.proceed(); } // target 是被攔截對象,這個方法的做用是給被攔截對象生成一個代理對象,並返回它 @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } // 設置參數 @Override public void setProperties(Properties properties) { } }
插件註冊,在mybatis-config.xml 中註冊插件:
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="offsetAsPageNum" value="true"/> ……後面所有省略…… </plugin> </plugins>
攔截簽名跟參數的順序有嚴格要求,若是按照順序找不到對應方法會拋出異常:
org.apache.ibatis.exceptions.PersistenceException: ### Error opening session. Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query
MyBatis 啓動時掃描<plugins> 標籤, 註冊到Configuration 對象的 InterceptorChain 中。property 裏面的參數,會調用setProperties()方法處理。
上面提到的能夠被代理的四大對象都是何時被代理的呢?Executor 是openSession() 的時候建立的; StatementHandler 是SimpleExecutor.doQuery()建立的;裏面包含了處理參數的ParameterHandler 和處理結果集的ResultSetHandler 的建立,建立以後即調用InterceptorChain.pluginAll(),返回層層代理後的對象。代理是由Plugin 類建立。在咱們重寫的 plugin() 方法裏面能夠直接調用returnPlugin.wrap(target, this);返回代理對象。
當個插件的狀況下,代理能不能被代理?代理順序和調用順序的關係? 能夠被代理。
由於代理類是Plugin,因此最後調用的是Plugin 的invoke()方法。它先調用了定義的攔截器的intercept()方法。能夠經過invocation.proceed()調用到被代理對象被攔截的方法。
調用流程時序圖:
先來看一下分頁插件的簡單用法:
PageHelper.startPage(1, 3); List<Blog> blogs = blogMapper.selectBlogById2(blog); PageInfo page = new PageInfo(blogs, 3);
對於插件機制咱們上面已經介紹過了,在這裏咱們天然的會想到其所涉及的核心類 :PageInterceptor。攔截的是Executor 的兩個query()方法,要實現分頁插件的功能,確定是要對咱們寫的sql進行改寫,那麼必定是在 intercept 方法中進行操做的,咱們會發現這麼一行代碼:
String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
調用到 AbstractHelperDialect 中的 getPageSql 方法:
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
// 獲取sql String sql = boundSql.getSql();
//獲取分頁參數對象 Page page = this.getLocalPage(); return this.getPageSql(sql, page, pageKey); }
這裏能夠看到會去調用 this.getLocalPage(),咱們來看看這個方法:
public <T> Page<T> getLocalPage() { return PageHelper.getLocalPage(); } //線程獨享 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal(); public static <T> Page<T> getLocalPage() { return (Page)LOCAL_PAGE.get(); }
能夠發現這裏是調用的是PageHelper的一個本地線程變量中的一個 Page對象,從其中獲取咱們所設置的 PageSize 與 PageNum,那麼他是怎麼設置值的呢?請看:
PageHelper.startPage(1, 3); public static <E> Page<E> startPage(int pageNum, int pageSize) { return startPage(pageNum, pageSize, true); } public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } //設置頁數,行數信息 setLocalPage(page); return page; } protected static void setLocalPage(Page page) {
//設置值 LOCAL_PAGE.set(page); }
在咱們調用 PageHelper.startPage(1, 3); 的時候,系統會調用 LOCAL_PAGE.set(page) 進行設置,從而在分頁插件中能夠獲取到這個本地變量對象中的參數進行 SQL 的改寫,因爲改寫有不少實現,咱們這裏用的Mysql的實現:
在這裏咱們會發現分頁插件改寫SQL的核心代碼,這個代碼就很清晰了,沒必要過多贅述:
public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); if (page.getStartRow() == 0) { sqlBuilder.append(" LIMIT "); sqlBuilder.append(page.getPageSize()); } else { sqlBuilder.append(" LIMIT "); sqlBuilder.append(page.getStartRow()); sqlBuilder.append(","); sqlBuilder.append(page.getPageSize()); pageKey.update(page.getStartRow()); } pageKey.update(page.getPageSize()); return sqlBuilder.toString(); }
PageHelper 就是這麼一步一步的改寫了咱們的SQL 從而達到一個分頁的效果。
關鍵類總結: