1、攔截器簡介
MyBatis提供了一種插件(plugin)的功能,但其實這是攔截器功能。基於這個攔截器我們可以選擇在這些被攔截的方法執行前后加上某些邏輯或者在執行這些被攔截的方法時執行自己的邏輯。
這點跟spring的攔截器是基本一致的。它的設計初衷就是為了供用戶在某些時候可以實現自己的邏輯而不必去動Mybatis固有的邏輯。
攔截器的使用中,分頁插件應該是使用得最多的了。分表的實現也差不多類似。
2、攔截的方法調用
MyBatis 允許你在已映射語句執行過程中的某一點進行攔截調用。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
總體概括為:
- 攔截執行器的方法
- 攔截參數的處理
- 攔截結果集的處理
- 攔截Sql語法構建的處理
我們看到了可以攔截Executor接口的部分方法,比如update,query,commit,rollback等方法,還有其他接口的一些方法等。
這4各方法在MyBatis的一個操作(新增,刪除,修改,查詢)中都會被執行到,執行的先后順序是Executor,ParameterHandler,ResultSetHandler,StatementHandler。undefine
3、Interceptor接口
了解到了攔截器能夠攔截的方法調用,就需要看看攔截接口是如何實現的了。
package
org.apache.ibatis.plugin;
import
java.util.Properties;
public
interface
Interceptor {
Object intercept(Invocation invocation)
throws
Throwable;
Object plugin(Object target);
void
setProperties(Properties properties);
}
|
接口中一共定義有三個方法,intercept、plugin 、setProperties。
- intercept方法就是要進行攔截的時候要執行的方法。
- setProperties方法是用於在Mybatis配置文件中指定一些屬性的。
- plugin方法是攔截器用於封裝目標對象的,通過該方法我們可以返回目標對象本身,也可以返回一個它的代理。當返回的是代理的時候我們可以對其中的方法進行攔截來調用intercept方法,當然也可以調用其他方法。
分表實現
1、大體思路
基於業務來看,我想要 按月 分表,因此數據庫表里增加了一個string類型字段 account_month 來記錄月份,分表字段就使用account_month。
分表表名:表名_年月 例如明細表:ebs_date_detail_201607。
分表是一月一張表,分表的建立就是默認建立了12個分表,如果超出了,后續再手工添加吧。也可以寫個腳本每月底創建下一個月的表,但是我覺得沒啥必要。就算哪天忘記添加了,代碼邏輯的異常處理流程里面也能夠保證我的數據不丟失,啟動一下異常數據處理也就妥妥的了。
在sql語句里面會要求帶上分表字段,通過分表字段計算得到分表的表名,然后替換掉原來的sql,直接將數據路由到指定的分表就行了。
聽起來好像很簡單的樣子,那么就這么出發吧。
2、問題目錄
分表開始之前的問題:
- Mybatis如何找到我們新增的攔截服務。
- 自定義的攔截服務應該在什么時間攔截查詢動作。即什么時間截斷Mybatis執行流。
- 自定義的攔截服務應該攔截什么樣的對象。不能攔截什么樣的對象。
- 自定義的攔截服務攔截的對象應該具有什么動作才能被攔截。
- 自定義的攔截服務如何獲取上下文中傳入的參數信息。
- 如何把簡單查詢,神不知鬼不覺的,無侵入性的替換為分表查詢語句。
- 最后,攔截器應該如何交還被截斷的Mybatis執行流。
帶着這些問題,我們來看看我們自定義的攔截服務是如何實現的。
3、逐步實現
3.1 Mybatis如何找到我們新增的攔截服務
對於攔截器Mybatis為我們提供了一個Interceptor接口,前面有提到,通過實現該接口就可以定義我們自己的攔截器。自定義的攔截器需要交給Mybatis管理,這樣才能使得Mybatis的執行與攔截器的執行結合在一起,即,攔截器需要注冊到mybatis-config配置文件中。
通過在Mybatis配置文件中plugins元素下的plugin元素來進行。一個plugin對應着一個攔截器,在plugin元素下面我們可以指定若干個property子元素。Mybatis在注冊定義的攔截器時會先把對應攔截器下面的所有property通過Interceptor的setProperties方法注入給對應的攔截器。
配置文件:mybatis-config.xml
<configuration> <plugins> <plugin interceptor="com.selicoco.sango.common.database.paginator.interceptor.ShardTableInterceptor"> </plugin> </plugins> </configuration>
3.2 什么時間截斷Mybatis執行流
Mybatis允許我們能夠進行切入的點:
-
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
-
ParameterHandler (getParameterObject, setParameters)
-
ResultSetHandler (handleResultSets, handleOutputParameters)
-
StatementHandler (prepare, parameterize, batch, update, query)
因為我是想要通過替換原來SQL中的表名來實現分表,包括查詢,新增,刪除等操作,所以攔截的合理時機選在StatementHandler中prepare。
執行流在PreparedStatementHandler.instantiateStatement()方法中 return connection.prepareStatement(sql); 最終真正的執行了語句。
所以攔截器的注解內容:
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
3.3 應該攔截什么樣的對象
並不是所有的表都進行了分表,也不是所有的表都需要攔截處理。所以我們要根據某些配置來確定哪些需要被處理。
這里主要使用注解的方式,設置了對應的參數。
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface TableSeg { //表名 public String tableName(); // 分表方式,取模,如%5:表示取5余數, // 按時間,如MONTH:表示按月分表 // 如果不設置,直接根據shardBy值分表 public String shardType(); //根據什么字段分表 ,多個字段用數學表達表示,如a+b a-b public String shardBy(); // 根據什么字段分表,多個字段用數學表達表示,如a+b a-b public String shardByTable(); }
注解完成后,在mapper上去配置。如果是自定義的查詢語句和返回,沒有對應的mapper文件,那么在對應的dao 上進行配置就可以了。
@TableSeg(tableName="ebs_date_detail",shardType="MONTH",shardBy="accountMonth",shardByTable="account_month") public interface EbsDataDetailMapper {}
@Repository @TableSeg(tableName="ebs_date_detail",shardType="MONTH",shardBy="accountMonth",shardByTable="account_month") public class EbsDataDetailDao {}
3.4 如何獲取上下文中傳入的參數
關於這個問題,我覺得我有很大的發言權,真的是摸着石頭過來的。
天真的以為,只要拿到要執行之前已經組裝好的語句,然后用我的分表表名替換一下原表名就可以了。當然其實也差不多就是這樣子的。不過實際情況還是有點坎坷的。
首先,如何拿到執行前已經組裝好的語句。分兩種情況來說,查詢和更新。
不說話先看圖:
新增數據的時候,我們從boundSql里面的additionalParameters 里面能輕松拿到注解上面 shardBy="accountMonth"所對應的參數值。然后根據參數來生成分表語句,一切順利。
如此簡單,覺得自己好機智。開心的去碼后面的代碼了,等到單測的時候執行查詢,然后就報錯啦。只能Debug看看。
沒有想到,都是mybatis的動態sql,結果參數方式竟然不同,想來也只能自己去取參數了。參數在哪里?看圖
具體的就看后面實現代碼吧,反正就是通過兩種方式取到我們要的分表字段的參數值,這樣才能求得分表表名。
3.5 真正實現分表查詢語句
攔截器主要的作用是讀取配置,根據配置的切分策略和字段,來切分表,然后替換原執行的SQL,從而實現自動切分。
String accountMonth = genShardByValue(metaStatementHandler, mappedStatement ,tableSeg, boundSql); String newSql = boundSql.getSql().replace(tableSeg.tableName(), tableSeg.tableName() + "_" + accountMonth); if (newSql != null) { logger.debug(tag, "分表后SQL =====>" + newSql); metaStatementHandler.setValue("delegate.boundSql.sql", newSql); }
3.6 交還被截斷的Mybatis執行流
把原有的簡單查詢語句替換為分表查詢語句了,現在是時候將程序的控制權交還給Mybatis了
// 傳遞給下一個攔截器處理 return invocation.proceed();
4 實現源碼
4.1 配置文件
見本文: 3.1 Mybatis如何找到我們新增的攔截服務 -- mybatis-config.xml
4.2 分表配置注解
分表注解定義、mapper注解配置、DAO注解配置
見本文: 3.3 應該攔截什么樣的對象
4.3 分表實現
分表具體實現
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) }) public class ShardTableInterceptor implements Interceptor { private final static Logger logger = LoggerFactory.getLogger(ShardTableInterceptor.class); private static final String tag = ShardTableInterceptor.class.getName(); @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaStatementHandler = MetaObject.forObject(statementHandler); MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement"); BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql"); String sqlId = mappedStatement.getId(); String className = sqlId.substring(0, sqlId.lastIndexOf(".")); Class<?> classObj = Class.forName(className); TableSeg tableSeg = classObj.getAnnotation(TableSeg.class); if(null == tableSeg){ //不需要分表,直接傳遞給下一個攔截器處理 return invocation.proceed(); } //根據配置獲取分表字段,生成分表SQL String accountMonth = genShardByValue(metaStatementHandler, mappedStatement ,tableSeg, boundSql); String newSql = boundSql.getSql().replace(tableSeg.tableName(), tableSeg.tableName() + "_" + accountMonth); if (newSql != null) { logger.debug(tag, "分表后SQL =====>" + newSql); metaStatementHandler.setValue("delegate.boundSql.sql", newSql); } // 傳遞給下一個攔截器處理 return invocation.proceed(); } @Override public Object plugin(Object target) { // 當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標本身,減少目標被代理的次數 if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } else { return target; } } @Override public void setProperties(Properties properties) { logger.info("scribeDbNames:" + properties.getProperty("scribeDbNames")); } //根據配置獲取分表的表名后綴 private String genShardByValue(MetaObject metaStatementHandler,MappedStatement mappedStatement, TableSeg tableSeg, BoundSql boundSql) { String accountMonth = null; Map<String, Object> additionalParameters = (Map<String, Object>) metaStatementHandler.getValue("delegate.boundSql.additionalParameters"); if (null != additionalParameters.get(tableSeg.shardBy())) { accountMonth = boundSql.getAdditionalParameter(tableSeg.shardBy()).toString(); } else { Configuration configuration = mappedStatement.getConfiguration(); String showSql = showSql(configuration,boundSql); accountMonth = getShardByValue(showSql,tableSeg); } return accountMonth; } //根據配置獲取分表參數值 public static String getShardByValue(String showSql,TableSeg tableSeg) { final String conditionWhere = "where"; String accountMonth = null ; if(StringUtils.isBlank(showSql)){ return null; }else{ String[] sqlSplit = showSql.toLowerCase().split(conditionWhere); if(sqlSplit.length>1 && sqlSplit[1].contains(tableSeg.shardByTable())){ accountMonth = sqlSplit[1].replace(" ","").split(tableSeg.shardByTable())[1].substring(2,8); } } return accountMonth; } //組裝查詢語句參數 public static String showSql(Configuration configuration, BoundSql boundSql) { Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); String sql = boundSql.getSql().replaceAll("[\\s]+", " "); if (parameterMappings.size() > 0 && parameterObject != null) { TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { sql = sql.replaceFirst("\\?", getParameterValue(parameterObject)); } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); if (metaObject.hasGetter(propertyName)) { Object obj = metaObject.getValue(propertyName); sql = sql.replaceFirst("\\?", getParameterValue(obj)); } else if (boundSql.hasAdditionalParameter(propertyName)) { Object obj = boundSql.getAdditionalParameter(propertyName); sql = sql.replaceFirst("\\?", getParameterValue(obj)); } } } }else{ return null; } return sql; } private static String getParameterValue(Object obj) { String value = null; if (obj instanceof String) { value = "'" + obj.toString() + "'"; } else if (obj instanceof Date) { DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA); value = "'" + formatter.format(new Date()) + "'"; } else { if (obj != null) { value = obj.toString(); } else { value = ""; } } return value; } }