MybatisPlus Sql Inject魔法
?本篇只讲应用,喜欢原理的可以不用看只保证Mysql可用,没有兼容其余数据库的方言代码笔者线上环境已用,非纸上谈兵?背景介绍笔者日常的工作有些业务会遇到唯一索引约束。注意到:
业务功能相似,要求当唯一键存在时根据唯一键更新,否则直接插入「Mysql DUPLICATE KEY UPDATE」天然支持上述功能由此引发笔者做出我司公共组件,用来提高开发效率。
常见做法保证业务安全,还要保证平稳更新与写入,遇到上述需求一般方案是使用分布式锁。步骤如下:
根据唯一索引查询,如果数据存在直接更新,完成写入数据不存在则加分布锁根据唯一索引查询,如果数据存在直接更新,反之插入数据,完成写入解锁简化代码如下:
publicvoidduplicateKeyUpdate(NeedInsertModel model){ NeedInsertModel dbModel = findByUnique(model); /* 这里没考虑此刻恰好数据被别的线程删除的场景 */ if(Objects.nonNull(dbModel)) { updateByUnique(model); return; }
String lockKey = lockKey(); RLocklock= redisClient.getLock(lockKey); lock.lock();
try{ dbModel = findByUnique(model);
if(Objects.nonNull(dbModel)) { updateByUnique(model); }else { insert(model); } }finally{ lock.unlock(); }}可以看到为了达成「当唯一键存在时根据唯一键更新,否则直接插入」这一简单目的,在多线程,多进程场景下会产生很多套版代码(没什么不好,只是笔者懒惰成性)。
Sql Inject新思路我想到MybatisPlus的源码中这么写道:
只要继承该接口,就自动具备基础的CRUD功能,这是因为框架帮你生成了代理对象。可是问题是Mybatis Plus怎么知道要生成哪些代理方法,其中的代理逻辑又是哪里定义的呢?后来查资料发现,Mybatis Plus定义了一系列的「com.baomidou.mybatisplus.core.injector.AbstractMethod」对象来定制具体的逻辑,也就是生成SQL的逻辑。每一个实现类对应BaseMapper的一个方法。
选取最简单SelectById分析,其余的原理相同,其实就是拼接SQL语句。
看到这里,笔者当时就想,我直接按照官方的规范定制一个「AbstractMethod」的实现不就可以一劳永逸嘛。
经过研究代码如下:
importcom.baomidou.mybatisplus.annotation.IdType;importcom.baomidou.mybatisplus.core.injector.AbstractMethod;importcom.baomidou.mybatisplus.core.metadata.TableFieldInfo;importcom.baomidou.mybatisplus.core.metadata.TableInfo;importcom.baomidou.mybatisplus.core.metadata.TableInfoHelper;importcom.baomidou.mybatisplus.core.toolkit.StringUtils;importcom.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;importorg.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;importorg.apache.ibatis.executor.keygen.KeyGenerator;importorg.apache.ibatis.executor.keygen.NoKeyGenerator;importorg.apache.ibatis.mapping.MappedStatement;importorg.apache.ibatis.mapping.SqlSource;
importjava.util.Objects;importjava.util.function.Function;importjava.util.function.Predicate;importjava.util.stream.Collectors;
/***@authorRaphael*@since2025/7/15 19:49*/publicclassDuplicateInserterextendsAbstractMethod{
/** * 创建时间应该不要被更新 */ privatestaticfinalStringCREATE_TIME="create_time";
/** * 新注入的方法名 */ privatestaticfinalStringMETHOD_NAME="duplicateUpdate";
/** * 更新字段集的sql片段 */ privatestaticfinalStringSEGMENT=" = VALUES(";
/** * SQL模板:INSERT INTO 表名 字段集合 VALUES 值集合 ON DUPLICATE KEY UPDATE 更新字段集 */ privatestaticfinalStringFORMAT="script"+ "\nINSERT INTO %s %s VALUES %s ON DUPLICATE KEY UPDATE %s\n"+ "/script";
publicDuplicateInserter(){ super(METHOD_NAME); }
@Override publicMappedStatementinjectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo){ StringinsertColumns=tableInfo.getAllInsertSqlColumnMaybeIf(EMPTY); StringinsertValues=tableInfo.getAllInsertSqlPropertyMaybeIf(EMPTY); StringinsertColumnsTrim=SqlScriptUtils.convertTrim( insertColumns, LEFT_BRACKET, RIGHT_BRACKET, null, COMMA StringinsertValuesTrim=SqlScriptUtils.convertTrim( insertValues, LEFT_BRACKET, RIGHT_BRACKET, null, COMMA
StringkeyProperty=tableInfo.getKeyProperty(), keyColumn = tableInfo.getKeyColumn();
/* 过滤掉主键和create_time字段 */ PredicateTableFieldInfo needConcat = field - ( !Objects.equals(field.getColumn(), keyColumn) && !Objects.equals(field.getColumn(), CREATE_TIME)
/* 构建 "column = VALUES(column)" 形式的字符串 */ FunctionTableFieldInfo, String stringFunc = field - field.getColumn() + SEGMENT + field.getColumn() + RIGHT_BRACKET;
/* 构建 ON DUPLICATE KEY UPDATE 后面的 SET 子句 */ StringupdateSet=tableInfo .getFieldList() .stream() .filter(needConcat) .map(stringFunc) .collect(Collectors.joining(COMMA));
KeyGeneratorkeyGenerator=NoKeyGenerator.INSTANCE; /* 表包含主键处理逻辑,如果不包含主键当普通字段处理 */ if(StringUtils.isNotBlank(tableInfo.getKeyProperty())) { if(tableInfo.getIdType() == IdType.AUTO) { /* 自增主键 */ keyGenerator = Jdbc3KeyGenerator.INSTANCE; }elseif(null!= tableInfo.getKeySequence()) { keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant); } }
Stringsql=String.format(FORMAT, tableInfo.getTableName(), insertColumnsTrim, insertValuesTrim, updateSet); SqlSourcesqlSource=languageDriver.createSqlSource(configuration, sql, modelClass); returnthis.addInsertMappedStatement( mapperClass, modelClass, METHOD_NAME, sqlSource, keyGenerator, keyProperty, keyColumn }
}然而并没有什么作用,因为你没有将自己的「AbstractMethod」定制实现注册到Mybatis Plus框架,我们需要找到一个切口。
publicclass?StrengthenSqlInjectorextendsDefaultSqlInjector{
@Override publicListAbstractMethodgetMethodList(Class mapperClass, TableInfo tableInfo) { ListAbstractMethod methodList =super.getMethodList(mapperClass, tableInfo); /* ?? ?? ?? :注册自己的定制实现 */ methodList.add(newDuplicateInserter()); returnmethodList; }
}还需要替换MybatisPlus自带的DefaultSqlInjector
@Configuration@EnableTransactionManagementpublicclassMybatisPlusAutoConfigurationimplementsMybatisPlusPropertiesCustomizer{
@Override publicvoidcustomize(MybatisPlusProperties properties) { properties.getGlobalConfig() .setSqlInjector(newStrengthenSqlInjector()); }
}如此便可完成了。你问我怎么使用?仅仅只需要在业务Mapper中声明一下duplicateUpdate即可,框架会自动帮你生成支持「DUPLICATE KEY UPDATE」语法的SQL。
@MapperpublicinterfaceBusinessMapper extendsBaseMapperBusinessModel {
voidduplicateUpdate(BusinessModel model);
}温馨提示?? ?? ??「接下来这段话非常重要」
因为我们是替换MybatisPlus自带的DefaultSqlInjector,注意这里是替换逻辑,因此假设你的系统中有多个MybatisPlusPropertiesCustomizer的Bean那么只会有最后一个生效,因此如果你有多个自定义实现最好全部都放置在一起,只有一个StrengthenSqlInjector最好。
AI编程资讯AI编码专区指南:
https://aicoding.juejin.cn/aicoding
点击“阅读译文“了解详情~
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线