天天看點

手把手使用mybatis攔截器

手把手使用mybatis攔截器

        • 前言
        • mybatis攔截器的使用
          • 1. 自定義攔截器類,實作Interceptor接口
          • 2. 注冊自定義攔截器

有道雲筆記位址:文檔:手把手使用mybatis攔截器.md

連結:

http://note.youdao.com/noteshare?id=9b96839d8c951a6761c1f7807f598871&sub=96A6E86DD053414B82A80F4543DF0E35

前言

先不看什麼介紹、原理、以及詳細的底層執行全過程,先講使用,再說一些解析

mybatis攔截器的使用

1. 自定義攔截器類,實作Interceptor接口
手把手使用mybatis攔截器
package org.hzero.boot.platform.mybatis;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;

import java.util.Properties;

/**
 * @author [email protected] 2019-12-17 11:14:45
 */
@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}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class CustomizeInterceptor implements Interceptor {
    public static final Logger logger = LoggerFactory.getLogger(CustomizeInterceptor.class);
    @Value("${spring.application.name:application}")
    private String serviceName;

    /**
     * 代理對象每次調用的方法,就是要進行攔截的時候要執行的方法。在這個方法裡面做我們自定義的邏輯處理
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 自定義代碼邏輯

        return invocation.proceed();
    }

    /**
     * plugin方法是攔截器用于封裝目标對象的,通過該方法我們可以傳回目标對象本身,也可以傳回一個它的代理
     *
     * 當傳回的是代理的時候我們可以對其中的方法進行攔截來調用intercept方法 -- Plugin.wrap(target, this)
     * 當傳回的是目前對象的時候 就不會調用intercept方法,相當于目前攔截器無效
     */
    @Override
    public Object plugin(Object target) {
        // 傳回代理類
        return Plugin.wrap(target, this);
    }

    /**
     * 用于在Mybatis配置檔案中指定一些屬性的,注冊目前攔截器的時候可以設定一些屬性
     */
    @Override
    public void setProperties(Properties properties) {
        // unnecessary
    }
    
}

           

代碼解析:

  • @Intercepts注解:在實作Interceptor接口的類聲明,使該類注冊成為攔截器
  • @Signature注解:定義哪些類(4種),方法,參數需要被攔截
// ParameterHandler,ResultSetHandler,StatementHandler,Executor
 Class<?> type()
 // Executor對應有query、update
 String method() 
 // 重點關注args裡面的MappedStatement、BoundSql,可debug調試了解這些參數類
 Class<?>[] args() 
           
  • 其中,我們隻需要在intercept方中加上我們想要的攔截邏輯代碼

當攔截器注冊好(請觀看第二點:攔截器注冊)做些測試:

列印sql
@Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 自定義代碼邏輯
        // 擷取BoundSql
        BoundSql boundSql = SqlUtils.getBoundSql(invocation);
        if (boundSql == null) {
            return invocation.proceed();
        }
        System.out.println("列印sql:" + boundSql.getSql());
        return invocation.proceed();
    }
           
手把手使用mybatis攔截器

注意:當使用@Component注解注冊自定義攔截器後,觀察自定義攔截器類是否在spring掃描路徑中

可以看到,當我們使用postman調用某個查詢接口後,成功的列印出了接口所執行的sql語句

參考上面這個例子,我們debug解析一下該方法和參數

Invocation 類方法的一個封裝,在攔截器中就是被調用的目标方法

Object target//調用的對象
Method method//調用的方法
Object[] args//參數
           
手把手使用mybatis攔截器

關注args數組

MappedStatement:

手把手使用mybatis攔截器
MappedStatement類位于mybatis包的org.apache.ibatis.mapping目錄下,是一個final類型也就是說執行個體化之後就不允許改變
MappedStatement對象對應Mapper.xml配置檔案中的一個select/update/insert/delete節點,描述的就是一條SQL語句,屬性如下:

  private String resource;//mapper配置檔案名,如:UserMapper.xml
  private Configuration configuration;//全局配置
  private String id;//節點的id屬性加命名空間,如:com.lucky.mybatis.dao.UserMapper.selectByExample
  private Integer fetchSize;
  private Integer timeout;//逾時時間
  private StatementType statementType;//操作SQL的對象的類型
  private ResultSetType resultSetType;//結果類型
  private SqlSource sqlSource;//sql語句
  private Cache cache;//緩存
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;//是否使用緩存,預設為true
  private boolean resultOrdered;//結果是否排序
  private SqlCommandType sqlCommandType;//sql語句的類型,如select、update、delete、insert
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;//資料庫ID
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;

其中StatementType指操作SQL對象的類型,是個枚舉類型,值分别為:
STATEMENT(直接操作SQL,不進行預編譯),
PREPARED(預處理參數,進行預編譯,擷取資料),
CALLABLE(執行存儲過程)
ResultSetType指傳回結果集的類型,也是個枚舉類型,值分别為:
FORWARD_ONLY:結果集的遊标隻能向下滾動
SCROLL_INSENSITIVE:結果集的遊标可以上下移動,當資料庫變化時目前結果集不變
SCROLL_SENSITIVE:結果集客自由滾動,資料庫變化時目前結果集同步改變
           

BoundSql :

手把手使用mybatis攔截器
BoundSql boundSql =BoundSql SqlUtils.getBoundSql(invocation);
/**
     * 擷取BoundSql
     *
     * @param invocation
     * @return BoundSql
     */
    public static BoundSql getBoundSql(Invocation invocation) {
        if (invocation == null) {
            return null;
        } else {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            BoundSql boundSql;
            // 由于邏輯關系,隻會進入一次
            if (args.length == ARGS_LENGTH_FOUR || args.length == ARGS_LENGTH_TWO) {
                // 4 個參數時
                boundSql = ms.getBoundSql(parameter);
            } else {
                // 6 個參數時
                boundSql = (BoundSql) args[5];
            }
            return boundSql;
        }
    }
           

就是擷取invocation中的sql語句,可以自己寫,通過args中的MappedStatement或BoundSql對象擷取

分頁查詢

實作:

@Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 自定義代碼邏輯
        // 擷取BoundSql
        BoundSql boundSql = SqlUtils.getBoundSql(invocation);
        if (boundSql == null) {
            return invocation.proceed();
        }
        System.out.println("列印sql:" + boundSql.getSql());
        // 自定義分頁
        String newSql = pagination(boundSql);
        SystemMetaObject.forObject(boundSql).setValue("sql", newSql);

        return invocation.proceed();
    }
           
private String pagination(BoundSql boundSql) {
        // 擷取page、pageSize參數值
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (servletRequestAttributes == null) {
            return boundSql.getSql();
        }
        HttpServletRequest request = servletRequestAttributes.getRequest();

        // 判斷是否有page/pageSize參數
        int realPage = page, realPageSize = pageSize;
        try {
            realPage = Integer.parseInt(request.getParameter(PAGE));
        } catch (NumberFormatException e) {
            logger.info("沒有page參數");
        }
        try {
            realPageSize = Integer.parseInt(request.getParameter(PAGE_SIZE));
        } catch (NumberFormatException e) {
            logger.info("沒有pageSize參數");
        }
        if (realPage <= 0 || realPageSize < 0) {
            return boundSql.getSql();
        }

        return StringUtil.join(" ", boundSql.getSql(), "limit", (realPage-1) * realPageSize, ",", realPageSize);
    }
           

測試:

手把手使用mybatis攔截器
權限屏蔽

需要對原本sql進行改造,限制created_by字段為目前登入使用者的Id。

第一時間想到解析并拼接sql,在where條件中加created_by條件,發現行不通,當遇到取别名或者複雜sql(比如sql中包含多個where/limit/order by/group by等等或者說包了多層),這時候再去解析拼接成正确的sql太複雜了。

經過思考,通過解析拼接的思維再考慮執行play B:

找到主表,加上條件去替換

private String mineDoc(BoundSql boundSql) {
        // 擷取mine的值  當mine=1時隻查詢建立人是自己的資料
//        Long userId = DetailsHelper.getUserDetails().getUserId();
        // 假定目前登入使用者Id = 0
        long userId = 0L;
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (servletRequestAttributes == null) {
            return boundSql.getSql();
        }
        HttpServletRequest request = servletRequestAttributes.getRequest();

        // 判斷是否有mine參數
        int mine = 0;
        try {
            mine = Integer.parseInt(request.getParameter(MINE));
        } catch (NumberFormatException e) {
            logger.info("沒有mine參數");
        }
        if (mine == 1) {
            /*String oldSql = boundSql.getSql().toLowerCase();
            // 如果sql含有where
            if (oldSql.toLowerCase().contains("where")) {
                return oldSql.replace("where", "where created_by = " + userId + " and ");
            } else {
                // 如果原sql含有group by / order by / limit
                if (oldSql.replaceAll(" ","").contains("groupby")
                        || oldSql.replaceAll(" ","").contains("orderby")
                        || oldSql.replaceAll(" ","").contains("limit")) {
                    // 在這三個前加where條件
                } else {
                    // 直接加
                    return oldSql + " where created_by = " + userId;
                }
            }*/

            // play B :找到主表做替換
            String oldSql = boundSql.getSql().toLowerCase();
            // 找到最後一個from後跟着的sql
            String str = oldSql.split("from")[oldSql.split("from").length-1];
            // 找到主表,加上條件替換主表
            String tabel = str.trim().split(" ")[0];
            String newTable = "(" + "select * from " + tabel + " where created_by = " + userId + ")";
            return oldSql.replace(tabel,newTable);
        }
        return boundSql.getSql();
    }
           

通過原SQL中最後一個from後跟着的字元串中找到主表,對主表做改造查詢包裹然後替換,測試:

手把手使用mybatis攔截器
手把手使用mybatis攔截器

play C:

利用工具類,使操作更友善、全面

public Object intercept(Invocation invocation) throws Throwable {
        // 自定義代碼邏輯
        // 擷取BoundSql
        BoundSql boundSql = SqlUtils.getBoundSql(invocation);
        if (boundSql == null) {
            return invocation.proceed();
        }
        System.out.println("列印sql:" + boundSql.getSql());
        /*// 自定義分頁
        String newSql = pagination(boundSql);
        SystemMetaObject.forObject(boundSql).setValue("sql", newSql);
        // 權限屏蔽(比如隻能查詢建立人是自己的單據)
        String newSql2 = mineDoc(boundSql);
        SystemMetaObject.forObject(boundSql).setValue("sql", newSql2);*/

        // 權限屏蔽 play C
        Statement statement;
        try {
            statement = CCJSqlParserUtil.parse(boundSql.getSql());
            if (statement instanceof Select) {
                Select select = (Select) statement;
                if (select.getSelectBody() instanceof PlainSelect) {
                    PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
    
                    logger.info(plainSelect.toString());
                } else /* 複雜Sql(UNION,INTERSECT,MINUS,EXCEPT) */ if (select.getSelectBody() instanceof SetOperationList) {

                }
            }

        } catch (JSQLParserException e) {
            logger.error("Error parser sql.", e);
            return invocation.proceed();
        }
        return invocation.proceed();
    }
           

debug:

手把手使用mybatis攔截器

如圖,可以設定PlainSelect對象中的屬性去更改sql,夜深了,這裡就不往下寫了。

2. 注冊自定義攔截器
  • 在@Configuration注解的類裡面,@Bean我們自定義的攔截器類
    手把手使用mybatis攔截器
  • 使用@Component注解,帶此注解的類看為元件,當使用基于注解的配置和類路徑掃描的時候,這些類就會被執行個體化。
    手把手使用mybatis攔截器

3.一些攔截器的原理和過程詳細解析文檔可自行查找

比如我自己有道雲筆記上記錄的一些網址:

https://blog.csdn.net/wuyuxing24/article/details/89343951

https://blog.csdn.net/weixin_39494923/article/details/91534658