簡介
插件是一種常見的擴展方式,大多數開源框架也都支持用戶通過添加自定義插件的方式來擴展或者改變原有的功能,MyBatis中也提供的有插件,雖然叫插件,但是實際上是通過攔截器(Interceptor)實現的,在MyBatis的插件模塊中涉及到責任鏈模式和JDK動態代理。
如何自定義實現插件?
step1:實現Interceptor接口;
step2:配置攔截器在全局配置文件
<plugins><plugin interceptor="com.dura.interceptor.FirstInterceptor"><-- 這里的這個屬性,我沒寫哈;不過,這里的用法倒是挺重要的,定義在攔截器中屬性 --><property name="testProp" value="1000"/></plugin></plugins>
step3:運行。
插件實現的原理?
初始化操作——全局配置文件解析
該方法用來解析全局配置文件中的plugins標簽,然后對應的創建Interceptor對象,并且封裝對應的屬性信息。最后調用了Configuration對象中的方法。 configuration.addInterceptor(interceptorInstance)
通過這個代碼我們發現我們自定義的攔截器最終是保存在了InterceptorChain這個對象中。而InterceptorChain的定義為
創建代理對象過程?
在解析的時候創建了對應的Interceptor對象,并保存在了InterceptorChain中,那么這個攔截器是如何和對應的目標對象進行關聯的呢?
首先攔截器可以攔截的對象是Executor,ParameterHandler,ResultSetHandler,StatementHandler。
那么我們來看下這四個對象在創建的時候又什么要注意的
Executor
Executor在裝飾完二級緩存后會通過pluginAll來創建Executor的代理對象。
StatementHandler
@Overridepublic <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();// 注意,已經來到SQL處理的關鍵對象 StatementHandlerStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 獲取一個 Statement對象stmt = prepareStatement(handler, ms.getStatementLog());// 執行查詢return handler.query(stmt, resultHandler);} finally {// 用完就關閉closeStatement(stmt);}}// 進入創建的方法public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 植入插件邏輯(返回代理對象)statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;}// 可以看到statementHandler的代理對象
ParameterHandler
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// StatementType 是怎么來的? 增刪改查標簽中的 statementType="PREPARED",默認值 PREPAREDswitch (ms.getStatementType()) {case STATEMENT:delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:// 創建 StatementHandler 的時候做了什么? >>delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException("Unknown statement type: " + ms.getStatementType());}}// 進入某個Handler看下
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);// 植入插件邏輯(返回代理對象)parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);return parameterHandler;}
ResultSetHandler
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,ResultHandler resultHandler, BoundSql boundSql) {ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);// 植入插件邏輯(返回代理對象)resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;}
執行過程
以Executor的query方法為例,當查詢請求到來的時候,Executor的代理對象是如何處理攔截請求的呢?我們來看下。當請求到了executor.query方法的時候
// 執行Plugin的invoke 方法
/*** 代理對象方法被調用時執行的代碼* @param proxy* @param method* @param args* @return* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 獲取當前方法所在類或接口中,可被當前Interceptor攔截的方法Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 當前調用的方法需要被攔截 執行攔截操作return interceptor.intercept(new Invocation(target, method, args));}// 不需要攔截 則調用 目標對象中的方法return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}//然后進入interceptor.intercept 會進入我們自定義的 FirstInterceptor對象中/*** 執行攔截邏輯的方法* @param invocation* @return* @throws Throwable*/@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("FirtInterceptor 攔截之前 ....");Object obj = invocation.proceed(); // 執行目標方法System.out.println("FirtInterceptor 攔截之后 ....");return obj;}
以上就是自定義的攔截器執行的完整流程。
如果我們有多個攔截器怎么辦
如果我們有多個自定義的攔截器,那么他的執行流程是怎么樣的呢?比如我們創建了兩個 Interceptor 都是用來攔截 Executor 的query方法,一個是用來執行邏輯A 一個是用來執行邏輯B的。
總結:Interceptor的相關對象作用
對象 | 作用 |
Interceptor | 自定義插件需要實現接口,實現4個方法 |
InterceptChain | 配置的插件解析后會保存在Configuration的InterceptChain中 |
Plugin | 觸發管理類,還可以用來創建代理對象 |
Invocation | 對被代理類進行包裝,可以調用proceed()調用到被攔截的方法 |
應用場景分析——其實在說攔截器啦
作用 | 描述 | 實現方式 |
水平分表 | 一張費用表按月度拆分為12張表。fee_202001-202012。當查詢條件出現月度(tran_month)時,把select語句中的邏輯表名修改為對應的月份表。 | 對query update方法進行攔截在接口上添加注解,通過反射獲取接口注解,根據注解上配置的參數進行分表,修改原SQL,例如id取模,按月分表 |
數據脫敏 | 手機號和身份證在數據庫完整存儲。但是返回給用戶,屏蔽手機號的中間四位。屏蔽身份證號中的出生日期。 | query——對結果集脫敏 |
菜單權限控制 | 不同的用戶登錄,查詢菜單權限表時獲得不同的結果,在前端展示不同的菜單 | 對query方法進行攔截在方法上添加注解,根據權限配置,以及用戶登錄信息,在SQL上加上權限過濾條件 |
黑白名單 | 有些SQL語句在生產環境中是不允許執行的,比如like %% | 對Executor的update和query方法進行攔截,將攔截的SQL語句和黑白名單 進行比較,控制SQL語句的執行 |
全局唯一ID | 在高并發的環境下傳統的生成ID的方式不太適用,這時我們就需要考慮其他方式了 | 創建插件攔截Executor的insert方法,通過UUID或者雪花算法來生成ID,并修改SQL中的插入信息 |