写 Java 后端这些年,和 MyBatis-Plus 打交道的时间占了很大比重,QueryWrapper 用顺手了确实爽,但用多了就发现一个问题:每个查询接口里,都在重复写一堆字段判空、拼接条件、设置 eq、like 的模板代码。尤其是一个列表查询接口,往往带着五六七八个可筛选条件,光构造 wrapper 的代码就占掉半个方法。这个痛点催生了我这个小工具——QueryWrapperGenerator:基于注解配置实体类,自动解析出查询条件并生成查询包装器。简单说,就是给实体类字段标上注解,告诉生成器“这个字段在查询时是等值匹配还是模糊匹配”“什么时候忽略、什么时候参与”,然后调用一个方法,QueryWrapper 就出来了。适合正在用 MyBatis-Plus、又受够了手写 wrapper 条件堆砌的朋友。今天我把这套思路、注解设计、核心实现和实际接入过程完整拆开讲,代码可直接参考改造。
1. 为什么会有这个工具:手写 QueryWrapper 的痛点迁移
1.1 每个查询接口都在重复造轮子
先还原一个很常见的列表查询接口场景。假设我们要按用户姓名、手机号、年龄区间、状态、创建时间区间这几个条件筛选用户,用 MyBatis-Plus 通常会这么写:
java复制public Page<User> queryUserPage(UserQuery query, Page<User> page) {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
if (StringUtils.hasText(query.getName())) {
wrapper.like(StringUtils.hasText(query.getName()), User::getName, query.getName());
}
if (StringUtils.hasText(query.getPhone())) {
wrapper.eq(StringUtils.hasText(query.getPhone()), User::getPhone, query.getPhone());
}
if (query.getAgeStart() != null) {
wrapper.ge(User::getAge, query.getAgeStart());
}
if (query.getAgeEnd() != null) {
wrapper.le(User::getAge, query.getAgeEnd());
}
if (query.getStatus() != null) {
wrapper.eq(User::getStatus, query.getStatus());
}
if (query.getCreateStart() != null) {
wrapper.ge(User::getCreateTime, query.getCreateStart());
}
if (query.getCreateEnd() != null) {
wrapper.le(User::getCreateTime, query.getCreateEnd());
}
return userMapper.selectPage(page, wrapper);
}
这段代码有什么问题?看起来逻辑清晰,但问题恰恰在于“逻辑清晰”的代价太高:
- 每个可筛选字段都要先判空、再选方法、再传列引用和值,三行起步。
- 字段一多,方法长度肉眼可见地膨胀,而且这种膨胀是没有信息量的。
- 如果十几个查询接口每个都这么写,复制粘贴后的修改成本很惊人,漏判一个空,bug 就溜进来了。
- 更难受的是,当你需要给字段加上一个新条件类型(比如从
eq改成like),得在所有写过的接口里逐一找出来改。
这种代码大量存在,本质上是在用“手写”的方式完成一件极其规整、模式化的任务:把实体类的一个字段,根据注解或约定,映射到 QueryWrapper 的一个条件片段上。既然是规整的、可枚举的映射,那就完全可以用一个生成器来解决。
1.2 从“条件对象”到“注解实体”的思路转变
有些人会做一个 UserCondition 类,里面有 name、phone、ageStart、ageEnd 这些字段,然后在 Service 里手动挨个判断和拼装。这样其实问题也还在,只是把 wrapper.eq(...) 搬到了另一种形态里,判断逻辑并没有消失。
我最初想的是:能不能不写判断?让数据自己告诉我们“要不要参与查询”?在 Java 生态里,注解是最自然的一种“元数据声明”方式。给字段打上 @QueryField,声明它的匹配规则和忽略策略,剩下的事情交给生成器。这样实体类本身就成为了查询条件的载体,而不用额外维护一个条件类,也不用在 Service 里手工搬运。
所以核心转变是:把“过程式”的条件构造,变成“声明式”的字段元数据描述。你不再关心 if 和 wrapper.eq(),只关心“这个字段在查询中应该扮演什么角色”,生成器负责把这些角色翻译成 SQL 条件。
这个思路用在 MyBatis-Plus 上非常合适,因为 QueryWrapper / LambdaQueryWrapper 本身就是一套可编程的条件 DSL。我们要做的,只是在这套 DSL 之上再加一层更靠近业务语义的“注解 DSL”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 注解体系设计:用最小的标记成本完成条件声明
2.1 核心注解定义:@QueryField
整个生成器的入口是一个自定义注解,我命名为 @QueryField。它负责描述一个字段作为查询条件时的行为。第一版的核心定义如下:
java复制@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QueryField {
/**
* 匹配类型,默认等值匹配
*/
MatchType value() default MatchType.EQ;
/**
* 数据库列名,默认不填,使用实体字段的驼峰转下划线
*/
String column() default "";
/**
* 是否允许为空时也拼接条件。
* 默认 false,即值为 null 时直接忽略该条件。
*/
boolean allowNull() default false;
/**
* 条件组名,用于处理 OR 组合,比如 "groupOr"。
*/
String group() default "";
}
MatchType 是一个枚举,涵盖了日常查询几乎所有的基础条件类型:
java复制public enum MatchType {
EQ, // 等于
NE, // 不等于
LIKE, // 模糊匹配,%value%
LIKE_LEFT,
