- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) > 編程語(yǔ)言 >
- Mybatis自定義攔截器和插件開(kāi)發(fā)詳解
在Spring中我們經(jīng)常會(huì )使用到攔截器,在登錄驗證、日志記錄、性能監控等場(chǎng)景中,通過(guò)使用攔截器允許我們在不改動(dòng)業(yè)務(wù)代碼的情況下,執行攔截器的方法來(lái)增強現有的邏輯。在mybatis中,同樣也有這樣的業(yè)務(wù)場(chǎng)景,有時(shí)候需要我們在不侵入原有業(yè)務(wù)代碼的情況下攔截sql,執行特定的某些邏輯。那么這個(gè)過(guò)程應該怎么實(shí)現呢,同樣,在mybatis中也為開(kāi)發(fā)者預留了攔截器接口,通過(guò)實(shí)現自定義攔截器這一功能,可以實(shí)現我們自己的插件,允許用戶(hù)在不改動(dòng)mybatis的原有邏輯的條件下,實(shí)現自己的邏輯擴展。
本文將按下面的結構進(jìn)行mybatis攔截器學(xué)習:
本文結構
1、攔截器核心對象
2、工作流程
3、攔截器能實(shí)現什么
4、插件定義與注冊
5、攔截器使用示例
6、總結
在實(shí)現攔截器之前,我們首先看一下攔截器的攔截目標對象是什么,以及攔截器的工作流程是怎樣的。mybatis攔截器可以對下面4種對象進(jìn)行攔截:
1、Executor:mybatis的內部執行器,作為調度核心負責調用StatementHandler操作數據庫,并把結果集通過(guò)ResultSetHandler進(jìn)行自動(dòng)映射
2、StatementHandler: 封裝了JDBC Statement操作,是sql語(yǔ)法的構建器,負責和數據庫進(jìn)行交互執行sql語(yǔ)句
3、ParameterHandler:作為處理sql參數設置的對象,主要實(shí)現讀取參數和對PreparedStatement的參數進(jìn)行賦值
4、ResultSetHandler:處理Statement執行完成后返回結果集的接口對象,mybatis通過(guò)它把ResultSet集合映射成實(shí)體對象
在mybatis中提供了一個(gè)Interceptor接口,通過(guò)實(shí)現該接口就能夠自定義攔截器,接口中定義了3個(gè)方法:
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:在攔截目標對象的方法時(shí),實(shí)際執行的增強邏輯,我們一般在該方法中實(shí)現自定義邏輯
plugin:用于返回原生目標對象或它的代理對象,當返回的是代理對象的時(shí)候,會(huì )調用intercept方法
setProperties:可以用于讀取配置文件中通過(guò)property標簽配置的一些屬性,設置一些屬性變量
看一下plugin方法中的wrap方法源碼:
public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> 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方法中,通過(guò)使用jdk動(dòng)態(tài)代理的方式,生成了目標對象的代理對象,在執行實(shí)際方法前,先執行代理對象中的邏輯,來(lái)實(shí)現的邏輯增強。以攔截Executor的query方法為例,在實(shí)際執行前會(huì )執行攔截器中的intercept方法:
在mybatis中,不同類(lèi)型的攔截器按照下面的順序執行:
Executor -> StatementHandler -> ParameterHandler -> ResultSetHandler
以執行query 方法為例對流程進(jìn)行梳理,整體流程如下:
1、Executor執行query()方法,創(chuàng )建一個(gè)StatementHandler對象
2、StatementHandler 調用ParameterHandler對象的setParameters()方法
3、StatementHandler 調用 Statement對象的execute()方法
4、StatementHandler 調用ResultSetHandler對象的handleResultSets()方法,返回最終結果
攔截器能實(shí)現什么
在對mybatis攔截器有了初步的認識后,來(lái)看一下攔截器被普遍應用在哪些方面:
可以攔截執行的sql方法,可以打印執行的sql語(yǔ)句、參數等信息,并且還能夠記錄執行的總耗時(shí),可供后期的sql分析時(shí)使用
mybatis中使用的RowBounds使用的內存分頁(yè),在分頁(yè)前會(huì )查詢(xún)所有符合條件的數據,在數據量大的情況下性能較差。通過(guò)攔截器,可以做到在查詢(xún)前修改sql語(yǔ)句,提前加上需要的分頁(yè)參數
在數據庫中通常會(huì )有createTime,updateTime等公共字段,這類(lèi)字段可以通過(guò)攔截統一對參數進(jìn)行的賦值,從而省去手工通過(guò)set方法賦值的繁瑣過(guò)程
在很多系統中,不同的用戶(hù)可能擁有不同的數據訪(fǎng)問(wèn)權限,例如在多租戶(hù)的系統中,要做到租戶(hù)間的數據隔離,每個(gè)租戶(hù)只能訪(fǎng)問(wèn)到自己的數據,通過(guò)攔截器改寫(xiě)sql語(yǔ)句及參數,能夠實(shí)現對數據的自動(dòng)過(guò)濾
除此之外,攔截器通過(guò)對上述的4個(gè)階段的介入,結合我們的實(shí)際業(yè)務(wù)場(chǎng)景,還能夠實(shí)現很多其他功能。
插件定義與注冊
在我們自定義的攔截器類(lèi)實(shí)現了Interceptor接口后,還需要在類(lèi)上添加@Intercepts 注解,標識該類(lèi)是一個(gè)攔截器類(lèi)。注解中的內容是一個(gè)@Signature對象的數組,指明自定義攔截器要攔截哪一個(gè)類(lèi)型的哪一個(gè)具體方法。其中type指明攔截對象的類(lèi)型,method是攔截的方法,args是method執行的參數。通過(guò)這里可以了解到 mybatis 攔截器的作用目標是在方法級別上進(jìn)行攔截,例如要攔截Executor的query方法,就在類(lèi)上添加:
@Intercepts({ @Signature(type = Executor.class,method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
如果要攔截多個(gè)方法,可以繼續以數組的形式往后追加。這里通過(guò)添加參數可以確定唯一的攔截方法,例如在Executor中存在兩個(gè)query方法,通過(guò)上面的參數可以確定要攔截的是下面的第2個(gè)方法:
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql); <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler);
當編寫(xiě)完成我們自己的插件后,需要向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中對自定義插件進(jìn)行注冊:
<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>
在前面我們了解了不同類(lèi)型攔截器執行的固定順序,那么對于同樣類(lèi)型的多個(gè)自定義攔截器,它們的執行順序是怎樣的呢?分別在plugin方法和intercept中添加輸出語(yǔ)句,運行結果如下:
從結果可以看到,攔截順序是按照注冊順序執行的,但代理邏輯的執行順序正好相反,最后注冊的會(huì )被最先執行。這是因為在mybatis中有一個(gè)類(lèi)InterceptorChain,在它的pluginAll()方法中,會(huì )對原生對象target進(jìn)行代理,如果有多個(gè)攔截器的話(huà),會(huì )對代理類(lèi)再次進(jìn)行代理,最終實(shí)現一層層的增強target對象,因此靠后被注冊的攔截器的增強邏輯會(huì )被優(yōu)先執行。從下面的圖中可以直觀(guān)的看出代理的嵌套關(guān)系:
在xml中注冊完成后,在application.yml中啟用配置文件,這樣插件就可以正常運行了:
mybatis: config-location: classpath:mybatis-config.xml
在了解了插件的基礎概念與運行流程之后,通過(guò)代碼看一下應用不同的攔截器能夠實(shí)現什么功能。
Executor
通過(guò)攔截Executor的query和update方法實(shí)現對sql的監控,在攔截方法中,打印sql語(yǔ)句、執行參數、實(shí)際執行時(shí)間:
@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<ParameterMapping> 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"); } }
執行代碼,日志輸出如下:
在上面的代碼中,通過(guò)Executor攔截器獲取到了BoundSql對象,進(jìn)一步獲取到sql的執行參數,從而實(shí)現了對sql執行的監控與統計。
StatementHandler
下面的例子中,通過(guò)改變StatementHandler對象的屬性,動(dòng)態(tài)修改sql語(yǔ)句的分頁(yè):
@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提供的一個(gè)用于方便、優(yōu)雅訪(fǎng)問(wèn)對象屬性的對象,通過(guò)將實(shí)例對象作為參數傳遞給它,就可以通過(guò)屬性名稱(chēng)獲取對應的屬性值。雖然說(shuō)我們也可以通過(guò)反射拿到屬性的值,但是反射過(guò)程中需要對各種異常做出處理,會(huì )使代碼中堆滿(mǎn)難看的try/catch,通過(guò)MetaObject可以在很大程度上簡(jiǎn)化我們的代碼,并且它支持對Bean、Collection、Map三種類(lèi)型對象的操作。
對比執行前后:
可以看到這里通過(guò)改變了分頁(yè)對象RowBounds的屬性,動(dòng)態(tài)的修改了分頁(yè)參數。
ResultSetHandler
ResultSetHandler 會(huì )負責映射sql語(yǔ)句查詢(xún)得到的結果集,如果在生產(chǎn)環(huán)境中存在一些保密數據,不想在外部系統中展示,那么可能就需要在查詢(xún)到結果后做一下數據的脫敏處理,這時(shí)候就可以使用ResultSetHandler對結果集進(jìn)行改寫(xiě)。
@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<Object> objList= (Collection) result; List<Object> resultList=new ArrayList<>(); for (Object obj : objList) { resultList.add(desensitize(obj)); } return resultList; }else { return desensitize(result); } } //脫敏方法,將加密字段變?yōu)樾翘? private Object desensitize(Object object) throws InvocationTargetException, IllegalAccessException { Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { Confidential confidential = field.getAnnotation(Confidential.class); if (confidential==null){ continue; } PropertyDescriptor ps = BeanUtils.getPropertyDescriptor(object.getClass(), field.getName()); if (ps.getReadMethod() == null || ps.getWriteMethod() == null) { continue; } Object value = ps.getReadMethod().invoke(object); if (value != null) { ps.getWriteMethod().invoke(object, "***"); } } return object; } }
運行上面的代碼,查看執行結果:
{"id":1358041517788299266,"orderNumber":"***","money":122.0,"status":3,"tenantId":2}
在上面的例子中,在執行完sql語(yǔ)句得到結果對象后,通過(guò)反射掃描結果對象中的屬性,如果實(shí)體的屬性上帶有自定義的@Confidential注解,那么在脫敏方法中將它轉化為星號再返回結果,從而實(shí)現了數據的脫敏處理。
ParameterHandler
mybatis可以攔截ParameterHandler注入參數,下面的例子中我們將結合前面介紹的其他種類(lèi)的對象,通過(guò)組合攔截器的方式,實(shí)現一個(gè)簡(jiǎn)單的多租戶(hù)攔截器插件,實(shí)現多租戶(hù)下的查詢(xú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; } } //直接執行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<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); boolean hasTenantId=false; for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getProperty().equals(TENANT_ID)) { hasTenantId=true; } } //添加參數 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; } }
在上面的過(guò)程中,攔截了sql執行的三個(gè)階段,來(lái)實(shí)現多租戶(hù)的邏輯,邏輯分工如下:
最終通過(guò)攔截不同執行階段的組合,實(shí)現了基于租戶(hù)的條件攔截。
總的來(lái)說(shuō),mybatis攔截器通過(guò)對Executor、StatementHandler、ParameterHandler、ResultSetHandler 這4種接口中的方法進(jìn)行攔截,并生成代理對象,在執行方法前先執行代理對象的邏輯,來(lái)實(shí)現我們自定義的邏輯增強。從上面的例子中,可以看到通過(guò)靈活使用mybatis攔截器開(kāi)發(fā)插件能夠幫助我們解決很多問(wèn)題,但是同樣它也是一把雙刃劍,在實(shí)際工作中也不要濫用插件、定義過(guò)多的攔截器,因為通過(guò)學(xué)習我們知道mybatis插件在執行中使用到了代理模式和責任鏈模式,在執行sql語(yǔ)句前會(huì )經(jīng)過(guò)層層代理,如果代理次數過(guò)多將會(huì )消耗額外的性能,并增加響應時(shí)間。
到此這篇關(guān)于Mybatis自定義攔截器和插件開(kāi)發(fā)的文章就介紹到這了,更多相關(guān)Mybatis自定義攔截器內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。
Copyright ? 2009-2022 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 特網(wǎng)科技 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 百度云 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站