Mybatis插件開發


前言

MyBatis開放用戶實現自己的插件,從而對整個調用過程進行個性化擴展。

這是MyBatis整個調用流程的主要參與者。

我們可以對其中的一些過程進行攔截,添加自己的功能,比如重寫Sql添加分頁參數。

 

攔截的接口

MyBatis允許攔截的接口如下

Executor

public interface Executor {
    ResultHandler NO_RESULT_HANDLER = null;
    int update(MappedStatement var1, Object var2) throws SQLException;
    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;
    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
    <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;
    List<BatchResult> flushStatements() throws SQLException;
    void commit(boolean var1) throws SQLException;
    void rollback(boolean var1) throws SQLException;
    CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);
    boolean isCached(MappedStatement var1, CacheKey var2);
    void clearLocalCache();
    void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5);
    Transaction getTransaction();
    void close(boolean var1);
    boolean isClosed();
    void setExecutorWrapper(Executor var1);
}

 

ParameterHandler

public interface ParameterHandler {
    Object getParameterObject();

    void setParameters(PreparedStatement var1) throws SQLException;
}

 

ResultSetHandler

public interface ResultSetHandler {
    <E> List<E> handleResultSets(Statement var1) throws SQLException;

    <E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;

    void handleOutputParameters(CallableStatement var1) throws SQLException;
}

 

StatementHandler

public interface StatementHandler {
    Statement prepare(Connection var1, Integer var2) throws SQLException;
    void parameterize(Statement var1) throws SQLException;
    void batch(Statement var1) throws SQLException;
    int update(Statement var1) throws SQLException;
    <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;
    <E> Cursor<E> queryCursor(Statement var1) throws SQLException;
    BoundSql getBoundSql();
    ParameterHandler getParameterHandler();
}

 

只要攔截器定義了攔截的接口和方法,后續調用該方法時,將會被攔截。

攔截器實現

如果要實現自己的攔截器,需要實現接口Interceptor


@Slf4j
@Intercepts(@Signature(type = Executor.class,
        method ="update",
        args ={MappedStatement.class,Object.class} ))
public class MyIntercetor implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        log.info("MyIntercetor ...");

        Object result = invocation.proceed();

        log.info("result = " + result);

        return result;
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o,this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

 

 

1. 攔截方法配置

Intercepts,Signature

public @interface Intercepts {
    Signature[] value();
}
public @interface Signature {
Class<?> type();

String method();

Class<?>[] args();
}
 

 

配置

@Intercepts(@Signature(type = Executor.class,
        method ="update",
        args ={MappedStatement.class,Object.class} ))

 

我們知道Java中方法的簽名包括所在的類,方法名稱,入參。 

@Signature定義方法簽名

type:攔截的接口,為上節定義的四個接口

method:攔截的接口方法

args:參數類型列表,需要和方法中定義的順序一致。

 也可以配置多個

@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})

 

2. intercept(Invocation invocation)

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 this.target;
}

public Method getMethod() {
return this.method;
}

public Object[] getArgs() {
return this.args;
}

public Object proceed() throws InvocationTargetException, IllegalAccessException {
return this.method.invoke(this.target, this.args);
}
}

 

通過Invocation可以獲取到被攔截的方法的調用對象,方法,參數。

proceed()用於繼續執行並獲得最終的結果。

這里使用了設計模式中的責任鏈模式。

 

3.這里不能返回null。

用於給被攔截的對象生成一個代理對象,並返回它。

@Override
    public Object plugin(Object o) {
        return Plugin.wrap(o,this);
    }

 可以看下wrap方法,其實現了JDK的接口InvocationHandler,也就是為傳入的target創建了一個代理對象。這里使用了JDK動態代理方式。也可以自己實現其他代理方式,比如cglib.

    public class Plugin implements InvocationHandler {
    private final Object target;
    private final Interceptor interceptor;
    private final Map<Class<?>, Set<Method>> 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);
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

  
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}

}

由於使用了動態代理,方法執行時,將會被調用invoke方法,會先判斷是否設置了攔截器:methods != null && methods.contains(method),

如果設置了攔截器,則調用攔截器this.interceptor.intercept(new Invocation(this.target, method, args))

否則直接調用method.invoke(this.target, args);

 

 

4.攔截器在執行前輸出"MyIntercetor ...",在數據庫操作返回后輸出"result =xxx"

       log.info("MyIntercetor ...");

        Object result = invocation.proceed();

        log.info("result = " + result);

 

插件實現完成!

 

測試

在Spring中引入很簡單。

第一種方式:

創建攔截器的bean

@Slf4j
@Configuration
public class IntercetorConfiguration {

    @Bean
    public MyIntercetor myIntercetor(){
        return new MyIntercetor();
    }

}

注意第一種方式和第二種方式僅適用於SpringBoot應用,並且引入以下依賴

<dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.2</version>
</dependency>

第二種方式

手動往Configuration中添加攔截器。

@Slf4j
@Configuration
public class IntercetorConfiguration {
  @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @PostConstruct
    public void addPageInterceptor() {
        MyIntercetor interceptor = new MyIntercetor();

        Iterator var3 = this.sqlSessionFactoryList.iterator();

        while(var3.hasNext()) {
            SqlSessionFactory sqlSessionFactory = (SqlSessionFactory)var3.next();
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }

    }
}

 第三種方式

如果是純Spring應用,可在mybatis配置文件中配置

<plugins>
    <plugin   intercetor="xxx.xxx.MyIntercetor">
            <property   name="xxx"  value="xxx">
    </plugin>
</plugins>

 

由於上面定義的攔截器是攔截Executor的update方法,所以在執行insert,update,delete的操作時,將會被攔截。

本例子使用insert來測試。具體代碼查看:GitHub

2019-06-10 16:08:03.109  INFO 20410 --- [nio-8110-exec-1] c.m.user.dao.intercetor.MyIntercetor     : MyIntercetor ...

2019-06-10 16:08:03.166  INFO 20410 --- [nio-8110-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2019-06-10 16:08:03.267 DEBUG 20410 --- [nio-8110-exec-1] o.m.s.t.SpringManagedTransaction         : JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5cb1c36e] will not be managed by Spring
2019-06-10 16:08:03.274 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList   : ==>  Preparing: insert into user (name) values (?) , (?) , (?) 
2019-06-10 16:08:03.307 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList   : ==> Parameters: name:58(String), name:64(String), name:69(String)
2019-06-10 16:08:03.355 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList   : <==    Updates: 3
2019-06-10 16:08:03.358 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey         : ==>  Preparing: SELECT LAST_INSERT_ID() 
2019-06-10 16:08:03.358 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey         : ==> Parameters: 
2019-06-10 16:08:03.380 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey         : <==      Total: 1

2019-06-10 16:08:03.381 INFO 20410 --- [nio-8110-exec-1] c.m.user.dao.intercetor.MyIntercetor : result = 3

 

可以看到攔截器被調用了。

簡單的分頁插件實現

這里攔截StatementHandler的prepare方法,也就是SQL語句預編譯之前進行SQL改寫。

@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PageIntercetor implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        log.info("StatementHandler  prepare ...");

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

        ParameterHandler parameterHandler = statementHandler.getParameterHandler();
        BoundSql boundSql = statementHandler.getBoundSql();
        //獲取到原始sql語句
        String sql = boundSql.getSql();
        String mSql = sql + " limit 0,1";
        //通過反射修改sql語句
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, mSql);

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        //此處可以接收到配置文件的property參數
        System.out.println(properties.getProperty("name"));
    }

}

分頁插件實現的難點在於當使用不同的Statement時,執行流程是不一樣的。

Statement需要定義statementType="STATEMENT",這個時候SQL語句不需要進行預編譯處理,參數是與xml中配飾的SQL語句拼接在一起的。

<select id="select" resultMap="BaseResultMap" statementType="STATEMENT">
select id, name
from user
where
name = '${name}'
</select>

 

而當使用PreparedStatement時需要定義statementType="PREPARED",這個時候SQL語句需要進行預編譯處理。CallableStatement(用於調用存儲過程)同理。

  <select id="select" resultMap="BaseResultMap" statementType="PREPARED">
    select id, name
    from user
    where
      name = #{name}
  </select>

 

因此需要考慮不同情況下的SQL改寫。

雖然Mybatis給我們實現了分頁,只要在接口上傳入RowBounds參數,即可實現分頁。

但是這個是內存分頁。也就是把所有的數據都讀到應用內存中,再進行分頁。造成了許多無效的讀取。

 

當然也沒必要搞的這么復雜!可以在mapper.xml中直接添加limit.

需要注意的是limit的參數的數據量不同,那么效率是不一樣的,需要進行相關的優化。

 

結束!!!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM