手把手使用mybatis攔截器
-
-
-
- 前言
- mybatis攔截器的使用
-
- 1. 自定義攔截器類,實作Interceptor接口
- 2. 注冊自定義攔截器
-
-
有道雲筆記位址:文檔:手把手使用mybatis攔截器.md
連結:
http://note.youdao.com/noteshare?id=9b96839d8c951a6761c1f7807f598871&sub=96A6E86DD053414B82A80F4543DF0E35
前言
先不看什麼介紹、原理、以及詳細的底層執行全過程,先講使用,再說一些解析
mybatis攔截器的使用
1. 自定義攔截器類,實作Interceptor接口
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();
}
注意:當使用@Component注解注冊自定義攔截器後,觀察自定義攔截器類是否在spring掃描路徑中
可以看到,當我們使用postman調用某個查詢接口後,成功的列印出了接口所執行的sql語句
參考上面這個例子,我們debug解析一下該方法和參數
Invocation 類方法的一個封裝,在攔截器中就是被調用的目标方法
Object target//調用的對象
Method method//調用的方法
Object[] args//參數
關注args數組
MappedStatement:
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 :
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);
}
測試:
權限屏蔽
需要對原本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後跟着的字元串中找到主表,對主表做改造查詢包裹然後替換,測試:
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:
如圖,可以設定PlainSelect對象中的屬性去更改sql,夜深了,這裡就不往下寫了。
2. 注冊自定義攔截器
- 在@Configuration注解的類裡面,@Bean我們自定義的攔截器類
- 使用@Component注解,帶此注解的類看為元件,當使用基于注解的配置和類路徑掃描的時候,這些類就會被執行個體化。
3.一些攔截器的原理和過程詳細解析文檔可自行查找
比如我自己有道雲筆記上記錄的一些網址:
https://blog.csdn.net/wuyuxing24/article/details/89343951
https://blog.csdn.net/weixin_39494923/article/details/91534658