一、 核心架构分层规范 (数据流转)
- 强制: 遵循 Controller -> Service -> Mapper 的单向调用链。
- 强制: 严禁 Service 循环依赖。若 A 需调用 B,B 也需调用 A,必须将共有逻辑提取至第三个 Service 或 Manager 层。
- 规范: 所有的 POJO 转换(Entity ↔ DTO ↔ VO)建议使用
MapStruct,避免手动BeanUtils.copyProperties导致的反射性能损耗及字段名对不上的 Bug。
二、 实体类 (Entity) 与数据库建模
-
强制: 核心主键使用
Long类型,并配合 Jackson 解决 JS 精度丢失问题:@TableId(type = IdType.ASSIGN_ID) @JsonSerialize(using = ToStringSerializer.class) // 防止前端丢失最后两位精度 private Long id; -
规范: 如有需要使用 乐观锁 机制防止数据并发覆盖。
- 数据库增加
version字段。 - Entity 增加
@Version注解。
@Data public class User extends BaseEntity { // ... 其他字段 @Version // 关键:标记为乐观锁字段 private Integer version; }// 注入插件(在 Config 类中) interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());-
只支持
updateById和update方法:乐观锁插件只有在通过实体对象(带 ID 和 Version)进行更新时才生效。 -
处理失败逻辑:当
update返回 0(失败)时,你需要决定是报错给前端“系统繁忙”,还是自动发起重试(重新查询 -> 修改 -> 再提交)。 -
严禁手动改 Version:在业务代码里,你不需要执行
user.setVersion(user.getVersion() + 1),MyBatis-Plus 插件会自动帮你完成这个动作。
- 数据库增加
-
规范: 状态字段建议使用
IEnum接口枚举,避免代码中出现if (status == 1)。public enum StatusEnum { DISABLED(0, "禁用"), ACTIVE(1, "启用"); @EnumValue // 标记这个字段存入数据库 private final int code; private final String desc; }
三、 事务管理规约 (防止 10% 的隐形 Bug)
-
强制: 严禁在 Service 内部 自调用 标注了
@Transactional的方法(会导致 AOP 事务失效)。- 修正方案: 拆分到不同类,或使用
AopContext.currentProxy()。或自己注入自己
// 使用延迟加载,避免启动时的循环依赖检测问题 @Autowired @Lazy private UserService self; - 修正方案: 拆分到不同类,或使用
-
强制: 事务方法内严禁进行 网络 IO 操作(如调用第三方支付接口、发邮件)。
- 原因: 网络延迟会导致数据库连接长期不释放,瞬间撑爆连接池。
-
规范: 声明式事务建议只加在
update/insert方法上,查询方法不加事务。
四、 代码编写“六不准” (硬性军规)
-
不准在
IN查询中传入超过 1000 个元素:- 风险: 部分数据库(如 Oracle)有 1000 个上限,且 SQL 过长导致索引失效。
-
不准在
Mapper.xml中使用${}:- 风险: 99% 的 SQL 注入源于此。除
ORDER BY动态排序外,一律使用#{}。
- 风险: 99% 的 SQL 注入源于此。除
-
不准在没有
Where条件下执行update/delete:- 措施: 必须配置
BlockAttackInnerInterceptor插件。
- 措施: 必须配置
-
不准在循环中调用
saveOrUpdate:- 风险: 每次判断存在都会先执行一次
SELECT,导致数据库压力翻倍。
- 风险: 每次判断存在都会先执行一次
-
不准省略逻辑删除过滤:
- 注意: 手写 SQL 时,MP 不会自动拼接
deleted=0,必须手动在 XML 中加上。
- 注意: 手写 SQL 时,MP 不会自动拼接
-
不准使用“魔法值”:Lambda 表达式
-
禁止: 使用字符串硬编码列名(如
queryWrapper.eq("user_name", "Jack"))。 -
强制: 必须使用 Lambda 形式,确保编译期类型检查。
// 正确示例 this.lambdaQuery() .eq(User::getUserName, dto.getUserName()) .orderByDesc(User::getCreateTime) .list(); -
五、 SQL 与性能优化规范 (解决 5% 的慢查询 Bug)
-
强制: 凡是
like查询,严禁使用左模糊(%关键词),必须使用右模糊(关键词%),否则索引失效。- ✅
likeRight("name", "张") - ❌
like("name", "张")
- ✅
-
推荐: 针对大数据量表,禁止执行
select count(*),应通过业务逻辑或 Redis 维护总数。 -
规范: 尽量避免多表
Join。阿里规约建议:超过 3 张表禁止 Join。- 对策: 分两次查询,在 Service 层使用 Map 组装数据。
六、 审计与扩展功能
1. 自动填充 (MetaObjectHandler)
强制: 严禁手动维护 update_time 等公共字段。
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
}
}
2. 字段脱敏
规范: 手机号、身份证、密码等敏感信息,在 Entity 或 VO 上配合 @TableField(select = false) 或自定义 Jackson 序列化器进行脱敏。
七、 投产前 Check List (最后 5% 的 Bug 拦截)
- **空指针检查**:`LambdaQuery` 传入参数前是否判断了 `null`?(或使用 `.condition()`)
- **分页溢出**:是否配置了 `PaginationInnerInterceptor` 且设置了最大单页限制(如 500 条)?
- **字段对应**:Entity 里的字段在数据库中是否都存在?(不存在的需加 `@TableField(exist = false)`)
- **日志检查**:`application-prod.yml` 是否关闭了 SQL 日志打印?(防止日志文件撑爆磁盘)
- **缓存同步**:如果使用了 Redis 缓存,`updateById` 后是否同步删除了缓存?
八 全量配置规范实现
1. 核心插件配置类 (MybatisPlusConfig.java)
本配置类集成了常用的插件,确保系统在大数据量下的安全与性能。
package com.yourcompany.project.common.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus 全量标准化配置
* 包含:分页、乐观锁、防全表攻击插件
* * @author 专业Java程序员
*/
@Configuration
@MapperScan("com.yourcompany.project.mapper") // 修改为你的 Mapper 实际路径
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 分页插件: 必须设置数据库类型,且建议放在插件链的最前面
// 自动防止分页超出总页数(溢出处理)
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(500L); // 限制单页最大 500 条,防止内存溢出
interceptor.addInnerInterceptor(paginationInterceptor);
// 2. 乐观锁插件: 实体类配合 @Version 注解使用,防止并发冲突
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 3. 防全表更新与删除插件: 禁止没有 where 条件的 update/delete,规避重大事故
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
}
2. 审计字段自动填充 (MyMetaObjectHandler.java)
实现阿里巴巴规约中“公共字段统一维护”的要求,解决手动设置 create_time 等产生的遗漏 Bug。
package com.yourcompany.project.common.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 自动化审计字段填充器
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
// 填充创建时间与更新时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 填充默认删除位(0: 未删除)
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
// 填充版本号(乐观锁初始值)
this.strictInsertFill(metaObject, "version", Integer.class, 1);
// 如果有安全框架(如Spring Security),可动态填充当前登录用户ID
// Long userId = SecurityUtils.getUserId();
// this.strictInsertFill(metaObject, "createBy", Long.class, userId);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
// 更新操作时,自动更新修改时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
3. 标准化基类实体 (BaseEntity.java)
封装 100% 实体类都需要具备的字段,减少代码冗余。
package com.yourcompany.project.common.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public abstract class BaseEntity {
/**
* 主键: 雪花算法,解决分布式ID唯一性
* ToStringSerializer: 解决前端 JS 无法处理 19 位 Long 导致的精度丢失问题
*/
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 自动填充创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 自动填充更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 乐观锁版本号: 配合 OptimisticLockerInnerInterceptor 使用
*/
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;
/**
* 逻辑删除位: 配合 @TableLogic 使用
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;
}
4. 重点应用说明
| 功能 | 解决的潜在 Bug |
|---|---|
| 分页 MaxLimit | 防止恶意请求 size=999999 撑爆 JVM 内存。 |
| BlockAttack | 防止因代码 Bug 或运维误操作导致 DELETE FROM table(无条件全表清空)。 |
| ToStringSerializer | 解决雪花 ID 最后两位在浏览器里变 00 的经典前端精度 Bug。 |
| OptimisticLocker | 解决高并发下“先读后写”导致的数据相互覆盖(Lost Update)。 |
| StrictInsertFill | 保证所有数据均有完整的审计追踪(谁在什么时候创建的)。 |
使用建议
-
全局扫描:请确保
MybatisPlusConfig中的@MapperScan路径与你实际的Mapper接口位置一致。 -
配置文件同步:在
application.yml中开启逻辑删除全局配置(logic-delete-field: deleted)。 -
YAML 配合:
YAML
mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段 logic-delete-value: 1 logic-not-delete-value: 0
总结
规范不是为了限制开发速度,而是为了在凌晨 3 点不需要爬起来处理生产事故。