Mybatis之Plugin使用及原理

近期一個業(yè)務(wù)需要配置禁寫袁辈,想通過Mybatis的Plugin來做妙黍,于是有了這篇文章碾篡。先來看官網(wǎng)對Plugin的介紹(以下內(nèi)容來自官網(wǎng))

// MyBatis 允許你在映射語句執(zhí)行過程中的某一點(diǎn)進(jìn)行攔截調(diào)用砍的。默認(rèn)情況下驼卖,MyBatis 允許使用插件來攔截的方法調(diào)用包括:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

也就是說氨肌,在mybatis的執(zhí)行流程中,支持Plugin的場景有且僅有:Executor酌畜、ParameterHandler怎囚、StatementHandler以及ResultSetHandler。針對我們的需求桥胞,剛開始打算直接從Executor入手恳守,實(shí)現(xiàn)禁寫并保存sql,下面是處理類

@Intercepts({
            // 注意贩虾,這里的type只能是接口催烘,否則不會生效,具體原因參考下面的源碼
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class ForbidInterceptor implements Interceptor {

    private Properties properties;
    private static final Logger LOGGER = LoggerFactory.getLogger(ForbidInterceptor.class);
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Executor executor = (Executor)invocation.getTarget();
        MappedStatement statement = (MappedStatement)invocation.getArgs()[0];
        Object paramObject = invocation.getArgs()[1];
        Configuration configuration = statement.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(executor, statement, paramObject, RowBounds.DEFAULT, null, null);
      
        // 簡單期間缎罢,拿到PreparedStatement颗圣,有點(diǎn)多此一舉,不如直接切PreparedStatmentHandler
        PreparedStatement preparedStatement = (PreparedStatement) prepareStatement(handler,executor.getTransaction().getConnection(),executor.getTransaction());
        String rawSql = preparedStatement.toString();
        int updateIndex = rawSql.indexOf("update") == -1 ? rawSql.indexOf("insert") : -1;
        if(updateIndex == -1 ){
            return invocation.proceed();
        }
      
        //業(yè)務(wù)邏輯省略屁使,這里僅打印執(zhí)行sql在岂,然后直接返回1,并不會真實(shí)執(zhí)行sql  
        rawSql = rawSql.substring(updateIndex);
        LOGGER.info("row sql : " + rawSql);
        return (Object)1;
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
        
    // 這里參照mybatis對preparedStatement的處理
    private Statement prepareStatement(StatementHandler handler, Connection connection, Transaction transaction) throws SQLException {
        Statement stmt;
        stmt = handler.prepare(connection, transaction.getTimeout());
        handler.parameterize(stmt);
        return stmt;
    }

雖然可以實(shí)現(xiàn)禁寫的目的蛮寂,但是就像代碼中注釋的那樣蔽午,有點(diǎn)多此一舉;所以酬蹋,直接來切PreparedStatementHandler,下面是處理類

@Intercepts({
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class})
})
public class ForbidInterceptorForMmc implements Interceptor {

    private Properties properties;
    private static final Logger LOGGER = LoggerFactory.getLogger(ForbidInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        PreparedStatement ps = (PreparedStatement)invocation.getArgs()[0];
        if(Objects.nonNull(ps)){
            // 省略業(yè)務(wù)邏輯及老,僅打印sql
            String rawSql = ps.toString();
            int updateIndex = rawSql.indexOf("update") == -1 ? rawSql.indexOf("insert") : -1;
            // 非更新語句抽莱,直接執(zhí)行
            if(updateIndex == -1 ){
                return invocation.proceed();
            }
            rawSql = rawSql.substring(updateIndex);
            LOGGER.info("row sql : " + rawSql);
        }
        return 1;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

結(jié)果也比較簡單,可以看到具體待執(zhí)行的sql(實(shí)際業(yè)務(wù)場景可能需要保存sql):

c.s.i.t.interceptor.ForbidInterceptor    : row sql : 
insert into employees (birth_date, first_name, last_name,gender, hire_date)
                    values ('2021-06-01 17:08:58', 'zhang', 'san',1, '2021-06-01 17:08:58')

基本功能實(shí)現(xiàn)了骄恶,來都來了食铐,順道看看Plugin的實(shí)現(xiàn)吧。

Mybatis中Plugin流程主要包括Plugin僧鲁、Interceptor虐呻、InterceptorChain三個核心類;其中Interceptor定義插件實(shí)際邏輯寞秃,然后Plugin使用JDK的動態(tài)代理對targetObject進(jìn)行代理(通常是Executor斟叼、PremeterHandler、StatementHandler春寿、ResultSetHandler的實(shí)現(xiàn))朗涩,最后在調(diào)用被代理對象方法時調(diào)用invoke(),執(zhí)行Interceptor邏輯绑改,所以可以把Plugin看作是wrapper+proxy谢床。InterceptorChain負(fù)責(zé)Interceptor的管理。使用Plugin通常需要實(shí)現(xiàn)org.apache.ibatis.plugin.Interceptor厘线,并重寫intercept()和plugin() (通常是Plugin.wrap(targe,this))萤悴。整個Plugin的執(zhí)行周期主要包括裝配和執(zhí)行兩部分,如下圖所示:

mybatis-plugin.jpg

先來看Plugin的裝配皆的,可以分為兩步:將Interceptor加入InterceptorChain覆履、對targetObject進(jìn)行代理。InterceptorChain負(fù)責(zé)裝配Plugin费薄,裝配的總?cè)肟谠贑onfiguration硝全,先來看第一步

public class InterceptorChain {
    // 全局Plugin
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
    
  // 組裝所有plugin,最終結(jié)果是target的代理
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      // 核心楞抡,為target生成proxy伟众,同時封裝interceptor執(zhí)行邏輯
      target = interceptor.plugin(target);
    }
    return target;
  }
    
  // 添加Interceptor
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

XMLConfigBuilder(解析mybatis-config.xml)和Configuration都有Plugin裝配的入口,XMLConfigBuilder最終也是通過Configuration實(shí)現(xiàn)召廷。先來看XMLConfigBuilder

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);
      // 借助configuration實(shí)現(xiàn)plugin裝配
      configuration.addInterceptor(interceptorInstance);
    }
  }
}
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

下面這段Configuration中的邏輯可以解釋官網(wǎng)中對攔截點(diǎn)的說明(僅支持ParameterHandler凳厢、ResultSetHandler、StatementHandler竞慢、Executor

// 創(chuàng)建ParameterHandler時先紫,為ParameterHandler裝配Plugins
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,                   parameterObject, boundSql);
    // 為ParameterHandler裝配Plugins
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

// 創(chuàng)建ResultSetHandler時,為ResultSetHandler裝配Plugins
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裝配Plugins
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

// 創(chuàng)建StatementHandler時筹煮,為StatementHandler裝配Plugins
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裝配Plugins
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

// 創(chuàng)建Executor時遮精,為Executor裝配Plugins
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) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 為Executor裝配Plugins
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

此時所有Interceptor已經(jīng)全部加入到InterceptorChain,接著,對targetObject進(jìn)行層層代理本冲,核心邏輯如下:

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      // 核心准脂,為target生成proxy,同時封裝interceptor執(zhí)行邏輯
      target = interceptor.plugin(target);
    }
    return target;
}

// 下面是Plugin類的核心邏輯
public static Object wrap(Object target, Interceptor interceptor) {
    // @Intercepts注解處理
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 被代理類真實(shí)類型
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 生成接口的動態(tài)代理對象檬洞,注意這里只能是接口
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
}

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());      
    }
    // 獲取注解內(nèi)Signature注解內(nèi)容至signatureMap狸膏,格式為<Class(攔截的接口),Set<Method>(攔截的接口方法集合)>,
    Signature[] sigs = interceptsAnnotation.value();
    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()) {
        // 接口在signatureMap,則會生成代理類
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}

Plugin的執(zhí)行相對比較簡單添怔,因?yàn)镻lugin實(shí)現(xiàn)了JDK的InvocationHandler接口(invoke方法)湾戳,調(diào)用被代理類對象方法,會執(zhí)行Plugin的invoke()澎灸,邏輯如下;

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    // 當(dāng)前方法在被代理方法集合內(nèi)遮晚,執(zhí)行intercept邏輯
    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);
  }
}

這就是Mybatis中對Plugin的實(shí)現(xiàn)性昭,核心是通過JDK的動態(tài)代理對target進(jìn)行代理,并利用責(zé)任鏈模式對Plugin進(jìn)行裝配县遣。最后糜颠,歡迎指正。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末萧求,一起剝皮案震驚了整個濱河市其兴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌夸政,老刑警劉巖元旬,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異守问,居然都是意外死亡匀归,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門耗帕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來穆端,“玉大人,你說我怎么就攤上這事仿便√鍐” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵嗽仪,是天一觀的道長荒勇。 經(jīng)常有香客問我,道長闻坚,這世上最難降的妖魔是什么枕屉? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮鲤氢,結(jié)果婚禮上搀擂,老公的妹妹穿的比我還像新娘西潘。我一直安慰自己,他們只是感情好哨颂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布喷市。 她就那樣靜靜地躺著,像睡著了一般威恼。 火紅的嫁衣襯著肌膚如雪品姓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天箫措,我揣著相機(jī)與錄音腹备,去河邊找鬼。 笑死斤蔓,一個胖子當(dāng)著我的面吹牛植酥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弦牡,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼友驮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驾锰?” 一聲冷哼從身側(cè)響起卸留,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎椭豫,沒想到半個月后耻瑟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赏酥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年匆赃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片今缚。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡算柳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出姓言,到底是詐尸還是另有隱情瞬项,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布何荚,位于F島的核電站囱淋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏餐塘。R本人自食惡果不足惜妥衣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧税手,春花似錦蜂筹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至兵扬,卻和暖如春麻裳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背器钟。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工津坑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人傲霸。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓疆瑰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親狞谱。 傳聞我的和親對象是個殘疾皇子乃摹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容