mybatis-plus的多租戶不同版本實現(xiàn)的兩種方式
Mybatis plus 3.4.0后的拓展插件
在mybatis- plus 3.4.0 版本之后可以,官方提供了MybatisPlusInterceptor 拓展插件
該插件是核心插件,目前代理了 Executor#query 和 Executor#update 和 StatementHandler#prepare 方法
InnerInterceptor
我們提供的插件都將基于此接口來實現(xiàn)功能
目前已有的功能:
- 自動分頁: PaginationInnerInterceptor
- 多租戶: TenantLineInnerInterceptor
- 動態(tài)表名: DynamicTableNameInnerInterceptor
- 樂觀鎖: OptimisticLockerInnerInterceptor
- sql 性能規(guī)范: IllegalSQLInnerInterceptor
- 防止全表更新與刪除: BlockAttackInnerInterceptor
注意:
使用多個功能需要注意順序關(guān)系,建議使用如下順序
- 多租戶,動態(tài)表名
- 分頁,樂觀鎖
- sql 性能規(guī)范,防止全表更新與刪除
總結(jié): 對 sql 進行單次改造的優(yōu)先放入,不對 sql 進行改造的最后放入
如果是mybatis plus 3.4.0 之后的版本可以直接使用多租戶插件
官方示例:
package com.baomidou.mybatisplus.samples.tenant.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; /** * @author miemie * @since 2018-08-10 */ @Configuration @MapperScan("com.baomidou.mybatisplus.samples.tenant.mapper") public class MybatisPlusConfig { /** * 新多租戶插件配置,一緩和二緩遵循mybatis的規(guī)則,需要設(shè)置 MybatisConfiguration#useDeprecatedExecutor = false 避免緩存萬一出現(xiàn)問題 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { @Override public Expression getTenantId() { // 這里可以寫自己系統(tǒng)的獲取租戶id的方法 比如下面自定義方法 // return new LongValue(CurrentUserUtils.getTenantId()); return new LongValue(1); } /** * 獲取租戶字段名 * <p> * 默認字段名叫: tenant_id * * @return 租戶字段名 */ @Override default String getTenantIdColumn() { // 如果該字段你不是固定的,請使用 SqlInjectionUtils.check 檢查安全性 return "tenant_id"; } /** * 根據(jù)表名判斷是否忽略拼接多租戶條件 * <p> * 默認都要進行解析并拼接多租戶條件 * * @param tableName 表名 * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租戶條件 */ @Override public boolean ignoreTable(String tableName) { String[] arr = new String[]{ "susCode", "suName", "suSex", "suAge" }; return ArrayUtil.contains(arr, tableName); } })); // 如果用了分頁插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor // 用了分頁插件必須設(shè)置 MybatisConfiguration#useDeprecatedExecutor = false // interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } // @Bean // public ConfigurationCustomizer configurationCustomizer() { // return configuration -> configuration.setUseDeprecatedExecutor(false); // } }
Mybatis plus 3.4.0 之前版本的自定義實現(xiàn)
因為本次開發(fā)的模塊需要與系統(tǒng)的老版本兼容,使用的版本為3.1.1則出現(xiàn)了不能夠使用官方自帶的增強插件,所以需要自己通過實現(xiàn)攔截器來達到類似效果。
寫的過程可以借鑒分頁插件PaginationInterceptor
自定義Interceptor 繼承AbstractSqlPAserHandler(SQL 解析處理器) 實現(xiàn)Inteceptor(攔截器)
攔截時機與PaginationInterceptor 一樣,在StatementHandler的prepare 進行處理 照搬即可
這個 Interceptor 接口定義了三個方法:
- intercept(Invocation invocation):這是攔截器的核心方法,它允許攔截器在執(zhí)行目標(biāo)方法之前或之后添加自定義邏輯。當(dāng)攔截器被激活時,intercept 方法會被調(diào)用。攔截器可以通過 Invocation 對象訪問目標(biāo)方法的參數(shù)、目標(biāo)對象等信息,并且可以通過調(diào)用 invocation.proceed() 來繼續(xù)執(zhí)行目標(biāo)方法,或者在此之前/之后添加自定義邏輯。
- plugin(Object target):這個方法用于包裝目標(biāo)對象,返回一個代理對象。攔截器通過調(diào)用此方法來生成一個目標(biāo)對象的代理,代理對象中包含了攔截器的邏輯。這樣,在調(diào)用目標(biāo)對象的方法時,攔截器的邏輯就會被觸發(fā)。
- setProperties(Properties properties):這個方法用于設(shè)置攔截器的屬性。攔截器可以通過這個方法接收外部傳入的配置參數(shù),以便在運行時動態(tài)調(diào)整其行為。
這三個方法結(jié)合起來,允許開發(fā)者在 MyBatis 中實現(xiàn)自定義的攔截器邏輯,例如添加日志記錄、權(quán)限控制、性能監(jiān)控等功能。
這里plugin()和setProperties()照搬即可
主要對intercept 進行處理
package com.panpass.rebate.service.config; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ReflectUtil; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.toolkit.PluginUtils; import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler; import com.panpass.rebate.service.utils.CurrentRebateUserUtil; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ItemsList; import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.select.*; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.statement.BaseStatementHandler; import org.apache.ibatis.executor.statement.PreparedStatementHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.junit.jupiter.api.Order; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.sql.Connection; import java.util.ArrayList; import java.util.List; import java.util.Properties; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.update.Update; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; /** * @Description: * @Author: potato * @Date: 2024/4/8 9:49 */ @Slf4j @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) //@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class DataScopeInterceptor extends AbstractSqlParserHandler implements Interceptor { /** * 是否對xml中SQL 進行多租戶增強 * false 不進行SQL增強 * true 進行SQL增強 * 配置 可轉(zhuǎn)移到nacos 中 */ private boolean openIngore = true; /** * 可以指定某些特定的XML中的SQL的方法進行手寫,不通過該攔截器增強 */ private List<String> ingoreList; @Override public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); //確保只有攔截的目標(biāo)對象是 StatementHandler 類型時才執(zhí)行特定邏輯 boolean flag = true; if (target instanceof StatementHandler) { StatementHandler statementHandler = (StatementHandler) target; StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(statementHandler, "delegate"); // true 則需要SQL多租戶增強 try { flag = isIngoreXML(invocation); } catch (Exception e) { log.info("StatementHandler 解析出現(xiàn)問題,請注意查看"+e.getMessage()); // e.printStackTrace(); } if (flag) { // 獲取 BoundSql 對象,包含原始 SQL 語句 BoundSql boundSql = statementHandler.getBoundSql(); String originalSql = boundSql.getSql(); String newSql = setEnvToStatement(originalSql); // 使用MetaObject對象將新的SQL語句設(shè)置到BoundSql對象中 MetaObject metaObject = SystemMetaObject.forObject(boundSql); metaObject.setValue("sql", newSql); } } // 執(zhí)行SQL return invocation.proceed(); } /** * * 根據(jù)xml名稱判斷是否需要進行xml 中SQL解析 可以拓展 * * @param invocation * @return fasle = 不進行SQL增強 true 進行SQL增強 */ public boolean isIngoreXML(Invocation invocation){ // 不開啟增強直接返回false if(!openIngore){ return false; } // 存在多層代理,SystemMetaObject獲取不確定, 借鑒PaginationInterceptor 中獲取Target的方法來獲取 StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); // SQL 解析 // this.sqlParser(metaObject); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); // ingoreList 為空,沒有設(shè)置需要忽略增強的方法名 模擬存在配置了忽略的方法 // ingoreList =new ArrayList<>(); // ingoreList.add("RebateFlowBulkMapper.flowList"); if(CollectionUtil.isEmpty(ingoreList)){ return true; } // Resource 示例 : file [D:\bjzx\project\rebate\rebate-dao\target\classes\mapper\plan\PlanConfigMapper.xml] xml路徑 // id 示例 : com.panpass.rebate.plan.persistent.mapper.PlanConfigMapper.getPageList // 確保Resource 來自xml,并且配置的存在忽略的方法名例如: PlanConfigMapper.getPageList,返回false 不進行增強處理; for (String menthodName : ingoreList) { if (mappedStatement.getResource().contains("xml") && mappedStatement.getId().contains(menthodName)) { return false; } } return true; } public static void printFields(Object obj) { Class clazz = obj.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); String fieldName = field.getName(); Object fieldValue = null; try { fieldValue = field.get(obj); } catch (IllegalAccessException e) { e.printStackTrace(); } System.out.println(fieldName + ": " + fieldValue); } } private String setEnvToStatement(String originalSql) { net.sf.jsqlparser.statement.Statement statement; try { statement = CCJSqlParserUtil.parse(originalSql); } catch (JSQLParserException e) { throw new RuntimeException("EnvironmentVariableInterceptor::SQL語句解析異常:"+originalSql); } if (statement instanceof Select) { //TODO需要遞歸處理 Select select = (Select) statement; PlainSelect selectBody = (PlainSelect) select.getSelectBody(); // 遞歸處理子查詢 processSubQuery(selectBody); return select.toString(); } // else if (statement instanceof Insert) { // Insert insert = (Insert) statement; // setEnvToInsert(insert); // // return insert.toString(); // } else if (statement instanceof Update) { // Update update = (Update) statement; // Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),null); // update.setWhere(newWhereExpression); // // return update.toString(); // } else if (statement instanceof Delete) { // Delete delete = (Delete) statement; // Expression newWhereExpression = setEnvToWhereExpression(delete.getWhere(),null); // delete.setWhere(newWhereExpression); // // return delete.toString(); // } return originalSql; } private void processSubQuery(PlainSelect selectBody ) { if(selectBody == null){ return; } if (selectBody.getFromItem() instanceof Table) { Expression newWhereExpression; if (selectBody.getJoins() == null || selectBody.getJoins().isEmpty()) { newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), null); } else { newWhereExpression = multipleTableJoinWhereExpression(selectBody); } selectBody.setWhere(newWhereExpression); } else { // 處理子查詢 SelectBody subSelectBody = ((SubSelect) selectBody.getFromItem()).getSelectBody(); processSubQuery((PlainSelect) subSelectBody); // selectBody.setFromItem((FromItem) processSubQuery((PlainSelect) subSelectBody)); } // return selectBody; } //非遞查詢 private String setEnvToStatement2(String originalSql) { net.sf.jsqlparser.statement.Statement statement; try { statement = CCJSqlParserUtil.parse(originalSql); } catch (JSQLParserException e) { throw new RuntimeException("EnvironmentVariableInterceptor::SQL語句解析異常:"+originalSql); } if (statement instanceof Select) { Select select = (Select) statement; PlainSelect selectBody = (PlainSelect) select.getSelectBody(); if (selectBody.getFromItem() instanceof Table) { Expression newWhereExpression; if (selectBody.getJoins() == null || selectBody.getJoins().isEmpty()) { newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), null); } else { // 如果是多表關(guān)聯(lián)查詢,在關(guān)聯(lián)查詢中新增每個表的環(huán)境變量條件 newWhereExpression = multipleTableJoinWhereExpression(selectBody); } // 將新的where設(shè)置到Select中 selectBody.setWhere(newWhereExpression); } else if (selectBody.getFromItem() instanceof SubSelect) { // 如果是子查詢,在子查詢中新增環(huán)境變量條件 // 當(dāng)前方法只能處理單層子查詢,如果有多層級的子查詢的場景需要通過遞歸設(shè)置環(huán)境變量 SubSelect subSelect = (SubSelect) selectBody.getFromItem(); PlainSelect subSelectBody = (PlainSelect) subSelect.getSelectBody(); Expression newWhereExpression = setEnvToWhereExpression(subSelectBody.getWhere(), null); subSelectBody.setWhere(newWhereExpression); } // 獲得修改后的語句 return select.toString(); } else if (statement instanceof Insert) { Insert insert = (Insert) statement; setEnvToInsert(insert); return insert.toString(); } else if (statement instanceof Update) { Update update = (Update) statement; Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),null); // 將新的where設(shè)置到Update中 update.setWhere(newWhereExpression); return update.toString(); } else if (statement instanceof Delete) { Delete delete = (Delete) statement; Expression newWhereExpression = setEnvToWhereExpression(delete.getWhere(),null); // 將新的where設(shè)置到delete中 delete.setWhere(newWhereExpression); return delete.toString(); } return originalSql; } /** * 將需要隔離的字段加入到SQL的Where語法樹中 * @param whereExpression SQL的Where語法樹 * @param alias 表別名 * @return 新的SQL Where語法樹 */ private Expression setEnvToWhereExpression(Expression whereExpression, String alias) { // 添加SQL語法樹的一個where分支,并添加環(huán)境變量條件 EqualsTo envEquals = new EqualsTo(); envEquals.setLeftExpression(new Column(StringUtils.isNotBlank(alias) ? String.format("%s.tenant_id", alias) : "tenant_id")); envEquals.setRightExpression(new LongValue(CurrentRebateUserUtil.getTenantId() == null ? 1 :CurrentRebateUserUtil.getTenantId())); if (whereExpression == null){ return envEquals; } else { AndExpression andExpression = new AndExpression(whereExpression,envEquals); // 將新的where條件加入到原where條件的右分支樹 andExpression.setRightExpression(envEquals); andExpression.setLeftExpression(whereExpression); return andExpression; } } /** * 多表關(guān)聯(lián)查詢時,給關(guān)聯(lián)的所有表加入環(huán)境隔離條件 * @param selectBody select語法樹 * @return 新的SQL Where語法樹 */ private Expression multipleTableJoinWhereExpression(PlainSelect selectBody){ Table mainTable = (Table) selectBody.getFromItem(); String mainTableAlias = mainTable.getAlias().getName(); // 將 t1.tenant_id = tenant_id 的條件添加到where中 Expression newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), mainTableAlias); List<Join> joins = selectBody.getJoins(); for (Join join : joins) { FromItem joinRightItem = join.getRightItem(); if (joinRightItem instanceof Table) { Table joinTable = (Table) joinRightItem; String joinTableAlias = joinTable.getAlias().getName(); // 將每一個join的 tx.env = ENV 的條件添加到where中 newWhereExpression = setEnvToWhereExpression(newWhereExpression, joinTableAlias); } } return newWhereExpression; } /** * 新增數(shù)據(jù)時,插入tenant_id字段 * @param insert Insert 語法樹 */ private void setEnvToInsert(Insert insert) { // 添加tenant_id列 List<Column> columns = insert.getColumns(); for (Column column : columns) { //若存在,不進行處理 if (column.getColumnName().equals("tenant_id")) { return; } } columns.add(new Column("tenant_id")); // values中添加環(huán)境變量值 // 獲取插入值列表 ItemsList itemsList = insert.getItemsList(); if(itemsList instanceof MultiExpressionList ){ List<ExpressionList> exprList = ((MultiExpressionList) itemsList).getExprList(); for (ExpressionList expressionList : exprList) { expressionList.getExpressions().add(new LongValue(CurrentRebateUserUtil.getTenantId() == null ? 1 :CurrentRebateUserUtil.getTenantId())); } } else if (itemsList instanceof SubSelect) { // 處理子查詢 log.info("子查詢插入語句業(yè)務(wù)"); } } //測試邏輯使用的 public static void main(String[] args) throws JSQLParserException { String slectSql = "select * from (select id from (select 1 from table3 where id =2))"; // String s = setEnvToStatement(slectSql); String sql = "INSERT INTO my_table (column1, column2) VALUES (value1, value2), (value3, value4)"; try { // 解析 INSERT 語句 Statement statement = CCJSqlParserUtil.parse(sql); // 判斷是否是 INSERT 語句 if (statement instanceof Insert) { Insert insert = (Insert) statement; List<Column> columns = insert.getColumns(); columns.add(new Column("tenant_id")); // 獲取插入值列表 ItemsList itemsList = insert.getItemsList(); if(itemsList instanceof MultiExpressionList ){ itemsList =(MultiExpressionList)itemsList; List<ExpressionList> exprList = ((MultiExpressionList) itemsList).getExprList(); for (ExpressionList expressionList : exprList) { expressionList.getExpressions().add(new LongValue(CurrentRebateUserUtil.getTenantId() == null ? 1 :CurrentRebateUserUtil.getTenantId())); } } else if (itemsList instanceof SubSelect) { // 處理子查詢 log.info("子查詢插入語句使用"); } } } catch (JSQLParserException e) { e.printStackTrace(); } } /** * 生成攔截對象的代理 * * @param target 目標(biāo)對象 * @return 代理對象 */ @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } /** * mybatis配置的屬性 * * @param properties mybatis配置的屬性 */ @Override public void setProperties(Properties properties) { } }
自對其邏輯進行修改適配自己系統(tǒng)項目,實現(xiàn)了遞歸支持多級子查詢,對部分xml,方法級別 的SQL進行忽略增強。
注冊攔截器:
package com.panpass.rebate.service.config; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.util.List; /** * @Description: * @Author: potato * @Date: 2024/4/3 13:49 */ @Configuration public class MyBatisConfig { @Autowired private List<SqlSessionFactory> sqlSessionFactoryList; // 只執(zhí)行一次 @PostConstruct public void addDefaultTimeInterceptor() { /** * Mybatis攔截器可以使用@Component注解也可以在這里進行配置 * 在這里配置可以控制攔截器的執(zhí)行順序,所以注意去掉@Component注解 */ for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) { org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration(); List<Interceptor> interceptors = configuration.getInterceptors(); // 最后添加的會更早執(zhí)行 configuration.addInterceptor(new PaginationInterceptor()); configuration.addInterceptor(new DataScopeInterceptor()); } } }
攔截器不用@Component 注解,否則會被自動配置掃描進攔截,導(dǎo)致存在多個攔截器,
查看項目攔截器順序可以在InterceptorChain對象查看
或者org.apache.ibatis.session.Configuration 對象中的 interceptorChain 對象
到此這篇關(guān)于mybatis-plus的多租戶不同版本實現(xiàn)的兩種方式的文章就介紹到這了,更多相關(guān)mybatis-plus多租戶不同版本內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實現(xiàn)登錄攔截器超詳細教程分享
對于管理系統(tǒng)或其他需要用戶登錄的系統(tǒng),登錄驗證都是必不可少的環(huán)節(jié),尤其在?SpringBoot?開發(fā)的項目中。本文為大家準(zhǔn)備了超詳細的SpringBoot實現(xiàn)登錄攔截器方法,快收藏一波吧2023-02-02SpringBoot整合Mybatis-plus關(guān)鍵詞模糊查詢結(jié)果為空
SpringBoot整合Mybatis-plus使用關(guān)鍵詞模糊查詢的時候,數(shù)據(jù)庫中有數(shù)據(jù),但是無法查找出來,本文就來介紹一下SpringBoot整合Mybatis-plus關(guān)鍵詞模糊查詢結(jié)果為空的解決方法2025-04-04IDEA的Mybatis Log Plugin插件配置和使用詳解
這篇文章主要介紹了IDEA的Mybatis Log Plugin插件配置和使用,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09RocketMQ?ConsumeQueue與IndexFile實時更新機制源碼解析
這篇文章主要為大家介紹了RocketMQ?ConsumeQueue與IndexFile實時更新機制源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05Java實現(xiàn)聯(lián)系人管理系統(tǒng)
這篇文章主要為大家詳細介紹了Java實現(xiàn)聯(lián)系人管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02