Mybatis的插件設(shè)計你知道多少?
本文主要分為兩部分,第一部分我們看插件設(shè)計原理和如何從 Mybatis 中學(xué)習(xí)設(shè)計插件,第二部分我們學(xué)習(xí)如何開發(fā)Mybatis插件。
一、插件設(shè)計原理
Mybatis 中的插件都是通過代理方式來實現(xiàn)的,通過攔截執(zhí)行器中指定的方法來達到改變核心執(zhí)行代碼的方式。舉一個列子,查詢方法核心都是通過 Executor來進行sql執(zhí)行的。那么我們就可以通過攔截下面的方法來改變核心代碼?;驹砭褪沁@樣,下面我們在來看 Mybatis 是如何處理插件。
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
...
}
| 名稱 | 類型 | 描述 |
|---|---|---|
Interceptor |
接口 | 插件都需要實現(xiàn)的接口,封裝代理執(zhí)行方法及參數(shù)信息 |
InterceptorChain |
類 | 攔截鏈 |
InvocationHandler |
接口 | JDK代理的接口,凡是JDK中的代理都要實現(xiàn)該接口 |
@Intercepts |
注解 | 用于聲明要代理和 @Signature 配合使用 |
@Signature |
注解 | 用于聲明要代理攔截的方法 |
Plugin |
類 | 代理的具體生成類 |
1. Interceptor
插件都需要實現(xiàn)的接口,封裝代理執(zhí)行方法及參數(shù)信息
public interface Interceptor {
// 執(zhí)行方法體的封裝,所有的攔截方法邏輯都在這里面寫。
Object intercept(Invocation invocation) throws Throwable;
// 如果要代理,就用Plugin.wrap(...),如果不代理就原樣返回
Object plugin(Object target);
// 可以添加配置,主要是xml配置時候可以從xml中讀取配置信息到攔截器里面自己解析
void setProperties(Properties properties);
}
2. InterceptorChain
攔截鏈,為什么需要攔截鏈,假如我們要對A進行代理, 具體的代理類有B和C。 我們要同時將B和C的邏輯都放到代理類里面,那我們會首先將A和B生成代理類,然后在前面生成代理的基礎(chǔ)上將C和前面生成的代理類在生成一個代理對象。這個類就是要做這件事 pluginAll
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
// 這里target就是A,而List中的Interceptor就相當(dāng)于B和C,通過循環(huán)方式生成統(tǒng)一代理類
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
//1. 是否需要代理,需要代理生成代理類放回,不需要原樣返回。通過for循環(huán)的方式將所有對應(yīng)的插件整合成一個代理對象
target = interceptor.plugin(target);
}
return target;
}
...
}
3. InvocationHandler
JDK代理的接口,凡是JDK中的代理都要實現(xiàn)該接口。這個比較基礎(chǔ),如果這個不清楚,那么代理就看不懂了。所以就不說了。
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
4. @Intercepts 和 @Signature
這兩個注解是配合使用的,用于指定要代理的類和方法。前面①說了,插件的核心邏輯是攔截執(zhí)行器的方法,那么這里我們看下如何聲明要攔截的類和方法。我們看一下分頁插件如何聲明攔截。
Signature 中 type 就是要攔截的類, method 要攔截的方法, args 要攔截的方法的入?yún)?因為有相同的方法,所以要指定攔截的方法和方法參數(shù))
@Intercepts(@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }))
public class MybatisPagerPlugin implements Interceptor {
}
args 要攔截的方法的入?yún)?因為有相同的方法,所以要指定攔截的方法和方法參數(shù))
比如 Executor 中就有2個 query 方法。所以要通過args來確定要攔截哪一個。
Mybatis這種插件管理模式, 在 Mybatis 的架構(gòu)中, 是有指定的,并不是說可以攔截任何類的任何方法,。它具體可以攔截什么類及方法,我們可以通過閱讀官方文檔 查看。
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
但是這種插件管理模式我們項目中也是可以用的。比如看下面例子。
public class Test {
public static void main(String[] args) {
InterceptorChain chain = new InterceptorChain();
PrintInterceptor printInterceptor = new PrintInterceptor();
Properties properties = new Properties();
properties.setProperty("name","https://blog.springlearn.cn");
printInterceptor.setProperties(properties);
chain.addInterceptor(printInterceptor);
Animal person = (Animal) chain.pluginAll(new Person());
String nihao = person.say("nihao");
System.out.println(nihao);
}
public interface Animal{
String say(String message);
String say(String name, String message);
}
public static class Person implements Animal {
public String say(String message) {
return message;
}
public String say(String name, String message) {
return name + " say: " + message;
}
}
@Intercepts(@Signature(type = Animal.class, method = "say", args = {String.class}))
public static class PrintInterceptor implements Interceptor {
private String name;
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println(name + ": before print ...");
Object proceed = invocation.proceed();
System.out.println(name + ": after print ...");
return proceed;
}
@Override
public Object plugin(Object target) {
if (target instanceof Person) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
this.name = properties.getProperty("name");
}
}
}
5. Plugin
代理的具體生成類,解析 @Intercepts 和 @Signature 注解生成代理。
我們看幾個重要的方法。
| 方法名 | 處理邏輯 |
|---|---|
| getSignatureMap | 解析@Intercepts和@Signature,找到要攔截的方法 |
| getAllInterfaces | 找到代理類的接口,jdk代理必須要有接口 |
| invoke | 是否需要攔截判斷 |
public class Plugin implements InvocationHandler {
//解析@Intercepts和@Signature找到要攔截的方法
private static Map<Class<?>, Set<Method>> 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[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
//通過方法名和方法參數(shù)查找方法
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;
}
//因為是jdk代理所以必須要有接口,如果沒有接口,就不會生成代理
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//執(zhí)行時候看當(dāng)前執(zhí)行的方法是否需要被攔截,如果需要就調(diào)用攔截器中的方法
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);
}
}
}
6. 總結(jié)
以上就是本篇文章的第一部分,主要講 "插件設(shè)計原理和如何從 Mybatis 中學(xué)習(xí)設(shè)計插件“
原理: 代理 ,并通過 @Intercepts 和 @Signature 配合指定要代理的方法。 注意Mybatis中那些類能指定是有限制的哦。
我們可以通過閱讀官方文檔 查看。
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
Mybatis 的插件模式,我們在項目中可以直接引入使用??梢詤⒖忌厦娴睦印?/p>
二、如何開發(fā)Mybatis插件代碼
如何開發(fā) Mybatis 插件,首先要知道原理, Mybatis 的原理前面就說了就是代理核心類的核心方法。前面我們也知道如何定義一個插件了。即就是用 @Intercepts 和 @Signature 來聲明要攔截的類和方法。 但是知道這些只能說會定義插件了,具體插件代碼怎么寫。我們要在看下 Mybatis 官方限制的那幾個類都有什么能力。
圖片描述的不是很具體,但是大概意思是這樣。 下面會一一簡述。
1. Executor
public interface Executor {
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
}
數(shù)據(jù)庫操作的第一步就是先調(diào)用 Executor , 如果要對sql語句進行增強 ,或者說是所有操作都進行增強都可以再這個里面處理。
2. ParameterHandler
sql入?yún)谶@里被解析并進行操作,哎呀,這么說真的太抽象了。舉例來說
public interface UserMapper {
@Insert("insert into bbs_role (role_id,role_name,created_date,updated_date,created_by,updated_by) values(#{user" +
".roleId}," +
"#{user.roleName},#{user.createdDate},#{user.updatedDate},#{user.createdBy},#{user.updatedBy})")
Integer insert(@Param("user") User user);
}
insert 方法中的user對象,如何填充到 sql 中,就是在 ParameterHandler 里面完成的。
-
第一步將sql中占位符替換成
?符號, 然后解析參數(shù)類型到ParameterMapping
在這里插入圖片描述
最終這些信息都會在BoundSql中保存。 總的來說 Sql信息(包括入?yún)⒌男畔?都會放在BoundSql中保存。 這里我們認(rèn)識了一個在ORM框架中非常重要的一個類
BoundSql如果想動態(tài)的修改sql就要跟著這個類的步伐。 將已經(jīng)解析好的sql提交給
PreparedStatement進行處理。
而ParameterHandler重要的一步就是將BoundSql里面的sql及入?yún)⒌姆诺?PreparedStatement里面進行數(shù)據(jù)查詢或者其他操作。PreparedStatement不解釋了,學(xué)JDBC的時候老師應(yīng)該都講過了。
如果要對sql到PreparedStatement的過程進行增強就可以代理整個類。
3. StatementHandler
代理 StatementHandler 能做什么?
前面 ParameterHandler 已經(jīng)可以將Sql信息寫入到 Statement 中,但是調(diào)用的邏輯就在 StatementHandler里面來處理了。如果要對這部分代碼做處理就可以攔截該方法。
4. ResultSetHandler
從名字就知道這個是對數(shù)據(jù)庫查詢后的記過進行處理的一個類。就是將jdbc的API返回數(shù)據(jù)轉(zhuǎn)換成方法簽名中的返回值。
public interface UserMapper {
@Select("select * from bbs_role")
List<User> query();
}
這里就是將 Statement 返回值轉(zhuǎn)換成 List<User>
以上就是Mybatis給我們提供插件增強的地方,以及每個地方要做的事情
但是到這里真的會寫插件了嗎? 我們還必須要參與實踐。如果我們要做一個功能將數(shù)據(jù)庫的sql信息打印出來,應(yīng)該知道在哪里處理了吧,只要獲取BoundSql對象打印sql即可。如果我們要寫分頁那就是對sql后面加上分頁的語法,這些說起來簡單,其實并不簡單,因為 Mybatis 提供對很多數(shù)據(jù)庫的支持, 每個數(shù)據(jù)庫的語法可能還不一樣,所以在寫插件時候要考慮的東西還是很多的, 如果我們不需要寫插件,也沒興趣做開源項目其實了解到這里已經(jīng)可以了。
但是如果感興趣的話可以關(guān)注我哦!
感謝您的閱讀,本文由 程序猿升級課 版權(quán)所有。如若轉(zhuǎn)載,請注明出處:程序猿升級課(https://blog.springlearn.cn/)