Mybatis超詳細(xì)插件機(jī)制解析,弄懂?dāng)r截器So easy
概述
Mybatis插件又稱攔截器,本篇文章中出現(xiàn)的攔截器都表示插件。
Mybatis采用責(zé)任鏈模式,通過(guò)動(dòng)態(tài)代理組織多個(gè)插件(攔截器),通過(guò)這些插件可以改變Mybatis的默認(rèn)行為(諸如SQL重寫之類的),由于插件會(huì)深入到Mybatis的核心,因此在編寫自己的插件前最好了解下它的原理,以便寫出安全高效的插件。
MyBatis 允許你在已映射語(yǔ)句執(zhí)行過(guò)程中的某一點(diǎn)進(jìn)行攔截調(diào)用。默認(rèn)情況下,MyBatis 允許使用插件來(lái)攔截的方法調(diào)用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
總體概括為:
- 攔截執(zhí)行器的方法
- 攔截參數(shù)的處理
- 攔截結(jié)果集的處理
- 攔截Sql語(yǔ)法構(gòu)建的處理
Mybatis是通過(guò)動(dòng)態(tài)代理的方式實(shí)現(xiàn)攔截的,閱讀此篇文章需要先對(duì)Java的動(dòng)態(tài)代理機(jī)制有所了解。
Mybatis四大接口
既然Mybatis是對(duì)四大接口進(jìn)行攔截的,那我們先要知道Mybatis的四大接口是哪些: Executor, StatementHandler, ResultSetHandler, ParameterHandler。

上圖Mybatis框架的整個(gè)執(zhí)行過(guò)程。Mybatis插件能夠?qū)@四大對(duì)象進(jìn)行攔截,可以說(shuō)包含到了Mybatis一次SQL執(zhí)行的所有操作??梢奙ybatis的的插件很強(qiáng)大。
- Executor是 Mybatis的內(nèi)部執(zhí)行器,它負(fù)責(zé)調(diào)用StatementHandler操作數(shù)據(jù)庫(kù),并把結(jié)果集通過(guò) ResultSetHandler進(jìn)行自動(dòng)映射,另外,他還處理了二級(jí)緩存的操作。從這里可以看出,我們也是可以通過(guò)插件來(lái)實(shí)現(xiàn)自定義的二級(jí)緩存的。
- StatementHandler是Mybatis直接和數(shù)據(jù)庫(kù)執(zhí)行sql腳本的對(duì)象。另外它也實(shí)現(xiàn)了Mybatis的一級(jí)緩存。這里,我們可以使用插件來(lái)實(shí)現(xiàn)對(duì)一級(jí)緩存的操作(禁用等等)。
- ParameterHandler是Mybatis實(shí)現(xiàn)Sql入?yún)⒃O(shè)置的對(duì)象。插件可以改變我們Sql的參數(shù)默認(rèn)設(shè)置。
- ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口對(duì)象。我們可以定義插件對(duì)Mybatis的結(jié)果集自動(dòng)映射進(jìn)行修改。
插件Interceptor
Mybatis的插件實(shí)現(xiàn)要實(shí)現(xiàn)Interceptor接口,我們看下這個(gè)接口定義的方法。
- public interface Interceptor {
- Object intercept(Invocation invocation) throws Throwable;
- Object plugin(Object target);
- void setProperties(Properties properties);
- }
這個(gè)接口只聲明了三個(gè)方法:
- setProperties方法是在Mybatis進(jìn)行配置插件的時(shí)候可以配置自定義相關(guān)屬性,即:接口實(shí)現(xiàn)對(duì)象的參數(shù)配置。
- plugin方法是插件用于封裝目標(biāo)對(duì)象的,通過(guò)該方法我們可以返回目標(biāo)對(duì)象本身,也可以返回一個(gè)它的代理,可以決定是否要進(jìn)行攔截進(jìn)而決定要返回一個(gè)什么樣的目標(biāo)對(duì)象,官方提供了示例:return Plugin.wrap(target, this)。
- intercept方法就是要進(jìn)行攔截的時(shí)候要執(zhí)行的方法。
理解這個(gè)接口的定義,先要知道java動(dòng)態(tài)代理機(jī)制。plugin接口即返回參數(shù)target對(duì)象(Executor/ParameterHandler/ResultSetHander/StatementHandler)的代理對(duì)象。在調(diào)用對(duì)應(yīng)對(duì)象的接口的時(shí)候,可以進(jìn)行攔截并處理。
Mybatis四大接口對(duì)象創(chuàng)建方法
Mybatis的插件是采用對(duì)四大接口的對(duì)象生成動(dòng)態(tài)代理對(duì)象的方法來(lái)實(shí)現(xiàn)的。那么現(xiàn)在我們看下Mybatis是怎么創(chuàng)建這四大接口對(duì)象的。
- public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
- //確保ExecutorType不為空(defaultExecutorType有可能為空)
- executorType = executorType == null ? defaultExecutorType : executorType;
- executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
- Executor executor; if (ExecutorType.BATCH == executorType) {
- executor = new BatchExecutor(this, transaction);
- } else if (ExecutorType.REUSE == executorType) {
- executor = new ReuseExecutor(this, transaction);
- } else {
- executor = new SimpleExecutor(this, transaction);
- } if (cacheEnabled) {
- executor = new CachingExecutor(executor);
- }
- executor = (Executor) interceptorChain.pluginAll(executor);
- return executor;
- }
- 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;
- }
- public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
- ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
- parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
- return parameterHandler;
- }
- 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;
- }
查看源碼可以發(fā)現(xiàn), Mybatis框架在創(chuàng)建好這四大接口對(duì)象的實(shí)例后,都會(huì)調(diào)用InterceptorChain.pluginAll()方法。InterceptorChain對(duì)象是插件執(zhí)行鏈對(duì)象,看源碼就知道里面維護(hù)了Mybatis配置的所有插件(Interceptor)對(duì)象。
- // target --> Executor/ParameterHandler/ResultSetHander/StatementHandler
- public Object pluginAll(Object target) {
- for (Interceptor interceptor : interceptors) {
- target = interceptor.plugin(target);
- }
- return target;
- }
其實(shí)就是按順序執(zhí)行我們插件的plugin方法,一層一層返回我們?cè)瓕?duì)象(Executor/ParameterHandler/ResultSetHander/StatementHandler)的代理對(duì)象。當(dāng)我們調(diào)用四大接口的方法的時(shí)候,實(shí)際上是調(diào)用代理對(duì)象的相應(yīng)方法,代理對(duì)象又會(huì)調(diào)用四大接口的實(shí)例。
Plugin對(duì)象
我們知道,官方推薦插件實(shí)現(xiàn)plugin方法為:Plugin.wrap(target, this);
- public static Object wrap(Object target, Interceptor interceptor) {
- // 獲取插件的Intercepts注解
- 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;
- }
這個(gè)方法其實(shí)是Mybatis簡(jiǎn)化我們插件實(shí)現(xiàn)的工具方法。其實(shí)就是根據(jù)當(dāng)前攔截的對(duì)象創(chuàng)建了一個(gè)動(dòng)態(tài)代理對(duì)象。代理對(duì)象的InvocationHandler處理器為新建的Plugin對(duì)象。
插件配置注解@Intercepts
Mybatis的插件都要有Intercepts注解來(lái)指定要攔截哪個(gè)對(duì)象的哪個(gè)方法。我們知道,Plugin.warp方法會(huì)返回四大接口對(duì)象的代理對(duì)象(通過(guò)new Plugin()創(chuàng)建的IvocationHandler處理器),會(huì)攔截所有的執(zhí)行方法。在代理對(duì)象執(zhí)行對(duì)應(yīng)方法的時(shí)候,會(huì)調(diào)用InvocationHandler處理器的invoke方法。Mybatis中利用了注解的方式配置指定攔截哪些方法。具體如下:
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- try {
- 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);
- }
- }
可以看到,只有通過(guò)Intercepts注解指定的方法才會(huì)執(zhí)行我們自定義插件的intercept方法。未通過(guò)Intercepts注解指定的將不會(huì)執(zhí)行我們的intercept方法。
官方插件開發(fā)方式
- @Intercepts({@Signature(type = Executor.class, method = "query",
- args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
- public class TestInterceptor implements Interceptor {
- public Object intercept(Invocation invocation) throws Throwable {
- Object target = invocation.getTarget(); //被代理對(duì)象
- Method method = invocation.getMethod(); //代理方法
- Object[] args = invocation.getArgs(); //方法參數(shù)
- // do something ...... 方法攔截前執(zhí)行代碼塊
- Object result = invocation.proceed();
- // do something .......方法攔截后執(zhí)行代碼塊
- return result;
- }
- public Object plugin(Object target) {
- return Plugin.wrap(target, this);
- }
- }
以上就是Mybatis官方推薦的插件實(shí)現(xiàn)的方法,通過(guò)Plugin對(duì)象創(chuàng)建被代理對(duì)象的動(dòng)態(tài)代理對(duì)象??梢园l(fā)現(xiàn),Mybatis的插件開發(fā)還是很簡(jiǎn)單的。
自定義開發(fā)方式
Mybatis的插件開發(fā)通過(guò)內(nèi)部提供的Plugin對(duì)象可以很簡(jiǎn)單的開發(fā)。只有理解了插件實(shí)現(xiàn)原理,對(duì)應(yīng)不采用Plugin對(duì)象我們一樣可以自己實(shí)現(xiàn)插件的開發(fā)。下面是我個(gè)人理解之后的自己實(shí)現(xiàn)的一種方式。
- public class TestInterceptor implements Interceptor {
- public Object intercept(Invocation invocation) throws Throwable {
- Object target = invocation.getTarget(); //被代理對(duì)象
- Method method = invocation.getMethod(); //代理方法
- Object[] args = invocation.getArgs(); //方法參數(shù)
- // do something ...... 方法攔截前執(zhí)行代碼塊
- Object result = invocation.proceed();
- // do something .......方法攔截后執(zhí)行代碼塊
- return result;
- }
- public Object plugin(final Object target) {
- return Proxy.newProxyInstance(Interceptor.class.getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- return intercept(new Invocation(target, method, args));
- }
- });
- }
- public void setProperties(Properties properties) {
- }
- }
當(dāng)然,Mybatis插件的那這個(gè)時(shí)候Intercepts的注解起不到作用了。
小結(jié)
我們?cè)贛yBatis配置了一個(gè)插件,在運(yùn)行發(fā)生了什么
- 所有可能被攔截的處理類都會(huì)生成一個(gè)代理
- 處理類代理在執(zhí)行對(duì)應(yīng)方法時(shí),判斷要不要執(zhí)行插件中的攔截方法
- 執(zhí)行插接中的攔截方法后,推進(jìn)目標(biāo)的執(zhí)行
如果有N個(gè)插件,就有N個(gè)代理,每個(gè)代理都要執(zhí)行上面的邏輯。這里面的層層代理要多次生成動(dòng)態(tài)代理,是比較影響性能的。雖然能指定插件攔截的位置,但這個(gè)是在執(zhí)行方法時(shí)動(dòng)態(tài)判斷,初始化的時(shí)候就是簡(jiǎn)單的把插件包裝到了所有可以攔截的地方。
因此,在編寫插件時(shí)需注意以下幾個(gè)原則:
- 不編寫不必要的插件;
- 實(shí)現(xiàn)plugin方法時(shí)判斷一下目標(biāo)類型,是本插件要攔截的對(duì)象才執(zhí)行Plugin.wrap方法,否者直接返回目標(biāo)本身,這樣可以減少目標(biāo)被代理的次數(shù)。
- // 假如我們只要攔截Executor對(duì)象,那么我們應(yīng)該這么做
- public Object plugin(final Object target) {
- if (target instanceof Executor) {
- return Plugin.wrap(target, this);
- } else {
- return target;
- }
- }
Mybatis插件很強(qiáng)大,可以對(duì)Mybatis框架進(jìn)行很大的擴(kuò)展。當(dāng)然,如果你不理解Mybatis插件的原理,開發(fā)起來(lái)只能是模擬兩可。在實(shí)際開發(fā)過(guò)程中,我們可以參考別人寫的插件。下面是一個(gè)Mybatis分頁(yè)的插件,可以為以后開發(fā)做參考。
- /**
- * Mybatis - 通用分頁(yè)插件(如果開啟二級(jí)緩存需要注意)
- */
- @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class}),
- @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
- @Log4j
- public class PageHelper implements Interceptor {
- public static final ThreadLocal<Page> localPage = new ThreadLocal<Page>();
- /**
- * 開始分頁(yè)
- *
- * @param pageNum
- * @param pageSize
- */
- public static void startPage(int pageNum, int pageSize) {
- localPage.set(new Page(pageNum, pageSize));
- }
- /**
- * 結(jié)束分頁(yè)并返回結(jié)果,該方法必須被調(diào)用,否則localPage會(huì)一直保存下去,直到下一次startPage
- *
- * @return
- */
- public static Page endPage() {
- Page page = localPage.get();
- localPage.remove();
- return page;
- }
- public Object intercept(Invocation invocation) throws Throwable {
- if (localPage.get() == null) {
- return invocation.proceed();
- }
- if (invocation.getTarget() instanceof StatementHandler) {
- StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
- MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
- // 分離代理對(duì)象鏈(由于目標(biāo)類可能被多個(gè)插件攔截,從而形成多次代理,通過(guò)下面的兩次循環(huán)
- // 可以分離出最原始的的目標(biāo)類)
- while (metaStatementHandler.hasGetter("h")) {
- Object object = metaStatementHandler.getValue("h");
- metaStatementHandler = SystemMetaObject.forObject(object);
- }
- // 分離最后一個(gè)代理對(duì)象的目標(biāo)類
- while (metaStatementHandler.hasGetter("target")) {
- Object object = metaStatementHandler.getValue("target");
- metaStatementHandler = SystemMetaObject.forObject(object);
- }
- MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
- //分頁(yè)信息if (localPage.get() != null) {
- Page page = localPage.get();
- BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
- // 分頁(yè)參數(shù)作為參數(shù)對(duì)象parameterObject的一個(gè)屬性
- String sql = boundSql.getSql();
- // 重寫sql
- String pageSql = buildPageSql(sql, page);
- //重寫分頁(yè)sql
- metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
- Connection connection = (Connection) invocation.getArgs()[0];
- // 重設(shè)分頁(yè)參數(shù)里的總頁(yè)數(shù)等
- setPageParameter(sql, connection, mappedStatement, boundSql, page);
- // 將執(zhí)行權(quán)交給下一個(gè)插件
- return invocation.proceed();
- } else if (invocation.getTarget() instanceof ResultSetHandler) {
- Object result = invocation.proceed();
- Page page = localPage.get();
- page.setResult((List) result);
- return result;
- }
- return null;
- }
- /**
- * 只攔截這兩種類型的
- * <br>StatementHandler
- * <br>ResultSetHandler
- *
- * @param target
- * @return
- */
- public Object plugin(Object target) {
- if (target instanceof StatementHandler || target instanceof ResultSetHandler) {
- return Plugin.wrap(target, this);
- } else {
- return target;
- }
- }
- public void setProperties(Properties properties) {
- }
- /**
- * 修改原SQL為分頁(yè)SQL
- *
- * @param sql
- * @param page
- * @return
- */
- private String buildPageSql(String sql, Page page) {
- StringBuilder pageSql = new StringBuilder(200);
- pageSql.append("select * from (");
- pageSql.append(sql);
- pageSql.append(" ) temp limit ").append(page.getStartRow());
- pageSql.append(" , ").append(page.getPageSize());
- return pageSql.toString();
- }
- /**
- * 獲取總記錄數(shù)
- *
- * @param sql
- * @param connection
- * @param mappedStatement
- * @param boundSql
- * @param page
- */
- private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
- BoundSql boundSql, Page page) {
- // 記錄總記錄數(shù)
- String countSql = "select count(0) from (" + sql + ") temp";
- PreparedStatement countStmt = null;
- ResultSet rs = null;
- try {
- countStmt = connection.prepareStatement(countSql);
- BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
- boundSql.getParameterMappings(), boundSql.getParameterObject());
- setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
- rs = countStmt.executeQuery();
- int totalCount = 0;
- if (rs.next()) {
- totalCount = rs.getInt(1);
- }
- page.setTotal(totalCount);
- int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);
- page.setPages(totalPage);
- } catch (SQLException e) {
- log.error("Ignore this exception", e);
- } finally {
- try {
- rs.close();
- } catch (SQLException e) {
- log.error("Ignore this exception", e);
- }
- try {
- countStmt.close();
- } catch (SQLException e) {
- log.error("Ignore this exception", e);
- }
- }
- }
- /**
- * 代入?yún)?shù)值
- *
- * @param ps
- * @param mappedStatement
- * @param boundSql
- * @param parameterObject
- * @throws SQLException
- */
- private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
- Object parameterObject) throws SQLException {
- ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
- parameterHandler.setParameters(ps);
- }
- @Data //采用lombok插件編譯
- public static class Page<E> {
- private int pageNum;
- private int pageSize;
- private int startRow;
- private int endRow;
- private long total;
- private int pages;
- private List<E> result;
- public Page(int pageNum, int pageSize) {
- this.pageNum = pageNum;
- this.pageSize = pageSize;
- this.startRow = pageNum > 0 ? (pageNum - 1) * pageSize : 0;
- this.endRow = pageNum * pageSize;
- }
- }
- }