做过后台管理系统的开发者应该都有同感:
MyBatis-Plus 已经帮我们省掉了大量单表 SQL,但每个模块的 Controller 层还是在重复劳动——增删改查、分页查询、条件检索、批量删除,每个接口结构都差不多,换个实体名就得重新写一遍,复制粘贴改改字段,枯燥又容易出错。
一个标准的后台模块,从实体到 Mapper、Service、Controller,一套 CRUD 写下来少说几十行样板代码,十个模块就是几百行重复劳动。
有没有办法再进一步,把 Controller 层的重复代码也彻底省掉?
最近圈内很火的 MyBatis-Plus Pro 就给出了答案:
基于 MyBatis-Plus 做上层抽象,核心只靠一个 BaseController,继承一下就能拥有全套增删改查、分页检索、动态排序、自动条件构造,单模块 CRUD 零手写代码,开发效率直接再上一个台阶。
一、MyBatis-Plus 已经够强了,为什么还要 Pro?
MyBatis-Plus 解决的是 Mapper 层的重复 SQL,而 Pro 解决的是 Controller + Service 层的重复逻辑。
我们先算一笔账:一个普通的后台管理模块,用原生 MP 你要写多少代码?
-
1. 实体类:字段注解,不可避免 -
2. Mapper 接口:继承 BaseMapper,一行代码 -
3. Service 层:继承 IService+ServiceImpl,基础方法有了 -
4. Controller 层:新增、修改、删除、批量删除、根据ID查、分页条件查询、列表查询……至少 6~8 个接口
真正重复的恰恰是 Controller 层:每个模块的分页接口都是「接收参数 → 构造条件 → 调用分页 → 封装返回」,结构一模一样,只是实体和业务参数不同。
十个模块就要写十遍,改个统一返回格式还要逐个改,纯纯的体力劳动。
MyBatis-Plus Pro 的核心设计思想
约定优于配置,抽象重复流程。
把所有单表 CRUD 的通用流程,全部抽象到 BaseController 父类中:
-
• 参数接收、校验统一处理 -
• 查询条件自动根据实体构造 -
• 分页、排序、列表逻辑统一封装 -
• 增删改查方法默认实现 -
• 统一响应格式、异常处理
子类只需要继承 BaseController,指定对应的实体和 Service,一行业务代码不用写,全套 CRUD 接口直接就绪。
它不是替代 MyBatis-Plus,而是站在 MP 的肩膀上,再往上做一层业务抽象,把重复度最高的 Controller 层样板代码彻底干掉。
二、四大核心特性
1. 开箱即用的 BaseController
这是整个方案的灵魂。
通用增删改查、分页查询、列表查询、批量删除,全部在父类实现好了。
业务 Controller 只需要继承,指定泛型,就能直接对外提供全套接口,零代码完成单模块 CRUD。
2. 实体驱动的自动条件构造
不用再手动写 QueryWrapper 拼条件。
前端传什么字段,后端就自动按字段等值查询;配合注解还能实现模糊查询、区间查询、IN 查询。
反射自动读取实体字段值,非空字段自动生成查询条件,彻底告别一堆 if (xx != null) wrapper.eq(...)。
3. 统一分页与排序规范
分页参数、排序字段、升降序全部统一封装,自动注入分页对象。
默认物理分页,内置排序字段白名单校验,从根源避免 SQL 注入风险,不用每个接口单独处理。
4. 完全兼容原生 MP
所有增强都只在 Controller 层做抽象,底层完全复用 MyBatis-Plus 的原生能力。
复杂查询、自定义 SQL、多表联查、插件体系,原来怎么写现在还怎么写,不会因为用了 Pro 就丧失灵活性。
三、代码示例
下面基于 Spring Boot 3.x + MyBatis-Plus 3.5.15,从零实现一套可直接复用的 Pro 版 CRUD 架构。
3.1 基础依赖
先引入 MyBatis-Plus 官方 Starter,Pro 层我们自己抽象实现,无需额外第三方包:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.15</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2 第一步:定义查询条件注解
先定义一套查询注解,用来标记字段的查询方式,这是自动条件构造的基础:
/**
* 查询条件注解,标记字段的查询类型
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QueryCondition {
/**
* 查询方式:EQ等于、LIKE模糊、GT大于、LT小于、BETWEEN区间、IN集合
*/
Type value() default Type.EQ;
/**
* 对应数据库列名,默认驼峰转下划线
*/
String column() default "";
enum Type {
EQ, NE, LIKE, LEFT_LIKE, RIGHT_LIKE,
GT, GE, LT, LE, BETWEEN, IN
}
}
3.3 第二步:自动条件构造工具类
核心工具类,通过反射读取实体字段和注解,自动生成 QueryWrapper。
这也是 Pro 版最能省代码的地方,不用每个接口手动拼条件。
/**
* 自动查询条件构造器
*/
public class QueryWrapperBuilder {
/**
* 根据实体对象自动构建查询条件
*/
public static <T> LambdaQueryWrapper<T> build(T entity) {
LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
if (entity == null) {
return wrapper;
}
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field : fields) {
// 跳过静态、final字段
if (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())) {
continue;
}
field.setAccessible(true);
try {
Object value = field.get(entity);
// 空值不加入条件
if (value == null || (value instanceof String str && str.isEmpty())) {
continue;
}
// 获取列名:默认驼峰转下划线
String columnName = getColumnName(field);
QueryCondition annotation = field.getAnnotation(QueryCondition.class);
QueryCondition.Type type = annotation != null
? annotation.value()
: QueryCondition.Type.EQ;
// 根据类型拼接条件
appendCondition(wrapper, columnName, type, value);
} catch (IllegalAccessException e) {
throw new RuntimeException("条件构造失败", e);
}
}
return wrapper;
}
private static String getColumnName(Field field) {
QueryCondition annotation = field.getAnnotation(QueryCondition.class);
if (annotation != null && !annotation.column().isEmpty()) {
return annotation.column();
}
// 驼峰转下划线
return StrUtil.toUnderlineCase(field.getName());
}
@SuppressWarnings("unchecked")
private static <T> void appendCondition(LambdaQueryWrapper<T> wrapper,
String column,
QueryCondition.Type type,
Object value) {
switch (type) {
case EQ -> wrapper.eq(column, value);
case NE -> wrapper.ne(column, value);
case LIKE -> wrapper.like(column, value);
case LEFT_LIKE -> wrapper.likeLeft(column, value);
case RIGHT_LIKE -> wrapper.likeRight(column, value);
case GT -> wrapper.gt(column, value);
case GE -> wrapper.ge(column, value);
case LT -> wrapper.lt(column, value);
case LE -> wrapper.le(column, value);
case IN -> wrapper.in(column, (Collection<?>) value);
default -> wrapper.eq(column, value);
}
}
}
3.4 第三步:统一分页请求 DTO
规范分页、排序参数,所有接口统一入参格式:
@Data
public class PageQuery<T> {
/** 页码 */
private Integer pageNum = 1;
/** 每页条数 */
private Integer pageSize = 10;
/** 排序字段 */
private String orderBy;
/** 排序方向 asc/desc */
private String orderDirection = "desc";
/** 查询条件实体 */
private T query;
}
3.5 第四步:核心 BaseController(灵魂)
把所有通用 CRUD 逻辑全部封装到父类,子类继承即用。
public abstract class BaseController<S extends IService<T>, T> {
@Autowired
protected S baseService;
/**
* 根据ID查询
*/
@GetMapping("/{id}")
public Result<T> getById(@PathVariable Long id) {
return Result.success(baseService.getById(id));
}
/**
* 分页查询
*/
@PostMapping("/page")
public Result<PageResult<T>> page(@RequestBody PageQuery<T> pageQuery) {
// 1. 构建分页对象
Page<T> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
// 2. 自动构造查询条件
LambdaQueryWrapper<T> wrapper = QueryWrapperBuilder.build(pageQuery.getQuery());
// 3. 排序处理(内置白名单校验,防止SQL注入)
if (StrUtil.isNotBlank(pageQuery.getOrderBy())) {
boolean isValidField = checkOrderField(pageQuery.getOrderBy());
if (isValidField) {
boolean isAsc = "asc".equalsIgnoreCase(pageQuery.getOrderDirection());
wrapper.orderBy(true, isAsc, pageQuery.getOrderBy());
}
}
// 4. 执行查询
baseService.page(page, wrapper);
// 5. 封装返回
return Result.success(new PageResult<>(
page.getRecords(),
page.getTotal(),
page.getCurrent(),
page.getSize()
));
}
/**
* 列表查询(不分页)
*/
@PostMapping("/list")
public Result<List<T>> list(@RequestBody(required = false) T query) {
LambdaQueryWrapper<T> wrapper = QueryWrapperBuilder.build(query);
return Result.success(baseService.list(wrapper));
}
/**
* 新增
*/
@PostMapping
public Result<Void> add(@RequestBody T entity) {
baseService.save(entity);
return Result.success();
}
/**
* 修改
*/
@PutMapping
public Result<Void> update(@RequestBody T entity) {
baseService.updateById(entity);
return Result.success();
}
/**
* 根据ID删除
*/
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
baseService.removeById(id);
return Result.success();
}
/**
* 批量删除
*/
@DeleteMapping("/batch")
public Result<Void> batchDelete(@RequestBody List<Long> ids) {
baseService.removeByIds(ids);
return Result.success();
}
/**
* 排序字段校验,子类可重写扩展白名单
*/
protected boolean checkOrderField(String field) {
// 默认允许的通用字段,业务类可重写扩展
return Set.of("id", "create_time", "update_time").contains(field);
}
}
3.6 第五步:业务模块零代码使用
有了 BaseController,业务模块的 Controller 会简洁到离谱。
以用户模块为例,实体、Mapper、Service 还是正常写,Controller 只需要一行继承:
/**
* 用户管理 Controller
* 继承BaseController,自动拥有全套CRUD接口
*/
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController<SysUserService, SysUser> {
// 什么都不用写,增删改查、分页、列表、批量删除全部就绪
// 需要扩展业务逻辑时,重写对应方法即可
@Override
protected boolean checkOrderField(String field) {
// 扩展用户模块允许排序的字段
return super.checkOrderField(field)
|| Set.of("username", "dept_id", "status").contains(field);
}
}
就这么一行代码,前端直接可以调用:
-
• GET /system/user/{id}查询单条 -
• POST /system/user/page分页查询 -
• POST /system/user/list全量列表 -
• POST /system/user新增 -
• PUT /system/user修改 -
• DELETE /system/user/{id}删除 -
• DELETE /system/user/batch批量删除
实体类加上查询注解,就能自动支持复杂条件:
@Data
@TableName("sys_user")
public class SysUser {
private Long id;
@QueryCondition(QueryCondition.Type.LIKE)
private String username;
@QueryCondition
private Integer deptId;
@QueryCondition
private Integer status;
private LocalDateTime createTime;
}
前端传 username: "张",自动按模糊查询;传 status: 1,自动按等值匹配,完全不用自己写条件拼接。
四、Pro 版 vs 原生 MP
我们用一个标准后台模块来对比,看看代码量和开发效率的差异:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
直观感受:
一个 10 个模块的后台管理系统,原生 MP 光写 Controller 层 CRUD 就要大半天;用 Pro 版,十几分钟就能全部搞定,剩下的精力全部投入到复杂业务逻辑上。
五、复杂场景怎么扩展
Pro 不是只能做简单 CRUD,复杂场景同样能很好地支持,核心就是「默认够用,按需重写」。
5.1 复杂查询:重写方法,自定义条件
如果查询逻辑很复杂,自动条件构造满足不了,直接重写对应方法即可:
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController<SysUserService, SysUser> {
@Override
@PostMapping("/page")
public Result<PageResult<SysUser>> page(@RequestBody PageQuery<SysUser> pageQuery) {
// 自定义复杂查询逻辑
Page<SysUser> page = baseService.customPageQuery(pageQuery);
return Result.success(new PageResult<>(
page.getRecords(), page.getTotal(),
page.getCurrent(), page.getSize()
));
}
}
父类提供默认实现,子类按需重写,既保证了效率,又不损失灵活性。
5.2 新增业务逻辑:前置后置处理
新增、修改前后需要加业务逻辑,比如数据校验、填充默认值、记录操作日志,重写方法加逻辑就行:
@Override
@PostMapping
public Result<Void> add(@RequestBody SysUser entity) {
// 前置校验
validateUser(entity);
// 填充默认字段
fillDefaultValue(entity);
// 调用父类通用保存
super.add(entity);
// 后置操作:记录日志
recordOperationLog("新增用户:" + entity.getUsername());
return Result.success();
}
5.3 统一权限控制
在 BaseController 里统一加权限校验注解,或者通过拦截器做接口权限控制,所有子接口自动生效,不用每个模块单独加。
六、注意事项
这套方案能极大提升效率,但用不好也容易出问题,这几个坑一定要避开。
1:排序字段不校验,存在 SQL 注入风险
排序字段是直接拼进 SQL 的,没有预编译。
✅ 必须做白名单校验,只允许指定字段排序,禁止前端任意字段透传,父类已经做了基础校验,子类一定要扩展自己的字段白名单。
2:把业务逻辑全堆在 Controller
不要因为 BaseController 方便,就把业务逻辑都写在 Controller 重写方法里。
✅ 业务逻辑依然要放在 Service 层,Controller 只做参数接收和结果封装,保持三层架构职责清晰。
3:所有模块一刀切,强行套用
不是所有模块都适合用这套通用 CRUD。
✅ 简单单表管理模块(用户、角色、部门、字典)非常适合;复杂业务模块(订单、交易、工作流)不要强行套,该怎么写就怎么写。
4:完全抛弃自定义 SQL
自动条件构造只能解决简单查询,不要为了省代码硬套。
✅ 复杂多表联查、统计查询、分组聚合,老老实实写 XML 或自定义 Mapper 方法,工具是提效的,不是束缚。
七、全文总结
MyBatis-Plus Pro 本质上不是什么黑科技,就是一套非常务实的工程实践:
把大家天天在写的重复代码,通过抽象、封装、约定,统一收敛到父类,让开发者从重复的体力劳动里解放出来,把精力放在真正有价值的业务逻辑上。
它没有改变 MyBatis-Plus 的底层,也没有引入什么复杂技术,就是用最朴素的继承、反射、注解,把能省的代码都省掉。
对于后台管理类项目,这套方案能让 CRUD 开发效率提升数倍,代码规范度也能上一个台阶。
技术从来不是越复杂越好,能实实在在解决痛点、提升效率的,就是好方案。
MyBatis-Plus 是 Java 后端最常用的持久层框架,但很多人只停留在基础 CRUD 用法,大量时间消耗在重复代码上。
后续持续更新 MyBatis-Plus 实战专栏:高级条件构造、性能优化、多租户最佳实践、代码生成器高阶用法全套生产源码干货。
喜欢 MyBatis 实战、效率工具、后端架构内容,欢迎点赞、收藏、关注,持续跟进后端开发进阶专栏!
微信赞赏
支付宝扫码领红包
