做过后台管理系统的开发者应该都有同感:
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. 1. 实体类:字段注解,不可避免
  2. 2. Mapper 接口:继承 BaseMapper,一行代码
  3. 3. Service 层:继承 IService + ServiceImpl,基础方法有了
  4. 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

我们用一个标准后台模块来对比,看看代码量和开发效率的差异:

对比项
原生 MyBatis-Plus
MyBatis-Plus Pro
Controller 代码量
约 80~120 行
约 5~10 行(仅继承)
单模块 CRUD 开发时间
15~30 分钟
1~2 分钟
条件构造
手动写 QueryWrapper
实体注解自动生成
分页排序逻辑
每个接口单独写
父类统一封装
接口规范一致性
容易各写各的,风格不统一
完全统一,风格一致
复杂查询扩展
灵活,直接写
可重写方法,同样灵活
学习成本
极低,约定大于配置

直观感受:
一个 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 实战、效率工具、后端架构内容,欢迎点赞、收藏、关注,持续跟进后端开发进阶专栏!

扫码领红包

微信赞赏支付宝扫码领红包

发表回复

后才能评论