MyBatis實現(xiàn)自定義MyBatis插件的流程詳解
初識插件
我們在執(zhí)行查詢的時候,如果sql沒有加上分頁條件,數(shù)據(jù)量過大的話會造成內(nèi)存溢出,因此我們可以通過MyBatis提供的插件機(jī)制來攔截sql,并進(jìn)行sql改寫。MyBatis的插件是通過動態(tài)代理來實現(xiàn)的,并且會形成一個插件鏈。原理類似于攔截器,攔截我們需要處理的對象,進(jìn)行自定義邏輯后,返回一個代理對象,進(jìn)行下一個攔截器的處理。
我們先來看下一個簡單插件的模板,首先要實現(xiàn)一個Interceptor接口,并實現(xiàn)三個方法。并加上@Intercepts注解。接下來我們以分頁插件為例將對每個細(xì)節(jié)進(jìn)行講解。
/**
* @ClassName : PagePlugin
* @Description : 分頁插件
* @Date: 2020/12/29
*/
@Intercepts({})
public class PagePlugin implements Interceptor {
private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
攔截對象
在進(jìn)行插件創(chuàng)建的時候,需要指定攔截對象。@Intercepts注解指定需要攔截的方法簽名,內(nèi)容是個Signature類型的數(shù)組,而Signature就是對攔截對象的描述。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
/**
* Returns method signatures to intercept.
*
* @return method signatures
*/
Signature[] value();
}
Signature 需要指定攔截對象中方法的信息的描述。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* 對象類型
*/
Class<?> type();
/**
* 方法名
*/
String method();
/**
* 參數(shù)類型
*/
Class<?>[] args();
}
在MyBatis中,我們只能對以下四種類型的對象進(jìn)行攔截
- ParameterHandler : 對sql參數(shù)進(jìn)行處理
- ResultSetHandler : 對結(jié)果集對象進(jìn)行處理
- StatementHandler : 對sql語句進(jìn)行處理
- Executor : 執(zhí)行器,執(zhí)行增刪改查
現(xiàn)在我們需要對sql進(jìn)行改寫,因此可以需要攔截Executor的query方法進(jìn)行攔截
@Intercepts({@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
攔截實現(xiàn)
每個插件除了指定攔截的方法后,還需要實現(xiàn)Interceptor接口。Interceptor接口有以下三個方法。其中intercept是我們必須要實現(xiàn)的方法,在這里面我們需要實現(xiàn)自定義邏輯。其它兩個方法給出了默認(rèn)實現(xiàn)。
public interface Interceptor {
/**
* 進(jìn)行攔截處理
* @param invocation
* @return
* @throws Throwable
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 返回代理對象
* @param target
* @return
*/
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 設(shè)置配置屬性
* @param properties
*/
default void setProperties(Properties properties) {
// NOP
}
}
因此我們實現(xiàn)intercept方法即可,因為我們要改寫查詢sql語句,因此需要攔截Executor的query方法,然后修改RowBounds參數(shù)中的limit,如果limit大于1000,我們強(qiáng)制設(shè)置為1000。
@Slf4j
@Intercepts({@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
public class PagePlugin implements Interceptor {
private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rowBounds = (RowBounds)args[2];
log.info("執(zhí)行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
if(rowBounds != null){
if(rowBounds.getLimit() > 1000){
Field field = rowBounds.getClass().getDeclaredField("limit");
field.setAccessible(true);
field.set(rowBounds, 1000);
}
}else{
rowBounds = new RowBounds(0 ,100);
args[2] = rowBounds;
}
log.info("執(zhí)行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
加載流程
以上我們已經(jīng)實現(xiàn)了一個簡單的插件,在執(zhí)行查詢的時候?qū)uery方法進(jìn)行攔截,并且修改分頁參數(shù)。但是我們現(xiàn)在還沒有進(jìn)行插件配置,只有配置了插件,MyBatis才能啟動過程中加載插件。
xml配置插件
在mybatis-config.xml中添加plugins標(biāo)簽,并且配置我們上面實現(xiàn)的plugin
<plugins> <plugin interceptor="com.example.demo.mybatis.PagePlugin"> </plugin> </plugins>
XMLConfigBuilder加載插件
在啟動流程中加載插件中使用到SqlSessionFactoryBuilder的build方法,其中XMLConfigBuilder這個解析器中的parse()方法就會讀取plugins標(biāo)簽下的插件,并加載Configuration中的InterceptorChain中。
// SqlSessionFactoryBuilder
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
可見XMLConfigBuilder這個parse()方法就是解析xml中配置的各個標(biāo)簽。
// XMLConfigBuilder
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 解析properties節(jié)點
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
// 記載插件
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
XMLConfigBuilder 的pluginElement就是遍歷plugins下的plugin加載到interceptorChain中。
// XMLConfigBuilder
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
// 遍歷每個plugin插件
for (XNode child : parent.getChildren()) {
// 讀取插件的實現(xiàn)類
String interceptor = child.getStringAttribute("interceptor");
// 讀取插件配置信息
Properties properties = child.getChildrenAsProperties();
// 創(chuàng)建interceptor對象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
// 加載到interceptorChain鏈中
configuration.addInterceptor(interceptorInstance);
}
}
}
InterceptorChain 是一個interceptor集合,相當(dāng)于是一層層包裝,后一個插件就是對前一個插件的包裝,并返回一個代理對象。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
// 生成代理對象
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// 將插件加到集合中
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
創(chuàng)建插件對象
因為我們需要對攔截對象進(jìn)行攔截,并進(jìn)行一層包裝返回一個代理類,那是什么時候進(jìn)行處理的呢?以Executor為例,在創(chuàng)建Executor對象的時候,會有以下代碼。
// Configuration
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);
}
// 創(chuàng)建插件對象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
創(chuàng)建完Executor對象后,就會調(diào)用interceptorChain.pluginAll()方法,實際調(diào)用的是每個Interceptor的plugin()方法。plugin()就是對目標(biāo)對象的一個代理,并且生成一個代理對象返回。而Plugin.wrap()就是進(jìn)行包裝的操作。
// Interceptor
/**
* 返回代理對象
* @param target
* @return
*/
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
Plugin的wrap()主要進(jìn)行了以下步驟:
- 獲取攔截器攔截的方法,以攔截對象為key,攔截方法集合為value
- 獲取目標(biāo)對象的class對,比如Executor對象
- 如果攔截器中攔截的對象包含目標(biāo)對象實現(xiàn)的接口,則返回攔截的接口
- 創(chuàng)建代理類Plugin對象,Plugin實現(xiàn)了
InvocationHandler接口,最終對目標(biāo)對象的調(diào)用都會調(diào)用Plugin的invocate方法。
// Plugin
public static Object wrap(Object target, Interceptor interceptor) {
// 獲取攔截器攔截的方法,以攔截對象為key,攔截方法為value
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 獲取目標(biāo)對象的class對象
Class<?> type = target.getClass();
// 如果攔截器中攔截的對象包含目標(biāo)對象實現(xiàn)的接口,則返回攔截的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 如果對目標(biāo)對象進(jìn)行了攔截
if (interfaces.length > 0) {
// 創(chuàng)建代理類Plugin對象
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
例子
我們已經(jīng)了解MyBatis插件的配置,創(chuàng)建,實現(xiàn)流程,接下來就以一開始我們提出的例子來介紹實現(xiàn)一個插件應(yīng)該做哪些。
確定攔截對象
因為我們要對查詢sql分頁參數(shù)進(jìn)行改寫,因此可以攔截Executor的query方法,并進(jìn)行分頁參數(shù)的改寫
@Intercepts({@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
實現(xiàn)攔截接口
實現(xiàn)Interceptor接口,并且實現(xiàn)intercept實現(xiàn)我們的攔截邏輯
@Slf4j
@Intercepts({@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
public class PagePlugin implements Interceptor {
private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rowBounds = (RowBounds)args[2];
log.info("執(zhí)行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
if(rowBounds != null){
if(rowBounds.getLimit() > 1000){
Field field = rowBounds.getClass().getDeclaredField("limit");
field.setAccessible(true);
field.set(rowBounds, 1000);
}
}else{
rowBounds = new RowBounds(0 ,100);
args[2] = rowBounds;
}
log.info("執(zhí)行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
配置插件
在mybatis-config.xml中配置以下插件
<plugins> <plugin interceptor="com.example.demo.mybatis.PagePlugin"> </plugin> </plugins>
測試
TTestUserMapper.java 新增selectByPage方法
List<TTestUser> selectByPage(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);
mapper/TTestUserMapper.xml 新增對應(yīng)的sql
<select id="selectByPage" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_test_user
<if test="offset != null">
limit #{offset}, #{pageSize}
</if>
</select>
最終測試代碼,我們沒有在查詢的時候指定分頁參數(shù)。
public static void main(String[] args) {
try {
// 1. 讀取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 創(chuàng)建SqlSessionFactory工廠
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 獲取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
// 4. 獲取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 執(zhí)行接口方法
List<TTestUser> list2 = userMapper.selectByPage(null, null);
System.out.println("list2="+list2.size());
// 6. 提交事物
sqlSession.commit();
// 7. 關(guān)閉資源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
}
最終打印的日志如下,我們可以看到rowBounds已經(jīng)被我們強(qiáng)制修改了只能查處1000條數(shù)據(jù)。
10:11:49.313 [main] INFO com.example.demo.mybatis.PagePlugin - 執(zhí)行前, rowBounds = [{"offset":0,"limit":2147483647}]
10:11:58.015 [main] INFO com.example.demo.mybatis.PagePlugin - 執(zhí)行后, rowBounds = [{"offset":0,"limit":1000}]
10:12:03.211 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
10:12:04.269 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 749981943.
10:12:04.270 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2cb3d0f7]
10:12:04.283 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user
10:12:04.335 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==> Parameters:
list2=1000
以上就是MyBatis實現(xiàn)自定義MyBatis插件的流程詳解的詳細(xì)內(nèi)容,更多關(guān)于MyBatis自定義MyBatis插件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ProtoStuff不支持BigDecimal序列化及反序列化詳解
這篇文章主要為大家介紹了ProtoStuff不支持BigDecimal序列化/反序列化,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
java開啟遠(yuǎn)程debug竟有兩種參數(shù)(最新推薦)
這篇文章主要介紹了java開啟遠(yuǎn)程debug竟有兩種參數(shù),本文結(jié)合實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07
SpringCloud?OpenFeign?服務(wù)調(diào)用傳遞?token的場景分析
這篇文章主要介紹了SpringCloud?OpenFeign?服務(wù)調(diào)用傳遞?token的場景分析,本篇文章簡單介紹?OpenFeign?調(diào)用傳遞?header?,以及多線程環(huán)境下可能會出現(xiàn)的問題,其中涉及到?ThreadLocal?的相關(guān)知識,需要的朋友可以參考下2022-07-07

