1. 插件簡介
一般情況下,開源框架都會提供插件或其他形式的拓展點雷逆,供開發(fā)者自行拓展污尉。這樣的好處是顯而易見的往产,一是增加了框架的靈活性某宪。一是開發(fā)者可以結合實際需求兴喂,對框架進行拓展衣迷,使其能夠更好的工作。
以 MyBatis
為例云矫,我們可基于 MyBatis
插件機制實現(xiàn)分頁让禀、分表陨界,監(jiān)控等功能菌瘪。由于插件和業(yè)務無關,業(yè)務也無法感知插件的存在缀皱。因此可以無感植入插件啤斗,在無形中增強功能赁咙。
2. Mybatis 插件介紹
Mybatis
作為?個應用廣泛的優(yōu)秀的 ORM
開源框架彼水,這個框架具有強大的靈活性凤覆,在四大組件 (Executor、StatementHandler慈俯、ParameterHandler贴膘、ResultSetHandler)
處提供了簡單易用的插件擴展機制。Mybatis
對持久層的操作就是借助于四大核心對象洋闽。MyBatis支持用插件對四大核心對象進行攔截诫舅,對 mybatis
來說插件就是攔截器骚勘,用來增強核心對象的功能撮奏,增強功能本質(zhì)上是借助于底層的動態(tài)代理實現(xiàn)的畜吊,換句話說玲献,MyBatis中的四大對象都是代理對象
梯浪。
MyBatis
所允許攔截的方法如下:
- 執(zhí)行器
Executor
(update
挂洛、query
虏劲、commi
t柒巫、rollback
等方法); -
SQL
語法構建器StatementHandler
(prepare
应结、parameterize
鹅龄、batch
、updates
漩绵、query
等方法)止吐; - 參數(shù)處理器
ParameterHandler
(getParameterObject
碍扔、setParameters
方法)秕重; - 結果集處理器
ResultSetHandler
(handleResultSets``handleOutputParameters
等方法)溶耘;
3. Mybatis 插件原理
3.1 在四大對象創(chuàng)建的時候
- 每個創(chuàng)建出來的對象不是直接返回的凳兵,而是
interceptorChain.pluginAll(parameterHandler);
- 獲取到所有的
Interceptor
(攔截器)庐扫,插件需要實現(xiàn)的接口;調(diào)用interceptor.plugin(target)
铅辞;返回target
包裝后的對象 - 插件機制斟珊,我們可以使用插件為目標對象創(chuàng)建?個代理對象验靡;
AOP
(面向切面)我們的插件可以為四大對象創(chuàng)建出代理對象胜嗓,代理對象就可以攔截到四大對象的每?個執(zhí)行辞州;
3.2 攔截
插件具體是如何攔截并附加額外的功能的呢?以 ParameterHandler
來說
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 Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
interceptorChain
保存了所有的攔截器(interceptors
)涝涤,是mybatis
初始化的時候創(chuàng)建的阔拳。調(diào)用攔截器鏈中的攔截器依次的對目標進行攔截或增強类嗤。interceptor.plugin(target)
中的 target
就可以理解為mybatis
中的四大對象遗锣。返回的 target
是被重重代理后的對象
如果我們想要攔截 Executor
的 query
方法精偿,那么可以這樣定義插件:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args= {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
//省略邏輯
}
除此之外笔咽,我們還需將插件配置到 sqlMapConfig.xml
中。
<plugins>
<plugin interceptor="com.lagou.plugin.ExamplePlugin"/>
</plugins>
這樣 MyBatis
在啟動時可以加載插件斯撮,并保存插件實例到相關對象(InterceptorChain
,攔截器鏈) 中枣氧。待準備工作做完后达吞,MyBatis
處于就緒狀態(tài)酪劫。我們在執(zhí)行 SQL
時寺董,需要先通過 DefaultSqlSessionFactory
創(chuàng)建 SqlSession
遮咖。Executor
實例會在創(chuàng)建 SqlSession
的過程中被創(chuàng)建, Executor
實例創(chuàng)建完畢后漓藕,MyBatis
會通過 JDK
動態(tài)代理為實例生成代理類挟裂。這樣诀蓉,插件邏輯即可在 Executor
相關方法被調(diào)用前執(zhí)行交排。
以上就是MyBatis插件機制的基本原理
4. 自定義插件
4.1 插件接口
Mybatis
插件接口 Interceptor
-
Intercept
方法埃篓,插件的核心方法 -
plugin
方法架专,生成target
的代理對象 -
setProperties
方法,傳遞插件所需參數(shù)
4.2 自定義插件
- 設計實現(xiàn)一個自定義插件
package com.study.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
/**
* 自定義插件
* @author xiaosong
* @since 2021/4/14
*/
@Intercepts({ //注意看這個大花括號,也就這說這里可以定義多個@Signature對多個地方攔截丧没,都用這個攔截器
@Signature(
type = StatementHandler.class,//這是指攔截哪個接口
method = "prepare", //這個接口內(nèi)的哪個方法名锡移,不要拼錯了
args = {Connection.class,Integer.class} // 這是攔截的方法的?參淆珊,按順序?qū)懙竭@施符,不要多也不要少戳吝,如果方法重載,可是要通過方法名和入?yún)泶_定唯一的
)
})
public class MyPlugin implements Interceptor {
/**
* //這里是每次執(zhí)?操作的時候柬采,都會進行這個攔截器的方法內(nèi)
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
//增強邏輯
System.out.println("對方法進?了增強....");
//執(zhí)行原方法
return invocation.proceed();
}
/**
* 主要是為了把這個攔截器?成一個代理放到攔截器鏈中粉捻,包裝?標對象 為?標對象創(chuàng)建代理對象
* @param target 為要攔截的對象
* @return 代理對象
*/
@Override
public Object plugin(Object target) {
System.out.println("將要包裝的目標對象:"+target);
return Plugin.wrap(target,this);
}
/**
* 獲取配置文件的屬性
* 插件初始化的時候調(diào)用斑芜,也只調(diào)用?次杏头,插件配置的屬性從這里設置進來
* @param properties
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化參數(shù):"+properties);
}
}
sqlMapConfig.xml
<plugins>
<plugin interceptor="com.study.plugin.MyPlugin">
<!--配置參數(shù)-->
<property name="name" value="Bob"/>
</plugin>
</plugins>
-
mapper
接口
public interface UserMapper {
List<User> selectUser();
}
- 測試類
package com.study.mapper;
import com.study.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* 緩存的測試
* @author xiaosong
* @since 2021/4/12
*/
public class PluginTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void before() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testSelect(){
//根據(jù) sqlSessionFactory 產(chǎn)生 session
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
}
}
-
執(zhí)行順序
5. 源碼分析
- 執(zhí)行插件邏輯
Plugin
實現(xiàn)了InvocationHandler
接口,因此它的invoke
方法會攔截所有的方法調(diào)用叛氨。invoke
方法會對所攔截的方法進行檢測寞埠,以決定是否執(zhí)行插件邏輯仁连。該方法的邏輯如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 獲取被攔截方法列表饭冬,比如:signatureMap.get(Executor.class), 可能返回 [query, update,commit]
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//檢測方法列表是否包含被攔截的方法
if (methods != null && methods.contains(method)) {
//執(zhí)行插件邏輯
return interceptor.intercept(new Invocation(target, method, args));
}
//執(zhí)行被攔截的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
invoke
方法的代碼比較少,邏輯不難理解颇象。首先, invoke
方法會檢測被攔截方法是否配置在插件的 @Signature
注解中伍伤,若是,則執(zhí)行插件邏輯遣钳,否則執(zhí)行被攔截方法。插件邏輯封裝在 intercept
中麦乞,該方法的參數(shù)類型為 Invocation
蕴茴,Invocation
主要用于存儲目標類,方法以及方法參數(shù)列表姐直。下面簡單看?下該類的定義
package org.apache.ibatis.plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author Clinton Begin
*/
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
// 調(diào)用被攔截的方法
return method.invoke(target, args);
}
}
6. pageHelper 分頁插件
MyBatis
可以使用第三方的插件來對功能進行擴展,分頁助手
PageHelper
是將分的復雜操作進行封裝声畏,使用簡單的方式即可獲得分頁的相關數(shù)據(jù)撞叽。
開發(fā)步驟:
① 導入通用 PageHelper
的坐標
② 在 mybatis
核心配置文件中配置 PageHelper
插件
③ 測試分頁數(shù)據(jù)獲取
代碼實現(xiàn):
- 導入通用
PageHelper
坐標
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.2.1</version>
</dependency>
- 在
mybatis
核心配置文件中配置PageHelper
插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
- 測試分頁代碼實現(xiàn)
@Test
public void testPageHelper(){
PageHelper.startPage(1,1);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
}
- 獲得分頁相關的其他參數(shù)
@Test
public void testPageHelper(){
PageHelper.startPage(1,1);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.findUserList();
for (User user : userList) {
System.out.println(user);
}
// 其它分頁數(shù)據(jù)
PageInfo<User> pageInfo = new PageInfo<>(userList);
System.out.println("總條數(shù):"+pageInfo.getTotal());
System.out.println("總?數(shù):"+pageInfo. getPages ());
System.out.println("當前?:"+pageInfo. getPageNum());
System.out.println("每?顯萬?度:"+pageInfo.getPageSize());
System.out.println("是否第??:"+pageInfo.isIsFirstPage());
System.out.println("是否最后??:"+pageInfo.isIsLastPage());
}
7. 通用 Mapper
7.1 什么是通用 Mapper
通用 Mapper
就是為了解決單表增刪改查姻成,基于 Mybatis
的插件機制。開發(fā)人員不需要編寫 SQL
,不需要在 DAO
中增加方法愿棋,只要寫好實體類科展,就能支持相應的增刪改查方法
7.2 如何使用
- 首先在
maven
項目,在pom.xml
中引入mapper
的依賴
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
-
Mybatis
配置文件中完成配置
<plugins>
<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
</plugin>
</plugins>
- 實體類設置主鍵
package com.study.pojo;
import lombok.Data;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.List;
/**
* 用戶的實體類
* @author xiaosong
* @since 2021/4/1
*/
@Data
@Table(name = "user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
private String birthday;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", birthday='" + birthday + '\'' +
'}';
}
}
- 定義通用
mapper
package com.study.mapper;
import com.study.pojo.User;
import tk.mybatis.mapper.common.Mapper;
import java.util.List;
/**
* 用戶的持久層
* @author xiaosong
* @since 2021/4/12
*/
public interface UserMapper extends Mapper<User> {
/**
* 查詢用戶信息
* @return List<User>
*/
List<User> findUserList();
}
- 測試
@Test
public void testMapper(){
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setId(1);
// mapper 基礎接口糠雨, select
// 根據(jù)實體中的屬性進行查詢才睹,只能有一個返回值
User user1 = userMapper.selectOne(user);
System.out.println(user1);
// 參數(shù)為 null 時,查詢?nèi)? List<User> userList = userMapper.select(null);
for (User user2 : userList) {
System.out.println(user2);
}
// 根據(jù)主鍵字段進行查詢甘邀,方法參數(shù)必須包含完整的主鍵屬性琅攘,查詢條件使用等號
User user2 = userMapper.selectByPrimaryKey(1);
System.out.println(user2);
//根據(jù)實體中的屬性查詢總數(shù),查詢條件使用等號
int count = userMapper.selectCount(user);
System.out.println("總數(shù)" + count);
// mapper 基礎接口松邪, insert
//保存一個實體坞琴,null值也會保存,不會使用數(shù)據(jù)庫默認值
user.setId(3);
int insertCount = userMapper.insert(user);
System.out.println("插入條數(shù)" + insertCount);
//保存實體逗抑,null的屬性不會保存剧辐,會使用數(shù)據(jù)庫默認值
user.setId(4);
int insertCount1 = userMapper.insertSelective(user);
System.out.println("插入條數(shù)" + insertCount1);
// mapper 基礎接口, update
//根據(jù)主鍵更新實體全部字段锋八, null值會被更新
user.setId(4);
int updateCount = userMapper.updateByPrimaryKey(user);
System.out.println("更新條數(shù)" + updateCount);
// mapper 基礎接口浙于, delete
//根據(jù)實體屬性作為條件進行刪除,查詢條件使用等號
int deleteCount = userMapper.delete(user);
System.out.println("刪除條數(shù)" + deleteCount);
// example方法
Example example = new Example(User.class);
example.createCriteria().andEqualTo("id",1)
.andLike("username","E%");
List<User> userList1 = userMapper.selectByExample(example);
for (User user3 : userList1) {
System.out.println(user3);
}
}