大多數框架,都支持插件,用戶可通過編寫插件來自行擴展功能,Mybatis也不例外。
我們從插件配置、插件編寫、插件運行原理、插件注冊與執行攔截的時機、初始化插件、分頁插件的原理等六個方面展開闡述。
一、插件配置
Mybatis的插件配置在configuration內部,初始化時,會讀取這些插件,保存於Configuration對象的InterceptorChain中。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> 3 <configuration> 4 <plugins> 5 <plugin interceptor="com.mybatis3.interceptor.MyBatisInterceptor"> 6 <property name="value" value="100" /> 7 </plugin> 8 </plugins> 9 </configuration>
1 public class Configuration 2 { 3 protected final InterceptorChain interceptorChain = new 4 InterceptorChain(); 5 }
org.apache.ibatis.plugin.InterceptorChain.java源碼。
1 public class InterceptorChain { 2 3 private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); 4 5 public Object pluginAll(Object target) { 6 for (Interceptor interceptor : interceptors) { 7 target = interceptor.plugin(target); 8 } 9 return target; 10 } 11 12 public void addInterceptor(Interceptor interceptor) { 13 interceptors.add(interceptor); 14 } 15 16 public List<Interceptor> getInterceptors() { 17 return Collections.unmodifiableList(interceptors); 18 } 19 20 }
上面的for循環代表了只要是插件,都會以責任鏈的方式逐一執行,所謂插件,其實就類似於攔截器。
二、插件的編寫
插件必須實現org.apache.ibatis.plugin.Interceptor接口。
1 public interface Interceptor { 2 3 Object intercept(Invocation invocation) throws Throwable; 4 5 Object plugin(Object target); 6 7 void setProperties(Properties properties); 8 9 }
-intercept()方法:執行攔截內容的地方,攔截目標對象的目標方法的執行
-plugin()方法:決定是否觸發intercept()方法。 作用:包裝目標對象,包裝就是為目標對象創建一個代理對象
-setProperties()方法:給自定義的攔截器傳遞xml配置的屬性參數。將插件注冊時的property屬性設置進來
下面自定義一個攔截器:
1 package com.atguigu.mybatis.dao; 2 3 import java.util.Properties; 4 5 import org.apache.ibatis.executor.statement.StatementHandler; 6 import org.apache.ibatis.plugin.Interceptor; 7 import org.apache.ibatis.plugin.Intercepts; 8 import org.apache.ibatis.plugin.Invocation; 9 import org.apache.ibatis.plugin.Plugin; 10 import org.apache.ibatis.plugin.Signature; 11 import org.apache.ibatis.reflection.MetaObject; 12 import org.apache.ibatis.reflection.SystemMetaObject; 13 14 15 /* 16 * 完成插件簽名: 17 * 告訴Mybatis當前插件用來攔截哪個對象的哪個方法 18 * 19 * */ 20 @Intercepts( 21 { 22 @Signature(type=StatementHandler.class,method ="parameterize",args = java.sql.Statement.class ) 23 } 24 ) 25 public class MyFirstPlugin implements Interceptor 26 { 27 /* 28 * 攔截目標對象的目標方法的執行 29 * */ 30 @Override 31 public Object intercept(Invocation invocation) throws Throwable 32 { 33 System.out.println("MyFirstPlugin的intercept方法執行,攔截方法:"+invocation.getMethod()); 34 35 //動態改變一下sql的參數,查3號員工,這里偷梁換柱一下查詢4號員工 36 Object target = invocation.getTarget(); 37 System.out.println("當前攔截的對象:"+target); 38 //拿到:StatementHandler==>ParameterHandler==>parameterObject 39 //拿到target(攔截對象)的元數據 40 MetaObject metaObject = SystemMetaObject.forObject(target); 41 Object value = metaObject.getValue("parameterHandler.parameterObject"); 42 System.out.println("sql語句用的參數是:"+value); 43 metaObject.setValue("parameterHandler.parameterObject", 4); 44 45 //執行目標方法 46 Object proceed = invocation.proceed(); 47 48 49 50 //返回執行后的返回值 51 return proceed; 52 } 53 54 55 /* 56 * 插件: 57 * 作用:包裝目標對象,包裝就是為目標對象創建一個代理對象 58 * */ 59 @Override 60 public Object plugin(Object target) 61 { 62 System.out.println("MyFirstPlugin的plugin方法執行,包裝的對象(目標對象):"+target); 63 64 65 //我們可以借助Plugin的wrap方法來使用當前的攔截器包裝我們的目標對象 66 Object wrap = Plugin.wrap(target, this); 67 68 //返回的就是為當前target創建的動態代理 69 return wrap; 70 } 71 72 /* 73 * setProperties: 74 * 將插件注冊時的property屬性設置進來 75 * 76 * */ 77 @Override 78 public void setProperties(Properties properties) 79 { 80 System.out.println("插件配置的信息:"+properties); 81 } 82 83 }
2.1、為什么要寫Annotation注解?注解都是什么含義?
Mybatis規定插件必須編寫Annotation注解,是必須,而不是可選。@Intercepts注解:裝載一個@Signature列表,一個@Signature其實就是一個需要攔截的方法封裝。那么,一個攔截器要攔截多個方法,自然就是一個@Signature列表。
type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }
解釋:要攔截Executor接口內的query()方法,參數類型為args列表。
2.2、Plugin.wrap(target, this)是干什么的?
使用JDK的動態代理,給target對象創建一個delegate代理對象,以此來實現方法攔截和增強功能,它會回調intercept()方法。
2.3、Mybatis可以攔截哪些接口對象?
Mybatis只能攔截ParameterHandler、ResultSetHandler、StatementHandler、Executor共4個接口對象內的方法。
重新審視interceptorChain.pluginAll()方法:該方法在創建上述4個接口對象時調用,其含義為給這些接口對象注冊攔截器功能,注意是注冊,而不是執行攔截。
攔截器執行時機:plugin()方法注冊攔截器后,那么,在執行上述4個接口對象內的具體方法時,就會自動觸發攔截器的執行,也就是插件的執行。
2.4、Invocation
1 public class Invocation { 2 private Object target; 3 private Method method; 4 private Object[] args; 5 }
可以通過invocation來獲取攔截的目標方法,以及執行目標方法。
2.5、分頁插件原理
由於Mybatis采用的是邏輯分頁,而非物理分頁,那么,市場上就出現了可以實現物理分頁的Mybatis的分頁插件。 要實現物理分頁,就需要對String sql進行攔截並增強,Mybatis通過BoundSql對象存儲String sql,而BoundSql則由StatementHandler對象獲取。
1 public interface StatementHandler { 2 <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; 3 4 BoundSql getBoundSql(); 5 }
1 public class BoundSql { 2 public String getSql() { 3 return sql; 4 } 5 }
因此,就需要編寫一個針對StatementHandler的query方法攔截器,然后獲取到sql,對sql進行重寫增強。