Mybatis自定義攔截器與插件開發(fā)
點擊關注公眾號,Java干貨及時送達??

在Spring中我們經(jīng)常會使用到攔截器,在登錄驗證、日志記錄、性能監(jiān)控等場景中,通過使用攔截器允許我們在不改動業(yè)務代碼的情況下,執(zhí)行攔截器的方法來增強現(xiàn)有的邏輯。在mybatis中,同樣也有這樣的業(yè)務場景,有時候需要我們在不侵入原有業(yè)務代碼的情況下攔截sql,執(zhí)行特定的某些邏輯。那么這個過程應該怎么實現(xiàn)呢,同樣,在mybatis中也為開發(fā)者預留了攔截器接口,通過實現(xiàn)自定義攔截器這一功能,可以實現(xiàn)我們自己的插件,允許用戶在不改動mybatis的原有邏輯的條件下,實現(xiàn)自己的邏輯擴展。
本文將按下面的結構進行mybatis攔截器學習:
本文結構
1、攔截器核心對象
2、工作流程
3、攔截器能實現(xiàn)什么
4、插件定義與注冊
5、攔截器使用示例
6、總結
攔截器核心對象
在實現(xiàn)攔截器之前,我們首先看一下攔截器的攔截目標對象是什么,以及攔截器的工作流程是怎樣的。mybatis攔截器可以對下面4種對象進行攔截:
1、Executor:mybatis的內(nèi)部執(zhí)行器,作為調(diào)度核心負責調(diào)用StatementHandler操作數(shù)據(jù)庫,并把結果集通過ResultSetHandler進行自動映射
2、StatementHandler: 封裝了JDBC Statement操作,是sql語法的構建器,負責和數(shù)據(jù)庫進行交互執(zhí)行sql語句
3、ParameterHandler:作為處理sql參數(shù)設置的對象,主要實現(xiàn)讀取參數(shù)和對PreparedStatement的參數(shù)進行賦值
4、ResultSetHandler:處理Statement執(zhí)行完成后返回結果集的接口對象,mybatis通過它把ResultSet集合映射成實體對象
工作流程
在mybatis中提供了一個Interceptor接口,通過實現(xiàn)該接口就能夠自定義攔截器,接口中定義了3個方法:
public?interface?Interceptor?{
??Object?intercept(Invocation?invocation)?throws?Throwable;
??default?Object?plugin(Object?target)?{
????return?Plugin.wrap(target,?this);
??}
??default?void?setProperties(Properties?properties)?{
????//?NOP
??}
}
intercept:在攔截目標對象的方法時,實際執(zhí)行的增強邏輯,我們一般在該方法中實現(xiàn)自定義邏輯plugin:用于返回原生目標對象或它的代理對象,當返回的是代理對象的時候,會調(diào)用intercept方法setProperties:可以用于讀取配置文件中通過property標簽配置的一些屬性,設置一些屬性變量
看一下plugin方法中的wrap方法源碼:
public?static?Object?wrap(Object?target,?Interceptor?interceptor)?{
??Map,?Set>?signatureMap?=?getSignatureMap(interceptor);
??Class>?type?=?target.getClass();
??Class>[]?interfaces?=?getAllInterfaces(type,?signatureMap);
??if?(interfaces.length?>?0)?{
????return?Proxy.newProxyInstance(
????????type.getClassLoader(),
????????interfaces,
????????new?Plugin(target,?interceptor,?signatureMap));
??}
??return?target;
}
可以看到,在wrap方法中,通過使用jdk動態(tài)代理的方式,生成了目標對象的代理對象,在執(zhí)行實際方法前,先執(zhí)行代理對象中的邏輯,來實現(xiàn)的邏輯增強。以攔截Executor的query方法為例,在實際執(zhí)行前會執(zhí)行攔截器中的intercept方法:

在mybatis中,不同類型的攔截器按照下面的順序執(zhí)行:
Executor -> StatementHandler -> ParameterHandler -> ResultSetHandler
以執(zhí)行query 方法為例對流程進行梳理,整體流程如下:
1、Executor執(zhí)行query()方法,創(chuàng)建一個StatementHandler對象
2、StatementHandler 調(diào)用ParameterHandler對象的setParameters()方法
3、StatementHandler 調(diào)用 Statement對象的execute()方法
4、StatementHandler 調(diào)用ResultSetHandler對象的handleResultSets()方法,返回最終結果
攔截器能實現(xiàn)什么
在對mybatis攔截器有了初步的認識后,來看一下攔截器被普遍應用在哪些方面:
sql 語句執(zhí)行監(jiān)控
可以攔截執(zhí)行的sql方法,可以打印執(zhí)行的sql語句、參數(shù)等信息,并且還能夠記錄執(zhí)行的總耗時,可供后期的sql分析時使用
sql 分頁查詢
mybatis中使用的
RowBounds使用的內(nèi)存分頁,在分頁前會查詢所有符合條件的數(shù)據(jù),在數(shù)據(jù)量大的情況下性能較差。通過攔截器,可以做到在查詢前修改sql語句,提前加上需要的分頁參數(shù)公共字段的賦值
在數(shù)據(jù)庫中通常會有
createTime,updateTime等公共字段,這類字段可以通過攔截統(tǒng)一對參數(shù)進行的賦值,從而省去手工通過set方法賦值的繁瑣過程數(shù)據(jù)權限過濾
在很多系統(tǒng)中,不同的用戶可能擁有不同的數(shù)據(jù)訪問權限,例如在多租戶的系統(tǒng)中,要做到租戶間的數(shù)據(jù)隔離,每個租戶只能訪問到自己的數(shù)據(jù),通過攔截器改寫sql語句及參數(shù),能夠實現(xiàn)對數(shù)據(jù)的自動過濾
除此之外,攔截器通過對上述的4個階段的介入,結合我們的實際業(yè)務場景,還能夠實現(xiàn)很多其他功能。
插件定義與注冊
在我們自定義的攔截器類實現(xiàn)了Interceptor接口后,還需要在類上添加@Intercepts 注解,標識該類是一個攔截器類。注解中的內(nèi)容是一個@Signature對象的數(shù)組,指明自定義攔截器要攔截哪一個類型的哪一個具體方法。其中type指明攔截對象的類型,method是攔截的方法,args是method執(zhí)行的參數(shù)。通過這里可以了解到 mybatis 攔截器的作用目標是在方法級別上進行攔截,例如要攔截Executor的query方法,就在類上添加:
@Intercepts({
????????@Signature(type?=?Executor.class,method?=?"query",?args?=?{?MappedStatement.class,?Object.class,
????????????????RowBounds.class,?ResultHandler.class?})
})
如果要攔截多個方法,可以繼續(xù)以數(shù)組的形式往后追加。這里通過添加參數(shù)可以確定唯一的攔截方法,例如在Executor中存在兩個query方法,通過上面的參數(shù)可以確定要攔截的是下面的第2個方法:
?List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?cacheKey,?BoundSql?boundSql) ;
?List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler) ;
當編寫完成我們自己的插件后,需要向mybatis中注冊插件,有兩種方式可以使用,第一種直接在SqlSessionFactory中配置:
@Bean
public?SqlSessionFactory?sqlSessionFactory(DataSource?dataSource)?throws?Exception?{
????SqlSessionFactoryBean?sqlSessionFactoryBean?=?new?SqlSessionFactoryBean();
????sqlSessionFactoryBean.setDataSource(dataSource);
????sqlSessionFactoryBean.setPlugins(new?Interceptor[]{new?ExecutorPlugin()});
????return?sqlSessionFactoryBean.getObject();
}
第2種是在mybatis-config.xml中對自定義插件進行注冊:
<configuration>
????<plugins>
????????<plugin?interceptor="com.cn.plugin.interceptor.MyPlugin">
?????????<property?name="text"?value="hello"/>
????????plugin>
????????<plugin?interceptor="com.cn.plugin.interceptor.MyPlugin2">plugin>
????????<plugin?interceptor="com.cn.plugin.interceptor.MyPlugin3">plugin>
????plugins>
configuration>
在前面我們了解了不同類型攔截器執(zhí)行的固定順序,那么對于同樣類型的多個自定義攔截器,它們的執(zhí)行順序是怎樣的呢?分別在plugin方法和intercept中添加輸出語句,運行結果如下:

從結果可以看到,攔截順序是按照注冊順序執(zhí)行的,但代理邏輯的執(zhí)行順序正好相反,最后注冊的會被最先執(zhí)行。這是因為在mybatis中有一個類InterceptorChain,在它的pluginAll()方法中,會對原生對象target進行代理,如果有多個攔截器的話,會對代理類再次進行代理,最終實現(xiàn)一層層的增強target對象,因此靠后被注冊的攔截器的增強邏輯會被優(yōu)先執(zhí)行。從下面的圖中可以直觀的看出代理的嵌套關系:

在xml中注冊完成后,在application.yml中啟用配置文件,這樣插件就可以正常運行了:
mybatis:
??config-location:?classpath:mybatis-config.xml
在了解了插件的基礎概念與運行流程之后,通過代碼看一下應用不同的攔截器能夠實現(xiàn)什么功能。
攔截器使用示例
Executor
通過攔截Executor的query和update方法實現(xiàn)對sql的監(jiān)控,在攔截方法中,打印sql語句、執(zhí)行參數(shù)、實際執(zhí)行時間:
@Intercepts({
????????@Signature(type?=?Executor.class,method?=?"update",?args?=?{MappedStatement.class,?Object.class}),
????????@Signature(type?=?Executor.class,method?=?"query",?args?=?{?MappedStatement.class,?Object.class,
????????????????RowBounds.class,?ResultHandler.class?})})
public?class?ExecutorPlugin?implements?Interceptor?{
????@Override
????public?Object?intercept(Invocation?invocation)?throws?Throwable?{
????????System.out.println("Executor?Plugin?攔截?:"+invocation.getMethod());
????????Object[]?queryArgs?=?invocation.getArgs();
????????MappedStatement?mappedStatement?=?(MappedStatement)?queryArgs[0];
????????//獲取?ParamMap
????????MapperMethod.ParamMap?paramMap?=?(MapperMethod.ParamMap)?queryArgs[1];
????????//?獲取SQL
????????BoundSql?boundSql?=?mappedStatement.getBoundSql(paramMap);
????????String?sql?=?boundSql.getSql();
????????log.info("==>?ORIGIN?SQL:?"+sql);
????????long?startTime?=?System.currentTimeMillis();
????????Configuration?configuration?=?mappedStatement.getConfiguration();
????????String?sqlId?=?mappedStatement.getId();
????????Object?proceed?=?invocation.proceed();
????????long?endTime=System.currentTimeMillis();
????????long?time?=?endTime?-?startTime;
????????printSqlLog(configuration,boundSql,sqlId,time);
????????return?proceed;
????}
????public?static?void?printSqlLog(Configuration?configuration,?BoundSql?boundSql,?String?sqlId,?long?time){
????????Object?parameterObject?=?boundSql.getParameterObject();
????????List?parameterMappings?=?boundSql.getParameterMappings();
????????String?sql=?boundSql.getSql().replaceAll("[\\s]+",?"?");
????????StringBuffer?sb=new?StringBuffer("==>?PARAM:");
????????if?(parameterMappings.size()>0?&&?parameterObject!=null){
????????????TypeHandlerRegistry?typeHandlerRegistry?=?configuration.getTypeHandlerRegistry();
????????????if?(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass()))?{
????????????????sql?=?sql.replaceFirst("\\?",?parameterObject.toString());
????????????}?else?{
????????????????MetaObject?metaObject?=?configuration.newMetaObject(parameterObject);
????????????????for?(ParameterMapping?parameterMapping?:?parameterMappings)?{
????????????????????String?propertyName?=?parameterMapping.getProperty();
????????????????????if?(metaObject.hasGetter(propertyName))?{
????????????????????????Object?obj?=?metaObject.getValue(propertyName);
????????????????????????String?parameterValue?=?obj.toString();
????????????????????????sql?=?sql.replaceFirst("\\?",?parameterValue);
????????????????????????sb.append(parameterValue).append("(").append(obj.getClass().getSimpleName()).append("),");
????????????????????}?else?if?(boundSql.hasAdditionalParameter(propertyName))?{
????????????????????????Object?obj?=?boundSql.getAdditionalParameter(propertyName);
????????????????????????String?parameterValue?=?obj.toString();
????????????????????????sql?=?sql.replaceFirst("\\?",?parameterValue);
????????????????????????sb.append(parameterValue).append("(").append(obj.getClass().getSimpleName()).append("),");
????????????????????}
????????????????}
????????????}
????????????sb.deleteCharAt(sb.length()-1);
????????}
????????log.info("==>?SQL:"+sql);
????????log.info(sb.toString());
????????log.info("==>?SQL?TIME:"+time+"?ms");
????}
}
執(zhí)行代碼,日志輸出如下:

在上面的代碼中,通過Executor攔截器獲取到了BoundSql對象,進一步獲取到sql的執(zhí)行參數(shù),從而實現(xiàn)了對sql執(zhí)行的監(jiān)控與統(tǒng)計。
StatementHandler
下面的例子中,通過改變StatementHandler對象的屬性,動態(tài)修改sql語句的分頁:
@Intercepts({
????????@Signature(type?=?StatementHandler.class,?method?=?"prepare",?args?=?{Connection.class,?Integer.class})})
public?class?StatementPlugin?implements?Interceptor?{
????@Override
????public?Object?intercept(Invocation?invocation)?throws?Throwable?{????????
????????StatementHandler?statementHandler?=?(StatementHandler)?invocation.getTarget();
????????MetaObject?metaObject?=?SystemMetaObject.forObject(statementHandler);????????????
????????metaObject.setValue("delegate.rowBounds.offset",?0);
????????metaObject.setValue("delegate.rowBounds.limit",?2);
????????return?invocation.proceed();
????}
}
MetaObject是mybatis提供的一個用于方便、優(yōu)雅訪問對象屬性的對象,通過將實例對象作為參數(shù)傳遞給它,就可以通過屬性名稱獲取對應的屬性值。雖然說我們也可以通過反射拿到屬性的值,但是反射過程中需要對各種異常做出處理,會使代碼中堆滿難看的try/catch,通過MetaObject可以在很大程度上簡化我們的代碼,并且它支持對Bean、Collection、Map三種類型對象的操作。
對比執(zhí)行前后:

可以看到這里通過改變了分頁對象RowBounds的屬性,動態(tài)的修改了分頁參數(shù)。
ResultSetHandler
ResultSetHandler 會負責映射sql語句查詢得到的結果集,如果在生產(chǎn)環(huán)境中存在一些保密數(shù)據(jù),不想在外部系統(tǒng)中展示,那么可能就需要在查詢到結果后做一下數(shù)據(jù)的脫敏處理,這時候就可以使用ResultSetHandler對結果集進行改寫。
@Intercepts({
????????@Signature(type=?ResultSetHandler.class,method?=?"handleResultSets",args?=?{Statement.class})})
public?class?ResultSetPlugin?implements?Interceptor?{
????@Override
????public?Object?intercept(Invocation?invocation)?throws?Throwable?{
????????System.out.println("Result?Plugin?攔截?:"+invocation.getMethod());
????????Object?result?=?invocation.proceed();
????????if?(result?instanceof?Collection)?{
????????????Collection運行上面的代碼,查看執(zhí)行結果:
{"id":1358041517788299266,"orderNumber":"***","money":122.0,"status":3,"tenantId":2}
在上面的例子中,在執(zhí)行完sql語句得到結果對象后,通過反射掃描結果對象中的屬性,如果實體的屬性上帶有自定義的@Confidential注解,那么在脫敏方法中將它轉化為星號再返回結果,從而實現(xiàn)了數(shù)據(jù)的脫敏處理。
ParameterHandler
mybatis可以攔截ParameterHandler注入?yún)?shù),下面的例子中我們將結合前面介紹的其他種類的對象,通過組合攔截器的方式,實現(xiàn)一個簡單的多租戶攔截器插件,實現(xiàn)多租戶下的查詢邏輯。
@Intercepts({
????????@Signature(type?=?Executor.class,method?=?"query",?args?=?{?MappedStatement.class,?Object.class,RowBounds.class,?ResultHandler.class?}),
????????@Signature(type?=?StatementHandler.class,?method?=?"prepare",?args?=?{Connection.class,?Integer.class}),
????????@Signature(type?=?ParameterHandler.class,?method?=?"setParameters",?args?=?PreparedStatement.class),
})
public?class?TenantPlugin?implements?Interceptor?{
????private?static?final?String?TENANT_ID?=?"tenantId";
????@Override
????public?Object?intercept(Invocation?invocation)?throws?Throwable?{
????????Object?target?=?invocation.getTarget();
????????String?methodName?=?invocation.getMethod().getName();
????????if?(target?instanceof?Executor?&&??methodName.equals("query")?&&?invocation.getArgs().length==4)?{
????????????return?doQuery(invocation);
????????}
????????if?(target?instanceof?StatementHandler){
????????????return?changeBoundSql(invocation);
????????}
????????if?(target?instanceof?ParameterHandler){
????????????return?doSetParameter(invocation);
????????}
????????return?null;
????}
????private?Object?doQuery(Invocation?invocation)?throws?Exception{
????????Executor?executor?=?(Executor)?invocation.getTarget();
????????MappedStatement?ms=?(MappedStatement)?invocation.getArgs()[0];
????????Object?paramObj?=?invocation.getArgs()[1];
????????RowBounds?rowBounds?=?(RowBounds)?invocation.getArgs()[2];
????????if?(paramObj?instanceof?Map){
????????????MapperMethod.ParamMap?paramMap=?(MapperMethod.ParamMap)?paramObj;
????????????if?(!paramMap.containsKey(TENANT_ID)){
????????????????Long?tenantId=1L;
????????????????paramMap.put("param"+(paramMap.size()/2+1),tenantId);
????????????????paramMap.put(TENANT_ID,tenantId);
????????????????paramObj=paramMap;
????????????}
????????}
????????//直接執(zhí)行query,不用proceed()方法
????????return?executor.query(ms,?paramObj,rowBounds,null);
????}
????private?Object?changeBoundSql(Invocation?invocation)?throws?Exception?{
????????StatementHandler?statementHandler?=?(StatementHandler)?invocation.getTarget();
????????MetaObject?metaObject?=?SystemMetaObject.forObject(statementHandler);
????????PreparedStatementHandler?preparedStatementHandler?=?(PreparedStatementHandler)?metaObject.getValue("delegate");
????????String?originalSql?=?(String)?metaObject.getValue("delegate.boundSql.sql");
????????metaObject.setValue("delegate.boundSql.sql",originalSql+?"?and?tenant_id=?");
????????return?invocation.proceed();
????}
????private?Object?doSetParameter(Invocation?invocation)?throws?Exception?{
????????ParameterHandler?parameterHandler?=?(ParameterHandler)?invocation.getTarget();
????????PreparedStatement?ps?=?(PreparedStatement)?invocation.getArgs()[0];
????????MetaObject?metaObject?=?SystemMetaObject.forObject(parameterHandler);
????????BoundSql?boundSql=?(BoundSql)?metaObject.getValue("boundSql");
????????List?parameterMappings?=?boundSql.getParameterMappings();
????????boolean?hasTenantId=false;
????????for?(ParameterMapping?parameterMapping?:?parameterMappings)?{
????????????if?(parameterMapping.getProperty().equals(TENANT_ID))?{
????????????????hasTenantId=true;
????????????}
????????}
????????//添加參數(shù)
????????if?(!hasTenantId){
????????????Configuration?conf=?(Configuration)?metaObject.getValue("configuration");
????????????ParameterMapping?parameterMapping=?new?ParameterMapping.Builder(conf,TENANT_ID,Long.class).build();
????????????parameterMappings.add(parameterMapping);
????????}
????????parameterHandler.setParameters(ps);
????????return?null;
????}
}
在上面的過程中,攔截了sql執(zhí)行的三個階段,來實現(xiàn)多租戶的邏輯,邏輯分工如下:
攔截 Executor的query方法,在查詢的參數(shù)Map中添加租戶的屬性值,這里只是簡單的對Map的情況作了判斷,沒有對Bean的情況進行設置攔截 StatementHandler的prepare方法,改寫sql語句對象BoundSql,在sql語句中拼接租戶字段的查詢條件攔截 ParameterHandler的setParameters方法,動態(tài)設置參數(shù),將租戶id添加到要設置到參數(shù)列表中
最終通過攔截不同執(zhí)行階段的組合,實現(xiàn)了基于租戶的條件攔截。
總結
總的來說,mybatis攔截器通過對Executor、StatementHandler、ParameterHandler、ResultSetHandler 這4種接口中的方法進行攔截,并生成代理對象,在執(zhí)行方法前先執(zhí)行代理對象的邏輯,來實現(xiàn)我們自定義的邏輯增強。從上面的例子中,可以看到通過靈活使用mybatis攔截器開發(fā)插件能夠幫助我們解決很多問題,但是同樣它也是一把雙刃劍,在實際工作中也不要濫用插件、定義過多的攔截器,因為通過學習我們知道m(xù)ybatis插件在執(zhí)行中使用到了代理模式和責任鏈模式,在執(zhí)行sql語句前會經(jīng)過層層代理,如果代理次數(shù)過多將會消耗額外的性能,并增加響應時間。
2.?阿里面試這樣問:Nacos配置中心交互模型是 push 還是 pull ?(原理+源碼分析)
最近面試BAT,整理一份面試資料《Java面試BATJ通關手冊》,覆蓋了Java核心技術、JVM、Java并發(fā)、SSM、微服務、數(shù)據(jù)庫、數(shù)據(jù)結構等等。
獲取方式:點“在看”,關注公眾號并回復?Java?領取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉發(fā)吧。
謝謝支持喲 (*^__^*)

