前言
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)攔截方法與參數:攔截預編譯,自然是要攔截StatementHandler的prepare()方法,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