MyBatis(八):MyBatis外掛機制詳解
阿新 • • 發佈:2021-03-12
1. MyBatis外掛外掛機制簡介
MyBatis外掛其實就是為使用者提供的自行拓展攔截器,主要是為了可以更好的滿足業務需要。
在MyBatis中提供了四大核心元件對資料庫進行處理,分別是Executor、Statement Handler、ParameterHandler及ResultSetHandler,同時也支援對這四大元件進行自定義擴充套件攔截,用來增強核心物件的功能。其本質上是使用底層的動態代理來實現的,即程式執行時執行的都是代理後的物件。
> MyBatis允許攔截的方法如下:
>
> - 執行器Executor (update、query、commit、rollback等方法);
>
> - SQL語法構建器StatementHandler (prepare、parameterize、batch、updates query等方 法);
>
> - 引數處理器ParameterHandler (getParameterObject、setParameters方法);
>
> - 結果集處理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);
2. 攔截器引數簡介
- 攔截器註解
```java
@Intercepts(//可配置多個@Signature,均使用此類進行攔截增強
{
@Signature(//指定需要攔截的類、方法及方法引數
type = Executor.class,//需要攔截介面
method = "update",//需要攔截方法名稱
args = {MappedStatement.class, Object.class}//攔截方法的請求引數
)
}
)
```
- 實現Interceptor介面,並實現方法
```java
public Object intercept(Invocation invocation)//每次攔截到都會執行此方法,方法內寫增強邏輯
invocation//代理物件,可以獲取目標方法、請求引數、執行結果等
invocation.proceed() //執行目標方法
```
```java
public Object plugin(Object target)
Plugin.wrap(target,this)//包裝目標物件,為目標物件建立代理物件,將當前生成的代理物件放入攔截器鏈中
```
```java
public void setProperties(Properties properties)//獲取配置檔案中的外掛引數,外掛初始化時呼叫一次
```
3. 自定義外掛
- 新建MyExecuter類,實現Interceptor介面
```java
package com.rangers.plugin;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Method;
import java.util.Properties;
/**
* @Author Rangers
* @Description 自定義Executor update方法
* @Date 2021-03-11
**/
@Intercepts({
@Signature(type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
})
public class MyExecuter implements Interceptor {
// 接收外掛的配置引數
private Properties properties = new Properties();
// 增強邏輯寫在此方法中
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 列印外掛的配置引數
System.out.println("外掛的配置引數:"+properties.toString());
// 獲取目標方法Method物件
Method method = invocation.getMethod();
// 獲取目標方法的請求引數 與args列表一一對應
String reqParam = invocation.getArgs()[1].toString();
System.out.println("方法名稱:"+method.getName()+" 請求引數:"+ reqParam);
// 執行目標方法
Object result = invocation.proceed();
System.out.println("方法名稱:"+method.getName()+" 執行結果:"+result);
return result;
}
/**
* @Author Rangers
* @Description
**/
@Override
public Object plugin(Object target) {
//System.out.println("需要包裝的目標物件:"+target+" 目標物件型別"+ target.getClass());
// 主要是將當前生成的代理物件放入攔截器鏈中,包裝目標物件,為目標物件建立代理物件
return Plugin.wrap(target,this);
}
/**
* @Author Rangers
* @Description 獲取配置檔案中的外掛屬性引數,外掛初始化時呼叫一次
**/
@Override
public void setProperties(Properties properties) {
// 將配置引數進行接收
this.properties = properties;
}
}
```
- 主配置檔案新增標籤
```xml
```
4. 外掛原理
a、在Executor、StatementHandler、ParameterHandler及ResultSetHandler四大物件建立時,並不是直接返回的,而是中間多了一步**interceptorChain.pluginAll()**(均在Configuration類中進行建立)。
- Executor—interceptorChain.pluginAll(executor);
```java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
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;
}
```
- StatementHandler—interceptorChain.pluginAll(statementHandler);
```java
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;
}
```
- ParameterHandler—interceptorChain.pluginAll(parameterHandler);
```java
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—interceptorChain.pluginAll(resultSetHandler)
```java
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;
}
```
b、interceptorChain.pluginAll()呼叫的就是實現了Interceptor介面的plugin()方法,plugin()方法又通過Plugin.wrap(target,this)為目標物件建立一個Plugin的代理物件,新增到攔截鏈interceptorChain中。具體程式碼如下:
```java
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 呼叫實現Interceptor介面的plugin方法
target = interceptor.plugin(target);
}
return target;
}
```
```java
@Override
public Object plugin(Object target) {
// 呼叫Plugin的wrap()方法,建立代理物件
return Plugin.wrap(target,this);
}
```
```java
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;
}
```
c、Plugin實現了 InvocationHandler介面,因此它的invoke方法會攔截所有的方法呼叫。invoke()方法會 對所攔截的方法進行檢測,以決定是否執行外掛邏輯。
```java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 根據元件物件Class從signatureMap中獲取到需要攔截的方法set集合
Set 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);
}
}
```
```java
private static Map, Set> getSignatureMap(Interceptor interceptor) {
// 獲取到所有攔截器的類
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
// 獲取到@Signature註解陣列
Signature[] sigs = interceptsAnnotation.value();
// signatureMap存放所有攔截到方法,key為四大元件的Class,value為元件對應的方法set集合
Map, Set> signatureMap = new HashMap, Set>();
for (Signature sig : sigs) {
Set methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
```
5. PageHelper外掛
PageHelper是MyBaits框架使用最廣泛的第三方物理分頁外掛,分頁助手PageHelper是將分頁的複雜操作進行封裝,使用簡單的方式即可獲得分頁的相關資料。
使用步驟:
- 新增依賴
```xml
com.github.pagehelper
pagehelper
5.1.8
com.github.jsqlparser
jsqlparser
1.2
```
- 配置外掛
```xml
```
- 使用分頁
```java
@org.junit.Test
public void testPagehealper() {
PageHelper.startPage(1, 2);
List users = userDao.findAll();
if (users != null && users.size() > 0) {
for (User user : users) {
System.out.println(user.toString());
}
PageInfo pageInfo = new PageInfo<>(users);
System.out.println("總條數:" + pageInfo.getTotal());
System.out.println("總頁數:" + pageInfo.getPages());
System.out.println("當前頁:" + pageInfo.getPageNum());
System.out.println("每頁顯萬長度:" + pageInfo.getPageSize());
System.out.println("是否第一頁:" + pageInfo.isIsFirstPage());
System.out.println("是否最後一頁:" + pageInfo.isIsLastPage());
}
}