動手實踐Mybatis插件


前言

  Mybatis的插件開發過程的前提是必須要對Mybatis整個SQL執行過程十分熟悉,這樣才能正確覆蓋源碼保證插件運行,總的來說Mybatis的插件式一種侵入式插件,使用時應該十分注意。

  在之前我的博文中已經介紹Mybatis的SqlSession運行原理本篇博文是在此知識基礎上學習記錄的,讀者可以先回顧再來看本博文。

  主要參數資料《深入淺出Myabtis基礎原理與實現》(PDF高清電子版,有需要的朋友可以評論/私信我)

 


 

一、插件開發前准備

插件開發前,我們需要知道簽名、插件接口、插件如何初始化、插件代理與反射、分離攔截對象常用工具類等

1、確定簽名

插件開發前,需要確定我們攔截的簽名,而簽名的確定需要以下的兩個因素

(1)確定攔截對象

Executor:調度以下三個對象並且執行SQL全過程,組裝參數、執行SQL、組裝結果集返回。通常不怎么攔截使用。

StatementHandler是執行SQL的過程(預處理語句構成),這里我們可以獲得SQL,重寫SQL執行。所以這是最常被攔截的對象。

ParameterHandler:參數組裝,可以攔截參數重組參數。

ResultSetHandler:結果集處理,可以重寫組裝結果集返回。

(2)攔截方法和參數

確定了攔截對象之后,需要確定攔截對象的方法與參數,比如攔截的是StatementHandler對象的關鍵預處理prepare(Connection connection, Integer transactionTimeout)方法。

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}

因此我們可以定義這樣的簽名:

//攔截StatementHandler對象的prepare預處理方法,同時指定該該方法的Connection參數
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})

@Intercepts說明是一個攔截器;

@Signature是注冊攔截器簽名的地方,只有滿足簽名條件才能攔截,type是四大對象中的一個。Method是指攔截的方法,args表示該方法參數。

2、插件接口

插件的開發第一步必須先實現Interceptor插件接口:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}
  • intercept方法:它是直接覆蓋你所攔截對象原有方法,因此它是插件的核心方法intercept里面有個參數Invoction(Invocation.getTarget()方法獲得攔截隊對象),通過它調用真正的對象方法(動態代理中經常使用)
  • plugin方法:target是被攔截對象,它的作用是給攔截對象生成一個代理對象,並返回它(使用Plugin.wrap(target,this)方法)當然也可以自己實現,但是需要特別小心。它實現InvoctionHandler接口,采用JDK動態代理。
  • setProperties方法:允許在mybatis-config.xml配置文件plugin元素中配置所需參數,方法在插件初始化的時候就被調用了一次,然后把插件對象存入到配置中,以便后續獲取。

以上其實就是模板(template)模式提供一個骨架,並告知骨架中的方法是用來做什么的。

3、插件初始化

插件初始化是在Mybatis初始化的時候完成,我們可以通過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.addInterceptor(interceptorInstance);
      }
    }
  }    

Mybatis上下文初始化過程中,就開始讀入插件節點和我們配置的參數,同時使用反射技術生成對應插件實例然后調用插件方法中的setProperties方法設置參數,然后將插件實例保存到配置對象中,以便讀取使用它。所以插件實例對象是一開始就被初始化的,而不是用到的時候才初始化。

4、插件的代理與反射設計

插件使用的是責任鏈模式(每一個在責任鏈上的角色都有機會去處理攔截對象)Mybatis中責任鏈是interceptorChain定義,比如執行器的生成

executor = (Executor)interceptorChain.pluginAll(executor);

pluginAll()方法的實現:

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  // 從interceptors中取出傳遞給plugin()方法,返回一個代理target
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  // interceptor實例存於interceptors這個List中(也就是上述所說的加入到Congfiguration對象中)   
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

pluginAll(Object target)方法:Configuration對象中取出的。從第一個對象到第四個對象(上述介紹過的四大對象)一次傳遞給plugin方法,然后返回一個代理target如果存在第二個插件,那么就拿到第一個代理對象,傳遞給plugin方法再返回第一個代理對象的代理.......依次類推。總之有多少個攔截器就有多少個代理對象。

addInterceptor(Interceptor interceptor)方法:將我們自定義的實現插件接口的interceptor實例存於interceptors這個List中(也就是上述所說的加入到Congfiguration對象中)

5、常用工具類MetaObject

它可以有效地讀取或者修改一些重要對象的屬性,在Mybatis中的四大對象提供的public設置參數方法很少,很難獲得相應的屬性,但是通過MetaObject工具類就可以讀取或修改這些屬性。常用的有三個方法:

  (1)MetaObject forObject(...)方法用來包裝對象,但是目前來說已經不再使用,而是使用SystemMetaObject.forObject(Object object)

  (2)Object getValue(String name)方法獲取對象屬性值,支持OGNL

  (3)void setValue(String name, Object value)方法修改對象屬性值,支持OGNL

Mybatis對象中大量使用這個類進行包裝,包括四大對象,使得我們可以通過它來給四大對象的某些屬性賦值從而滿足要求。

比如,攔截StatementHandler對象,我們先獲取要執行SQL修改它的值,這時候就使用MetaObject。在插件下修改運行參數如下:

     // 取出被攔截對象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        
        // 分離代理對象,從而形成多次代理
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }
        // 分離最后一個代理對象的目標類
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }
        
        // 取出即將執行的SQL
        String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
        String limitSql;
        
        // 判斷是否是MySQL數據庫且SQL沒有被重寫過
        if ("mysql".equals(this.dbType) && sql.indexOf(LIMIT_TABLE_NAME) == -1) {            
            sql = sql.trim();
            // 將參數寫入SQL生成:select*from(select*from table_name) temp_table_name limit N的形式
            limitSql = "select * from ("+sql+") " + LIMIT_TABLE_NAME + " limit " + limit;
            // 重寫要執行的SQL
            metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
        }

二、插件開發實例

 實際開發過程中我可能需要限制每次SQL返回的數據行數,限制的行數需要是一個可配置的參數,也去可以根據自己的需要配置。有如下的數據表,假設每次我只需要返回4條數據記錄!

注:以下的源代碼都是使用Mybatis的SqlSession運行原理中的代碼,有需要源碼的可以下方評論!

mysql> select * from test_table;
+----+----------+--------+
| id | name     | gender |
+----+----------+--------+
|  1 | Lijian   | M      |
|  2 | Zhangtao | F      |
|  3 | Zhangsan | M      |
|  4 | Lisi     | M      |
|  5 | Wangwu   | M      |
|  6 | Zhaoliu  | F      |
|  7 | Zhouqi   | F      |
|  8 | test     | M      |
+----+----------+--------+
8 rows in set

那么,可以通過以下簡單幾步實現插件實現(SQL攔截)

(1)確定需要攔截對象:限制返回條數肯定是先要攔截StatementHandler對象,在預編譯SQL之前,修改SQL返回數量

# Mapper中原始的SQL
select * from test_table
# 我們最后需要的SQL,也就插件最后執行的SQL
select * from (select * from test_table) temp_table_nmae limit 4

(2)攔截方法與參數:攔截預編譯,自然是要攔截StatementHandlerprepare()方法,prepare()方法傳入參數Connection對象與超時參數Integer類型。最后設計攔截器簽名如下:

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})

(3)實現攔截方法:

//攔截StatementHandler對象的prepare預處理方法,同時指定該該方法的Connection參數
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class QueryLimitPlugin implements Interceptor{
    
    // 默認限制查詢返回行數
    private int limit;
    // 數據庫類型
    private String dbType;
    // 為了防止表名不沖突,起一個特殊的中間表名
    private static final String LIMIT_TABLE_NAME = "limit_table_name_1";
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 取出被攔截對象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        
        // 分離代理對象,從而形成多次代理
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }
        // 分離最后一個代理對象的目標類
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }
        
        // 取出即將執行的SQL
        String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
        String limitSql;
        
        // 判斷是否是MySQL數據庫且SQL沒有被重寫過
        if ("mysql".equals(this.dbType) && sql.indexOf(LIMIT_TABLE_NAME) == -1) {
            sql = sql.trim();
            // 將參數寫入SQL生成:select*from(select*from table_name) temp_table_name limit N的形式
            limitSql = "select * from ("+sql+") " + LIMIT_TABLE_NAME + " limit " + limit;
            // 重寫要執行的SQL
            metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
        }
        // 調用原對象的方法,進入責任鏈的下一層
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 使用默認的Mybatis提供的類生成代理對象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 讀取設置的limit
        String strLimit = properties.getProperty("limit","4");
        this.limit = Integer.parseInt(strLimit);
        // 讀取設置的數據庫類型
        this.dbType = (String)properties.getProperty("dbType", "mysql");
    }

}

(4)配置與運行:

在mybatis-config.xml中:

<!-- 插件配置 -->
     <plugins>
         <plugin interceptor="com.lijian.mybatis.plugin.QueryLimitPlugin">
             <property name="dbType" value="mysql"/>
             <property name="limit" value="4"/>
         </plugin>
     </plugins>

在userMapper.xml配置<select>

<select id="listUsers" resultMap="userMap">
      select * from test_table
</select>

在UserMapper.java接口中編寫listUsers方法:

List<User> listUsers();

測試類:

public class MybatisMain2 {
    public static void main(String[] args) {
        SqlSession sqlSession = null;
        try {
            //獲得SqlSession
            sqlSession = SqlSessionFactoryUtils.openSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> users= userMapper.listUsers();
            users.forEach(user -> {
                System.out.println(user.toString());
            });
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
        finally {
            if (sqlSession != null) {
                //sqlSession生命周期是隨着SQL查詢而結束的
                sqlSession.close();
            }
        }
    }
}

查看日志打印結果:發現我們最初的SQL語句select  *from test_table變為select * from(select*from test_table) limit_table_name_1 limit 4,表示SQL已經被攔截修改執行

[DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.cache.decorators.LoggingCache] - Cache Hit Ratio [com.lijian.dao.UserMapper]: 0.0
[DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==>  Preparing: select * from (select * from test_table) limit_table_name_1 limit 4 
[DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - ==> Parameters: 
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <==    Columns: id, name, gender
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <==        Row: 1, Lijian, M
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <==        Row: 2, Zhangtao, F
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <==        Row: 3, Zhangsan, M
[TRACE][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <==        Row: 4, Lisi, M
[DEBUG][main][2018-08-21 13:41:09][org.apache.ibatis.logging.jdbc.BaseJdbcLogger] - <==      Total: 4

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM