MyBatis插件原理分析,看完感覺自己better了
本文主要內(nèi)容:
大多數(shù)框架都支持插件,用戶可通過編寫插件來自行擴(kuò)展功能,Mybatis也不例外。
在Mybatis中最出名的就是PageHelper 分頁插件,下面我們先來使用一下這個分頁插件。
如何集成分頁插件
Spring-Boot+Mybatis+PageHelper 。
引入pom依賴
- <dependency>
- <groupId>com.github.pagehelper</groupId>
- <artifactId>pagehelper-spring-boot-starter</artifactId>
- <version>1.2.3</version>
- </dependency>
配置分頁插件配置項
- pagehelper:
- helperDialect: mysql
- reasonable: true
- supportMethodsArguments: true
- params: count=countSql
service接口代碼中
- PageInfo selectUsersByName(int pageIndex, int pageSize);
service實現(xiàn)類代碼中
- @Override
- public PageInfo selectUsersByName(int pageIndex, int pageSize) {
- PageHelper.startPage(pageIndex, pageSize);
- List<User> users = userMapper.selectUsersByName(null);
- return new PageInfo(users);
- }
Mapper代碼代碼
- <select id="selectUsersByName" resultMap="User">
- select * from m_user
- <where>
- <if test="userName != null and userName != ''">
- `name` = #{userName}
- </if>
- </where>
- </select>
- List<User> selectUsersByName(@Param("userName") String userName);
controller中代碼
- @GetMapping("/user/name")
- public PageInfo selectUsersByName(int pageIndex, int pageSize) {
- return userService.selectUsersByName(pageIndex, pageSize);
- }
然后我們訪問
http://localhost:9002/user/name?pageIndex=1&pageSize=10
輸出結(jié)果:
輸出重要項說明:
- pageNum:當(dāng)前頁碼。
- pageSize:每頁數(shù)。
- list:就是我們返回的業(yè)務(wù)數(shù)據(jù)。
- total:總數(shù)據(jù)。
- hasNextPage:是否存在下一頁。
我們在看看輸出SQL:
發(fā)現(xiàn)其實執(zhí)行了兩條SQL:count和limit。
猜測分頁插件實現(xiàn)
1.這個分頁插件無非就是在我們的查詢條件上拼接了個limit和做了一個count查詢。
2.我們這里使用的是Mysql作為數(shù)據(jù)庫,如果是Oracle的話那就不是limit了,所以這里有多重數(shù)據(jù)庫對應(yīng)的方案。
3.在沒有此插件的前面攔截并做了sql和相關(guān)處理。
根據(jù)官網(wǎng)快速入門插件
下面是來自官網(wǎng)的一段話:
MyBatis 允許你在映射語句執(zhí)行過程中的某一點進(jìn)行攔截調(diào)用。默認(rèn)情況下,MyBatis 允許使用插件來攔截的方法調(diào)用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
這些類中方法的細(xì)節(jié)可以通過查看每個方法的簽名來發(fā)現(xiàn),或者直接查看 MyBatis 發(fā)行包中的源代碼。如果你想做的不僅僅是監(jiān)控方法的調(diào)用,那么你最好相當(dāng)了解要重寫的方法的行為。因為在試圖修改或重寫已有方法的行為時,很可能會破壞 MyBatis 的核心模塊。這些都是更底層的類和方法,所以使用插件的時候要特別當(dāng)心。
通過 MyBatis 提供的強(qiáng)大機(jī)制,使用插件是非常簡單的,只需實現(xiàn) Interceptor 接口,并指定想要攔截的方法簽名即可。
那我們就嘗試著按照官方來寫一個插件。
自定義插件
- @Intercepts({@Signature(
- type= Executor.class,
- method = "update",
- args = {MappedStatement.class,Object.class})})
- public class TianPlugin implements Interceptor {
- private Properties properties = new Properties();
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- System.out.println("老田寫的一個Mybatis插件--start");
- Object returnObject = invocation.proceed();
- System.out.println("老田寫的一個Mybatis插件---end");
- return returnObject;
- }
- }
然后把插件類注入到容器中。
這里的自定義完全是官網(wǎng)給出的案例。從自定義的插件類中看到有個update,我們猜測肯定是需要執(zhí)行update才會被攔截到。
訪問前面的代碼:http://localhost:9002/updateUser
成功了。
這是大家肯定會聯(lián)想到我們剛剛開始學(xué)動態(tài)代理的時候,不就是在要調(diào)用的方法的前面和后面做點小東東嗎?
Mybatis的插件確實就是這樣的。
我們來分析一下官方的那段話和我們自定義的插件。
分析
首先,我們自定義的插件必須是針對下面這四個類以及方法。
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
其次,我們必須實現(xiàn)Mybatis的Interceptor。
Interceptor中三個方法的作用:
- intercept():執(zhí)行攔截內(nèi)容的地方,比如:在調(diào)用某類方法前后做一些自己的處理,簡單就是打印日志。
- plugin():決定是否觸發(fā)intercept()方法。
- setProperties():給自定義的攔截器傳遞我們配置的屬性參數(shù)(這個可以暫時不管他,后面我們寫一個相對完整點的插件,你就明白是干啥的了)。
plugin方法
- default Object plugin(Object target) {
- return Plugin.wrap(target, this);
- }
默認(rèn)實現(xiàn)方法,里面調(diào)用了Plugin.wrap()方法。
- public class Plugin implements InvocationHandler {
- private Object target;
- private Interceptor interceptor;
- private Map<Class<?>, Set<Method>> signatureMap;
- private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
- this.target = target;
- this.interceptor = interceptor;
- this.signatureMap = signatureMap;
- }
- public static Object wrap(Object target, Interceptor interceptor) {
- Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
- Class<?> type = target.getClass();
- Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
- if (interfaces.length > 0) {
- // 創(chuàng)建JDK動態(tài)代理對象
- return Proxy.newProxyInstance(
- type.getClassLoader(),
- interfaces,
- new Plugin(target, interceptor, signatureMap));
- }
- return target;
- }
- @Override
- 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)) {
- // 回調(diào)intercept()方法
- return interceptor.intercept(new Invocation(target, method, args));
- }
- return method.invoke(target, args);
- } catch (Exception e) {
- throw ExceptionUtil.unwrapThrowable(e);
- }
- }
- //...省略其他不相關(guān)代碼
- }
這不就是一個JDK動態(tài)代理嗎?
Map
所以,我們不要動不動就說反射性能很差,那是因為你沒有像Mybatis一樣去緩存一個對象的反射結(jié)果。
判斷是否是需要攔截的方法,這句注釋很重要,一旦忽略了,都不知道Mybatis是怎么判斷是否執(zhí)行攔截內(nèi)容的,要記住。
Plugin.wrap(target, this)是干什么的?
使用JDK的動態(tài)代理,給target對象創(chuàng)建一個delegate代理對象,以此來實現(xiàn)方法攔截和增強(qiáng)功能,它會回調(diào)intercept()方法。
為什么要寫注解?注解都是什么含義?
在我們自定義的插件上有一堆注解,別害怕。
Mybatis規(guī)定插件必須編寫Annotation注解,是必須,而不是可選。
- @Intercepts({@Signature( type= Executor.class, method = "update",
- args = {MappedStatement.class,Object.class})}
- )
- public class TianPlugin implements Interceptor {
@Intercepts注解:裝載一個@Signature列表,一個@Signature其實就是一個需要攔截的方法封裝。那么,一個攔截器要攔截多個方法,自然就是一個@Signature列表。
- type= Executor.class, method = "update",args = {MappedStatement.class,Object.class}
解釋:要攔截Executor接口內(nèi)的query()方法,參數(shù)類型為args列表。
那如果想攔截多個方法呢?
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
- public @interface Intercepts {
- Signature[] value();
- }
這就簡單了吧,我們在@Intercepts注解中可以存放多個@Signature注解。
比如說前面分頁插件中就是攔截多個方法的。
為什么攔截兩個都是query方法呢?因為在Executor中有兩個query方法。
總結(jié)下:
Mybatis規(guī)定必須使用@Intercepts注解。
@Intercepts注解內(nèi)可以添加多個類多個方法,注意方法名和參數(shù)類型個數(shù)一定要對應(yīng)起來。
本文轉(zhuǎn)載自微信公眾號「 Java后端技術(shù)全?!梗梢酝ㄟ^以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 Java后端技術(shù)全棧公眾號。