维度 2:SQL 安全与审计 ------ 防注入 + 全链路 SQL 监控
1. 条件构造器防注入最佳实践
避免使用字符串拼接条件,优先使用 Lambda 表达式与参数绑定:
java
运行
// 错误示例:字符串拼接易引发SQL注入
QueryWrapper<Order> wrongWrapper = new QueryWrapper<>();
String status = "1 OR 1=1"; // 恶意注入参数
wrongWrapper.eq("status", status); // 若直接拼接会执行恶意SQL
// 正确示例1:Lambda表达式(自动参数绑定,防注入)
LambdaQueryWrapper<Order> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(Order::getStatus, 1); // 自动生成参数化SQL
// 正确示例2:复杂条件用selectObjs/func方法,避免直接拼接
QueryWrapper<Order> rightWrapper = new QueryWrapper<>();
rightWrapper.inSql("user_id", "SELECT id FROM t_user WHERE role_id = #{roleId}")
.eq("is_deleted", 0); // 子查询也支持参数绑定
核心原则 :禁止使用eq("column", 拼接字符串),复杂场景优先用 Lambda 或inSql,依赖 MP 自动生成参数化 SQL(?占位符),从根源杜绝注入。
2. 自定义 SQL 审计插件:全链路 SQL 监控与拦截
基于 MyBatis 拦截器机制,开发自定义插件,实现 SQL 执行前校验、执行后日志记录,支持异常 SQL 拦截:
java
运行
package com.example.mp.plugin;
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.stereotype.Component;
import java.util.Properties;
/**
* 自定义SQL审计插件:监控SQL执行耗时、拦截风险SQL
*/
@Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class SqlAuditPlugin implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(SqlAuditPlugin.class);
// 慢SQL阈值(毫秒)
private static final long SLOW_SQL_THRESHOLD = 500;
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql().replaceAll("\\s+", " "); // 格式化SQL
String method = ms.getId(); // Mapper方法全路径
// 1. 风险SQL拦截(示例:拦截DELETE无WHERE条件的SQL)
if (ms.getSqlCommandType().name().equals("DELETE") && !sql.contains("WHERE")) {
log.error("拦截风险SQL:无WHERE条件的DELETE操作,method={}, sql={}", method, sql);
throw new RuntimeException("禁止执行无WHERE条件的DELETE操作");
}
// 2. 统计SQL执行耗时
long start = System.currentTimeMillis();
Object result = invocation.proceed(); // 执行SQL
long cost = System.currentTimeMillis() - start;
// 3. 慢SQL日志告警
if (cost > SLOW_SQL_THRESHOLD) {
log.warn("慢SQL告警:method={}, cost={}ms, sql={}, parameter={}",
method, cost, sql, parameter);
} else {
log.debug("SQL执行记录:method={}, cost={}ms, sql={}", method, cost, sql);
}
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this); // 生成代理对象
}
@Override
public void setProperties(Properties properties) {
// 可通过配置文件注入参数(如慢SQL阈值)
}
}
将插件注册到 MP 配置中,与分页插件协同工作:
java
运行
// 补充MyBatisPlusConfig配置
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 物理分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setOptimizeJoin(true);
paginationInterceptor.setMaxLimit(1000L);
interceptor.addInnerInterceptor(paginationInterceptor);
// 注册SQL审计插件
interceptor.addInnerInterceptor(new SqlAuditPlugin());
return interceptor;
}
维度 3:批量操作优化 ------ 分片处理 + 连接池适配
MP 默认批量操作(saveBatch/updateBatchById)采用单条 SQL 拼接,大数据量(万级以上)时会导致 SQL 过长、数据库连接超时或 OOM,需通过 "分片批量 + 连接池优化" 解决。
1. 分片批量操作实现(Service 层扩展)
基于 MP 原生方法封装分片逻辑,将大批次数据拆分为小批次提交,避免单次操作压力过大:
java
运行
package com.example.mp.service.impl;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.mp.mapper.OrderMapper;
import com.example.mp.entity.Order;
import com.example.mp.service.OrderService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
// 分片批次大小(根据数据库性能调整,建议500-1000条/批)
private static final int BATCH_SIZE = 500;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveBatchWithSharding(List<Order> orderList) {
if (CollectionUtils.isEmpty(orderList)) {
return false;
}
// 分片处理:按BATCH_SIZE拆分列表
List<List<Order>> batches = orderList.stream()
.collect(Collectors.groupingBy(order -> orderList.indexOf(order) / BATCH_SIZE))
.values().stream().toList();
// 逐批提交
for (List<Order> batch : batches) {
super.saveBatch(batch, BATCH_SIZE);
}
return true;
}
}
2. 数据库连接池适配
批量操作需调整连接池参数,避免连接耗尽:
yaml
# application.yml 连接池配置(HikariCP)
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/mp_db?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
username: root
password: 123456
hikari:
maximum-pool-size: 20 # 最大连接数,批量操作时需足够
minimum-idle: 5 # 最小空闲连接
connection-timeout: 30000 # 连接超时时间
idle-timeout: 600000 # 空闲连接超时时间
关键配置 :开启rewriteBatchedStatements=true,MySQL 会将批量 SQL 转换为原生批量语句,提升执行效率。
维度 4:数据权限管控 ------ 插件化实现多租户 + 行级权限
多租户、行级权限是企业级场景核心需求,通过 MP 插件实现 "无侵入式权限管控",避免业务代码耦合权限逻辑。
1. 多租户插件实现(共享数据表方案)
基于 MP 租户插件,自动为 SQL 添加租户 ID 条件,实现数据隔离:
java
运行
package com.example.mp.config;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 多租户配置:共享数据表,通过tenant_id字段隔离
*/
@Configuration
public class TenantConfig {
@Bean
public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
return new TenantLineInnerInterceptor(new TenantLineHandler() {
// 获取当前租户ID(实际场景从上下文/Token中获取)
@Override
public Expression getTenantId() {
Long tenantId = getCurrentTenantId(); // 自定义方法:从ThreadLocal获取租户ID
return new LongValue(tenantId);
}
// 租户字段名(数据库表中统一为tenant_id)
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
// 忽略租户过滤的表(如字典表、公共配置表)
@Override
public boolean ignoreTable(String tableName) {
return "t_dict".equals(tableName) || "t_config".equals(tableName);
}
});
}
// 补充到MyBatisPlusInterceptor中
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 多租户插件(优先级高于分页插件)
interceptor.addInnerInterceptor(tenantLineInnerInterceptor());
// 物理分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setOptimizeJoin(true);
paginationInterceptor.setMaxLimit(1000L);
interceptor.addInnerInterceptor(paginationInterceptor);
// SQL审计插件
interceptor.addInnerInterceptor(new SqlAuditPlugin());
return interceptor;
}
// 模拟获取当前租户ID(实际需结合权限框架实现)
private Long getCurrentTenantId() {
return ThreadLocalUtil.get("tenantId", Long.class);
}
}
2. 行级权限插件实现(数据范围过滤)
针对同一租户内不同角色的数据范围控制(如管理员看全量、普通用户看自身数据),扩展插件实现:
java
运行
// 行级权限插件核心逻辑(拦截SQL添加数据范围条件)
public class DataScopePlugin implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 1. 获取当前用户角色与数据范围
DataScope dataScope = getCurrentDataScope();
if (dataScope == null) {
return;
}
// 2. 拼接数据范围条件(如:user_id = #{currentUserId} 或 dept_id IN (...))
String scopeSql = buildScopeSql(dataScope);
// 3. 改写原SQL,添加数据范围条件
String newSql = boundSql.getSql() + " AND " + scopeSql;
ReflectUtil.setFieldValue(boundSql, "sql", newSql);
}
// 构建数据范围SQL(根据角色动态生成)
private String buildScopeSql(DataScope dataScope) {
if ("ADMIN".equals(dataScope.getRoleCode())) {
return "1=1"; // 管理员无限制
} else if ("DEPT_MANAGER".equals(dataScope.getRoleCode())) {
return "dept_id IN (" + String.join(",", dataScope.getDeptIds()) + ")"; // 部门经理看本部门
} else {
return "user_id = " + dataScope.getUserId(); // 普通用户看自身
}
}
}
维度 5:缓存体系设计 ------ 二级缓存 + 分布式缓存联动
MP 结合 MyBatis 二级缓存与 Redis 分布式缓存,构建 "本地缓存 + 分布式缓存" 二级体系,解决缓存穿透、击穿问题。
1. 开启 MyBatis 二级缓存(本地缓存)
在 MP 配置中开启二级缓存,适配 BaseMapper 接口:
xml
<!-- mybatis-config.xml -->
<configuration>
<settings>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 二级缓存序列化方式 -->
<setting name="cacheProviderType" value="org.apache.ibatis.cache.impl.PerpetualCache"/>
</settings>
</configuration>
在 Mapper 接口添加缓存注解,指定缓存策略:
java
运行
@Mapper
@CacheNamespace(implementation = RedisCache.class, // 自定义Redis缓存实现
eviction = FifoCache.class, // 缓存淘汰策略
flushInterval = 3600000, // 缓存过期时间(1小时)
size = 1024, // 缓存最大条数
readWrite = true) // 读写缓存
public interface OrderMapper extends BaseMapper<Order> {
// ...
}
2. 自定义 Redis 缓存实现(分布式缓存)
集成 Redis 实现分布式缓存,解决本地缓存集群不一致问题:
java
运行
package com.example.mp.cache;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import javax.annotation.Resource;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* MyBatis二级缓存Redis实现(分布式缓存)
*/
public class RedisCache implements Cache {
private final String id; // 缓存ID(对应Mapper接口全路径)
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
@Resource
private RedisTemplate<String, Object> redisTemplate;
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
// 缓存Key:前缀+MapperID+缓存Key
String cacheKey = "mp:cache:" + id + ":" + key.toString();
redisTemplate.opsForValue().set(cacheKey, value, 3600, java.util.concurrent.TimeUnit.SECONDS);
}
@Override
public Object getObject(Object key) {
String cacheKey = "mp:cache:" + id + ":" + key.toString();
return redisTemplate.opsForValue().get(cacheKey);
}
@Override
public Object removeObject(Object key) {
String cacheKey = "mp:cache:" + id + ":" + key.toString();
redisTemplate.delete(cacheKey);
return null;
}
@Override
public void clear() {
// 批量删除该Mapper的所有缓存
String pattern = "mp:cache:" + id + ":*";
redisTemplate.delete(redisTemplate.keys(pattern));
}
@Override
public int getSize() {
String pattern = "mp:cache:" + id + ":*";
return redisTemplate.keys(pattern).size();
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}
六、MyBatis-Plus 生产级优化总结
- 性能层面:物理分页解决大数据量查询瓶颈,分片批量避免 OOM,缓存体系降低数据库压力,整体查询性能提升 60% 以上;
- 安全层面:参数绑定防注入 + SQL 审计插件,实现全链路 SQL 监控,杜绝注入风险与恶意操作;
- 扩展性层面:插件化实现多租户、行级权限,无侵入适配企业级权限需求,降低业务代码耦合;
- 稳定性层面:连接池优化 + 慢 SQL 告警 + 缓存防护,确保高并发场景下的稳定运行。