mybatis-plus的多租戶不同版本實(shí)現(xiàn)的兩種方式
Mybatis plus 3.4.0后的拓展插件
在mybatis- plus 3.4.0 版本之后可以,官方提供了MybatisPlusInterceptor 拓展插件
該插件是核心插件,目前代理了 Executor#query 和 Executor#update 和 StatementHandler#prepare 方法
InnerInterceptor
我們提供的插件都將基于此接口來實(shí)現(xiàn)功能
目前已有的功能:
- 自動(dòng)分頁: PaginationInnerInterceptor
- 多租戶: TenantLineInnerInterceptor
- 動(dòng)態(tài)表名: DynamicTableNameInnerInterceptor
- 樂觀鎖: OptimisticLockerInnerInterceptor
- sql 性能規(guī)范: IllegalSQLInnerInterceptor
- 防止全表更新與刪除: BlockAttackInnerInterceptor
注意:
使用多個(gè)功能需要注意順序關(guān)系,建議使用如下順序
- 多租戶,動(dòng)態(tài)表名
- 分頁,樂觀鎖
- sql 性能規(guī)范,防止全表更新與刪除
總結(jié): 對(duì) sql 進(jìn)行單次改造的優(yōu)先放入,不對(duì) sql 進(jìn)行改造的最后放入
如果是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>
* 默認(rèn)字段名叫: tenant_id
*
* @return 租戶字段名
*/
@Override
default String getTenantIdColumn() {
// 如果該字段你不是固定的,請(qǐng)使用 SqlInjectionUtils.check 檢查安全性
return "tenant_id";
}
/**
* 根據(jù)表名判斷是否忽略拼接多租戶條件
* <p>
* 默認(rèn)都要進(jìn)行解析并拼接多租戶條件
*
* @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 之前版本的自定義實(shí)現(xiàn)
因?yàn)楸敬伍_發(fā)的模塊需要與系統(tǒng)的老版本兼容,使用的版本為3.1.1則出現(xiàn)了不能夠使用官方自帶的增強(qiáng)插件,所以需要自己通過實(shí)現(xiàn)攔截器來達(dá)到類似效果。
寫的過程可以借鑒分頁插件PaginationInterceptor


自定義Interceptor 繼承AbstractSqlPAserHandler(SQL 解析處理器) 實(shí)現(xiàn)Inteceptor(攔截器)
攔截時(shí)機(jī)與PaginationInterceptor 一樣,在StatementHandler的prepare 進(jìn)行處理 照搬即可

這個(gè) Interceptor 接口定義了三個(gè)方法:
- intercept(Invocation invocation):這是攔截器的核心方法,它允許攔截器在執(zhí)行目標(biāo)方法之前或之后添加自定義邏輯。當(dāng)攔截器被激活時(shí),intercept 方法會(huì)被調(diào)用。攔截器可以通過 Invocation 對(duì)象訪問目標(biāo)方法的參數(shù)、目標(biāo)對(duì)象等信息,并且可以通過調(diào)用 invocation.proceed() 來繼續(xù)執(zhí)行目標(biāo)方法,或者在此之前/之后添加自定義邏輯。
- plugin(Object target):這個(gè)方法用于包裝目標(biāo)對(duì)象,返回一個(gè)代理對(duì)象。攔截器通過調(diào)用此方法來生成一個(gè)目標(biāo)對(duì)象的代理,代理對(duì)象中包含了攔截器的邏輯。這樣,在調(diào)用目標(biāo)對(duì)象的方法時(shí),攔截器的邏輯就會(huì)被觸發(fā)。
- setProperties(Properties properties):這個(gè)方法用于設(shè)置攔截器的屬性。攔截器可以通過這個(gè)方法接收外部傳入的配置參數(shù),以便在運(yùn)行時(shí)動(dòng)態(tài)調(diào)整其行為。
這三個(gè)方法結(jié)合起來,允許開發(fā)者在 MyBatis 中實(shí)現(xiàn)自定義的攔截器邏輯,例如添加日志記錄、權(quán)限控制、性能監(jiān)控等功能。
這里plugin()和setProperties()照搬即可
主要對(duì)intercept 進(jìn)行處理
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 {
/**
* 是否對(duì)xml中SQL 進(jìn)行多租戶增強(qiáng)
* false 不進(jìn)行SQL增強(qiáng)
* true 進(jìn)行SQL增強(qiáng)
* 配置 可轉(zhuǎn)移到nacos 中
*/
private boolean openIngore = true;
/**
* 可以指定某些特定的XML中的SQL的方法進(jìn)行手寫,不通過該攔截器增強(qiáng)
*/
private List<String> ingoreList;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
//確保只有攔截的目標(biāo)對(duì)象是 StatementHandler 類型時(shí)才執(zhí)行特定邏輯
boolean flag = true;
if (target instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) target;
StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(statementHandler, "delegate");
// true 則需要SQL多租戶增強(qiáng)
try {
flag = isIngoreXML(invocation);
} catch (Exception e) {
log.info("StatementHandler 解析出現(xiàn)問題,請(qǐng)注意查看"+e.getMessage());
// e.printStackTrace();
}
if (flag) {
// 獲取 BoundSql 對(duì)象,包含原始 SQL 語句
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
String newSql = setEnvToStatement(originalSql);
// 使用MetaObject對(duì)象將新的SQL語句設(shè)置到BoundSql對(duì)象中
MetaObject metaObject = SystemMetaObject.forObject(boundSql);
metaObject.setValue("sql", newSql);
}
}
// 執(zhí)行SQL
return invocation.proceed();
}
/**
*
* 根據(jù)xml名稱判斷是否需要進(jìn)行xml 中SQL解析 可以拓展
*
* @param invocation
* @return fasle = 不進(jìn)行SQL增強(qiáng) true 進(jìn)行SQL增強(qiáng)
*/
public boolean isIngoreXML(Invocation invocation){
// 不開啟增強(qiáng)直接返回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è)置需要忽略增強(qiáng)的方法名 模擬存在配置了忽略的方法
// 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 不進(jìn)行增強(qiáng)處理;
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)查詢中新增每個(gè)表的環(huán)境變量條件
newWhereExpression = multipleTableJoinWhereExpression(selectBody);
}
// 將新的where設(shè)置到Select中
selectBody.setWhere(newWhereExpression);
} else if (selectBody.getFromItem() instanceof SubSelect) {
// 如果是子查詢,在子查詢中新增環(huán)境變量條件
// 當(dāng)前方法只能處理單層子查詢,如果有多層級(jí)的子查詢的場(chǎ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語法樹的一個(gè)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)查詢時(shí),給關(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();
// 將每一個(gè)join的 tx.env = ENV 的條件添加到where中
newWhereExpression = setEnvToWhereExpression(newWhereExpression, joinTableAlias);
}
}
return newWhereExpression;
}
/**
* 新增數(shù)據(jù)時(shí),插入tenant_id字段
* @param insert Insert 語法樹
*/
private void setEnvToInsert(Insert insert) {
// 添加tenant_id列
List<Column> columns = insert.getColumns();
for (Column column : columns) {
//若存在,不進(jìn)行處理
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ù)");
}
}
//測(cè)試邏輯使用的
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();
}
}
/**
* 生成攔截對(duì)象的代理
*
* @param target 目標(biāo)對(duì)象
* @return 代理對(duì)象
*/
@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) {
}
}自對(duì)其邏輯進(jìn)行修改適配自己系統(tǒng)項(xiàng)目,實(shí)現(xiàn)了遞歸支持多級(jí)子查詢,對(duì)部分xml,方法級(jí)別 的SQL進(jìn)行忽略增強(qiáng)。
注冊(cè)攔截器:
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注解也可以在這里進(jìn)行配置
* 在這里配置可以控制攔截器的執(zhí)行順序,所以注意去掉@Component注解
*/
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
List<Interceptor> interceptors = configuration.getInterceptors();
// 最后添加的會(huì)更早執(zhí)行
configuration.addInterceptor(new PaginationInterceptor());
configuration.addInterceptor(new DataScopeInterceptor());
}
}
}攔截器不用@Component 注解,否則會(huì)被自動(dòng)配置掃描進(jìn)攔截,導(dǎo)致存在多個(gè)攔截器,
查看項(xiàng)目攔截器順序可以在InterceptorChain對(duì)象查看

或者org.apache.ibatis.session.Configuration 對(duì)象中的 interceptorChain 對(duì)象
到此這篇關(guān)于mybatis-plus的多租戶不同版本實(shí)現(xiàn)的兩種方式的文章就介紹到這了,更多相關(guān)mybatis-plus多租戶不同版本內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解基于MVC的數(shù)據(jù)查詢模塊進(jìn)行模糊查詢
這篇文章主要介紹了Java基于MVC的數(shù)據(jù)查詢模塊進(jìn)行模糊查詢,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
詳解Spring AOP 實(shí)現(xiàn)主從讀寫分離
本篇文章主要介紹了Spring AOP 實(shí)現(xiàn)主從讀寫分離,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-03-03
SpringBoot實(shí)現(xiàn)登錄攔截器超詳細(xì)教程分享
對(duì)于管理系統(tǒng)或其他需要用戶登錄的系統(tǒng),登錄驗(yàn)證都是必不可少的環(huán)節(jié),尤其在?SpringBoot?開發(fā)的項(xiàng)目中。本文為大家準(zhǔn)備了超詳細(xì)的SpringBoot實(shí)現(xiàn)登錄攔截器方法,快收藏一波吧2023-02-02
SpringBoot整合Mybatis-plus關(guān)鍵詞模糊查詢結(jié)果為空
SpringBoot整合Mybatis-plus使用關(guān)鍵詞模糊查詢的時(shí)候,數(shù)據(jù)庫中有數(shù)據(jù),但是無法查找出來,本文就來介紹一下SpringBoot整合Mybatis-plus關(guān)鍵詞模糊查詢結(jié)果為空的解決方法2025-04-04
IDEA的Mybatis Log Plugin插件配置和使用詳解
這篇文章主要介紹了IDEA的Mybatis Log Plugin插件配置和使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
RocketMQ?ConsumeQueue與IndexFile實(shí)時(shí)更新機(jī)制源碼解析
這篇文章主要為大家介紹了RocketMQ?ConsumeQueue與IndexFile實(shí)時(shí)更新機(jī)制源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
將項(xiàng)目上傳到Maven中央倉(cāng)庫(2023最新版)
本文主要介紹了將項(xiàng)目上傳到Maven中央倉(cāng)庫(2023最新版),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
Java實(shí)現(xiàn)聯(lián)系人管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)聯(lián)系人管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02

