Android使用自定义注解一键校验表单、实体类参数

Android 自定义注解实现实体类一键校验(完整版 v3.1)

本文是在原博客 Android自定义注解实现一键校验实体类参数 基础上的 完整重写与能力升级

旧版侧重「从零手写反射校验」的思路讲解;新版将能力沉淀为独立 Module io.coderf.arklab.annotation:annotation ,在保留全部原有 API 习惯的同时,新增 条件校验、跨字段比较、批量错误收集、父类字段扫描、Room 友好实践 等能力。

当前版本:3.1.0 | Java 17 | Maven 坐标见下文


目录

  1. 前言与适用场景
  2. 快速开始
  3. 核心设计思路(继承旧文)
  4. [注解 API 全览](#注解 API 全览)
  5. [VerifyType 校验类型大全](#VerifyType 校验类型大全)
  6. [分组校验 VerifyGroup](#分组校验 VerifyGroup)
  7. [条件校验 @VerifyWhen(v3.1 新增)](#条件校验 @VerifyWhen(v3.1 新增))
  8. [跨字段校验 @VerifyCrossField(v3.1 新增)](#跨字段校验 @VerifyCrossField(v3.1 新增))
  9. [嵌套对象校验 @Valid](#嵌套对象校验 @Valid)
  10. [校验顺序 @VerifySort](#校验顺序 @VerifySort)
  11. [EntityValidator 调用方式](#EntityValidator 调用方式)
  12. [VerifyResult 结果对象](#VerifyResult 结果对象)
  13. [与 Room 数据库配合(重要)](#与 Room 数据库配合(重要))
  14. [完整实战示例 Person](#完整实战示例 Person)
  15. [两个 Demo 页面对照](#两个 Demo 页面对照)
  16. [ProGuard 混淆配置](#ProGuard 混淆配置)
  17. [版本升级说明(2.x → 3.1)](#版本升级说明(2.x → 3.1))
  18. [常见问题 FAQ](#常见问题 FAQ)
  19. 写在最后

前言与适用场景

表单提交前,我们往往要写大量 if-else

  • 姓名是否为空?
  • 长度是否合法?
  • 手机号格式是否正确?
  • 开始日期是否早于结束日期?
  • 性别为「女」时是否必须填写座机?

本框架的目标:把校验规则声明在实体类字段上,提交时一行代码完成校验:

java 复制代码
VerifyResult result = EntityValidator.validate(formData);
if (!result.isOk()) {
    showToast(result.getErrorMsg());
    return;
}
// 校验通过,继续提交

适合场景:

  • Android 表单 / MVVM DataBinding 绑定 Bean
  • 同一 Bean 在「新增 / 编辑 / 草稿」场景下规则不同(分组校验)
  • Bean 同时是 Room @Entity(持久化字段与 UI 专用字段分离)
  • 需要嵌套校验子对象(如 Family family

前置知识: Java 注解基础、反射、RetentionPolicy.RUNTIME


快速开始

1. Maven 依赖(推荐 3.1.0)

settings.gradle 配置阿里云私服(需配置环境变量 ALIYUN_USER_NAME / ALIYUN_PASSWORD),本库外部无法使用,大家可以自行发布项目里已提供好脚本:

仓库源码:https://github.com/fzkf9225/mvvm-componnent-master/blob/master/annotation/src/main/java/io/coderf/arklab/annotation/verify/EntityValidator.java

groovy 复制代码
maven {
    url = '阿里云仓库地址'
    credentials {
        username = System.getenv("ALIYUN_USER_NAME")
        password = System.getenv("ALIYUN_PASSWORD")
    }
}

Module 依赖:

groovy 复制代码
// 运行时校验
implementation "io.coderf.arklab.annotation:annotation:3.1.0"

// 若使用 @FormatDecimal APT(已废弃,可选)
annotationProcessor "io.coderf.arklab.annotation:annotation:3.1.0"

本地 Module 引用(源码调试):

groovy 复制代码
implementation project(':annotation')
annotationProcessor project(':annotation')

2. 最小示例

实体类:

java 复制代码
@VerifyEntity
public class LoginForm {
    @VerifyParams(type = VerifyType.NOT_EMPTY, errorMsg = "账号不能为空")
    private String username;

    @VerifyParams(type = VerifyType.NOT_EMPTY, errorMsg = "密码不能为空")
    private String password;

    // getter / setter ...
}

Activity 中调用:

java 复制代码
binding.submit.setOnClickListener(v -> {
    VerifyResult result = EntityValidator.validate(binding.getForm());
    showToast(result.isOk() ? "验证成功" : "验证失败:" + result.getErrorMsg());
});

核心设计思路(继承旧文)

旧文从 @VerifyEntity + @VerifyParams 出发,通过反射遍历字段、解析注解、返回 VerifyResult,核心流程如下:

复制代码
点击提交
   ↓
EntityValidator.validate(entity)
   ↓
读取 @VerifyEntity(是否启用 / 是否排序)
   ↓
遍历字段(含父类字段,v3.1)
   ↓
解析 @VerifyField / @VerifyParams / @Valid / @VerifyWhen / @VerifyCrossField
   ↓
按分组过滤 → 条件判断 → 单字段规则 → 跨字段规则 → 嵌套对象递归
   ↓
返回 VerifyResult(首个错误或全部错误)

旧文已解决的问题(仍适用)

问题 方案
一个字段多种规则、多种 errorMsg @VerifyField({ @VerifyParams(...), @VerifyParams(...) })
空值语义混乱 使用 VerifyType.NOTNULLVerifyType.NOT_EMPTY 区分
非 NOTNULL 规则遇空值 有值才校验,无值跳过(除 NOTNULL / NOT_EMPTY 外)
反射字段顺序与源码不一致 @VerifyEntity(sort = true) + @VerifySort(n)
多种格式(手机、邮箱、范围、正则) VerifyType 枚举

v3.1 新增增强

能力 说明
@VerifyWhen 某字段满足条件时才校验当前字段
@VerifyCrossField 当前字段与另一字段比较(≥、≤、= 等)
validateAll() 一次返回全部错误项
getFieldName() 首个失败字段名
父类字段扫描 父类 @ColumnInfo 等字段上的注解也会生效
嵌套 @Valid 传递分组 子对象递归校验使用同一 VerifyGroup
异常不再静默放行 校验异常返回 fail,不再误报成功
新 VerifyType ID_CARD / URL / POSTAL_CODE / AGE / DATE / DATETIME / TIME

注解 API 全览

@VerifyEntity(类注解)

标注在实体类上,表示该类参与校验。

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyEntity {
    /** 是否启用校验,默认 true */
    boolean enable() default true;
    /** 是否按 @VerifySort 排序后校验,默认 false */
    boolean sort() default false;
}

@VerifyParams(字段注解)

单条校验规则,必须指定 type

java 复制代码
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyParams {
    VerifyType type();
    Class<?>[] group() default VerifyGroup.Default.class;
    String equalStr() default "";
    int minLength() default -1;
    int maxLength() default -1;
    double minNumber() default -Double.MAX_VALUE;
    double maxNumber() default Double.MAX_VALUE;
    String errorMsg() default "信息填写错误,请验证后重新输入!";
    String regex() default "";
    String dateFormat() default "";
    /** v3.1:单条规则生效条件,默认始终生效 */
    VerifyWhen when() default @VerifyWhen(refField = VerifyWhen.SKIP);
}

与旧版差异: 旧版 @VerifyParams 上的 notNull() / notEmpty() 布尔属性已移除,请统一改用 VerifyType.NOTNULLVerifyType.NOT_EMPTY

@VerifyField(字段注解)

同一字段声明 多条 @VerifyParams,等价于规则数组:

java 复制代码
@VerifyField({
    @VerifyParams(type = VerifyType.NOT_EMPTY, errorMsg = "姓名为空!"),
    @VerifyParams(type = VerifyType.LENGTH_RANGE_EQUAL, minLength = 2, maxLength = 10, errorMsg = "姓名长度 2~10"),
    @VerifyParams(type = VerifyType.REGEX, regex = "^[\\u4e00-\\u9fa5]+$", errorMsg = "仅限中文")
})
private String name;

@VerifyField 与单个 @VerifyParams 可混用,校验时会合并为一个规则数组。

@VerifySort(字段注解)

控制校验顺序,数值越小越先校验。旧版名称为 @VerifyFieldSort,现已统一为 @VerifySort

java 复制代码
@VerifySort(1)
private String name;

需配合 @VerifyEntity(sort = true) 使用。

@VerifyArray

同一字段多个 @Valid 时使用(与 @VerifyField 设计类似)。

@VerifyWhen / @VerifyWhenAll(v3.1)

条件校验,详见 条件校验 章节。

@VerifyCrossField / @VerifyCrossFields(v3.1)

跨字段比较,详见 跨字段校验 章节。

@Valid

标记字段为 嵌套对象或集合 ,递归校验内部 @VerifyEntity 实体:

java 复制代码
@Valid(notNull = true, group = VerifyGroup.Create.class, errorMsg = "请填写家庭信息")
private Family family;
  • notNull = true:对象本身不能为 null
  • notEmpty = true:集合 / Map 不能为空
  • 若字段是 Collection,会遍历每个元素递归校验

VerifyType 校验类型大全

枚举值 含义 主要参数
NOTNULL 不能为 null(允许空字符串) ---
NOT_EMPTY 不能为 null,且不能为 "" / 空集合 / 空 Map ---
EQUALS 等于指定字符串 equalStr
NOT_EQUALS 不等于指定字符串 equalStr
NUMBER 数字(整数或小数) ---
NUMBER_INTEGER 整数 ---
NUMBER_DOUBLE 小数 ---
NUMBER_00 两位小数,如 72.00 ---
EMAIL 邮箱 ---
PHONE 手机或固话 ---
MOBILE_PHONE 仅手机号 ---
TEL_PHONE 仅固话 ---
NUMBER_RANGE 数字范围(不含边界) minNumber / maxNumber
NUMBER_RANGE_EQUAL 数字范围(含边界) minNumber / maxNumber
LENGTH_RANGE 字符串长度范围(不含边界) minLength / maxLength
LENGTH_RANGE_EQUAL 字符串长度范围(含边界) minLength / maxLength
REGEX 自定义正则 regex
DATE 日期格式 dateFormat,默认 yyyy-MM-dd
DATETIME 日期时间 dateFormat,默认 yyyy-MM-dd HH:mm:ss
TIME 时间 08:3008:30:00
ID_CARD 身份证号 ---
URL URL 地址 ---
POSTAL_CODE 国内 6 位邮编 ---
AGE 年龄 0~120 ---

空值处理约定(重要)

  1. NOTNULL / NOT_EMPTY:无论其他条件,空值直接失败。
  2. 其他类型 :值为 null / 空字符串 / 空集合 / 空 Map 时 跳过该校验(视为「未填写则不校验格式」)。
  3. 若业务要求「未填写也要报错」,请显式增加一条 NOT_EMPTYNOTNULL 规则。

分组校验 VerifyGroup

不同业务场景可使用不同规则集:

java 复制代码
public class VerifyGroup {
    public interface Default {}   // 默认
    public interface Create {}    // 新增
    public interface Editor {}    // 编辑
}

字段上指定分组:

java 复制代码
@VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Create.class, errorMsg = "新增时邮箱必填")
private String email;

调用时指定分组:

java 复制代码
// 单个分组
EntityValidator.validate(entity, VerifyGroup.Create.class);

// 多个分组(字段 group 与任一传入分组匹配即生效)
EntityValidator.validate(entity, VerifyGroup.Create.class, VerifyGroup.Editor.class);

条件校验 @VerifyWhen(v3.1 新增)

语义: 仅当「参考字段」满足条件时,才对「当前字段」执行校验。

方式一:字段级 @VerifyWhen(作用于该字段全部规则)

java 复制代码
@VerifyWhen(refField = "age", operator = ConditionOperator.GREATER_THAN_OR_EQUAL, value = "18",
        group = VerifyGroup.Create.class)
@VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Create.class,
        errorMsg = "成年人请填写紧急联系人!")
@Ignore  // 非 Room 字段
private String emergencyContact;

含义:Create 分组下,仅当 age >= 18 时,才校验 emergencyContact 非空。

方式二:单条规则级 when(写在 @VerifyParams 内)

java 复制代码
@VerifyField({
    @VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Default.class,
            errorMsg = "请填写固话号码!"),
    @VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Create.class,
            errorMsg = "女性用户请填写座机号码!",
            when = @VerifyWhen(refField = "sex", operator = ConditionOperator.EQUALS, value = "女")),
    @VerifyParams(type = VerifyType.TEL_PHONE, group = VerifyGroup.Create.class,
            errorMsg = "座机号码格式不正确!",
            when = @VerifyWhen(refField = "sex", operator = ConditionOperator.EQUALS, value = "女"))
})
private String tel;

含义:Default 分组座机始终必填;Create 分组下 仅当性别为「女」 时座机才必填且校验格式。

ConditionOperator 操作符

操作符 说明
EQUALS / NOT_EQUALS 等于 / 不等于常量 value
NOT_NULL / IS_NULL 参考字段非空 / 为空
NOT_EMPTY / IS_EMPTY 参考字段非空串 / 为空
IN / NOT_IN 参考字段值在 / 不在 values[]
GREATER_THAN 参考字段与 value 比较大小
CONTAINS 参考字段字符串包含 value

可通过 compareAs 指定比较策略:AUTO(自动)、NUMBERSTRINGDATE

多条件 AND:@VerifyWhenAll

java 复制代码
@VerifyWhenAll({
    @VerifyWhen(refField = "sex", operator = ConditionOperator.EQUALS, value = "女"),
    @VerifyWhen(refField = "age", operator = ConditionOperator.GREATER_THAN_OR_EQUAL, value = "18")
})
@VerifyParams(type = VerifyType.NOT_EMPTY, errorMsg = "满足条件时必填")
private String someField;

跨字段校验 @VerifyCrossField(v3.1 新增)

语义: 当前字段值 与 参考字段值 满足指定关系。

java 复制代码
// endDate >= startDate
@VerifyCrossField(refField = "startDate", operator = CrossFieldOperator.GREATER_THAN_OR_EQUAL,
        dateFormat = "yyyy-MM-dd", errorMsg = "结束日期不能早于开始日期")
@VerifyParams(type = VerifyType.NOT_EMPTY, errorMsg = "结束日期不能为空")
private String endDate;
java 复制代码
// 开学时间不能早于生日(@Ignore 演示字段,不影响 Room)
@VerifyCrossField(refField = "birthday", operator = CrossFieldOperator.GREATER_THAN_OR_EQUAL,
        dateFormat = "yyyy-MM-dd", group = VerifyGroup.Default.class,
        errorMsg = "开学时间不能早于生日!")
@Ignore
private String schoolStartTime;
java 复制代码
// Create 分组:体重数值应小于身高(演示数值比较)
@VerifyCrossField(refField = "height", operator = CrossFieldOperator.LESS_THAN,
        group = VerifyGroup.Create.class, errorMsg = "体重数值应小于身高")
@ColumnInfo
private String weight;

CrossFieldOperator

操作符 含义(当前字段 vs 参考字段)
EQUALS 相等
NOT_EQUALS 不相等
GREATER_THAN 大于
GREATER_THAN_OR_EQUAL 大于等于
LESS_THAN 小于
LESS_THAN_OR_EQUAL 小于等于

比较策略通过 compareAs 控制;日期类场景配置 dateFormat

多条跨字段规则使用 @VerifyCrossFields({ ... })

注意: 跨字段校验在「当前字段值为空」时会跳过(与格式类 VerifyType 行为一致)。请先配合 NOT_EMPTY 保证有值。


嵌套对象校验 @Valid

java 复制代码
@VerifyEntity(sort = true)
public class Family {
    @VerifyParams(type = VerifyType.NOTNULL, errorMsg = "请填写妻子姓名")
    private String wife;

    @VerifyParams(type = VerifyType.NOTNULL, errorMsg = "请填写丈夫姓名")
    private String husband;
}
java 复制代码
@VerifySort(15)
@Valid(notNull = true, group = VerifyGroup.Create.class, errorMsg = "请选择家庭信息!")
@Ignore
public Family family;

调用 EntityValidator.validate(person, VerifyGroup.Create.class) 时,会递归校验 family 内部字段,且 子对象沿用同一分组(v3.1 修复了旧版嵌套校验分组丢失的问题)。


校验顺序 @VerifySort

Java 反射 getDeclaredFields() 不保证 与源码声明顺序一致。旧文已通过自定义排序解决,现用 @VerifySort(旧名 @VerifyFieldSort):

java 复制代码
@VerifyEntity(sort = true)
public class Person {
    @VerifySort(1)
    private String name;

    @VerifySort(2)
    private String sex;

    @VerifySort(3)
    private String birthday;
    // ...
}

未标注 @VerifySort 的字段排在最后(Integer.MAX_VALUE)。


EntityValidator 调用方式

方法 说明
validate(entity) Default 分组,返回 第一个 错误
validate(entity, group) 指定分组,返回第一个错误
validate(entity, group1, group2, ...) 多分组,返回第一个错误
validateAll(entity) Default 分组,返回 全部 错误
validateAll(entity, group) 指定分组,返回全部错误
validateAll(entity, groups...) 多分组,返回全部错误

validateAll 示例(VerifyActivity 用法):

java 复制代码
VerifyResult result = EntityValidator.validateAll(binding.getData());
if (!result.isOk()) {
    StringBuilder sb = new StringBuilder("验证失败(共 ")
            .append(result.getErrors().size()).append(" 项):\n");
    for (FieldVerifyError error : result.getErrors()) {
        sb.append(error.getFieldName()).append(":").append(error.getErrorMsg()).append('\n');
    }
    showToast(sb.toString().trim());
    return;
}

单错误 + 字段名(VerifyTopActivity 用法):

java 复制代码
VerifyResult result = EntityValidator.validate(binding.getData(), VerifyGroup.Create.class);
if (!result.isOk()) {
    showToast(String.format("验证失败[%s]:%s",
            result.getFieldName(), result.getErrorMsg()));
    return;
}

VerifyResult 结果对象

java 复制代码
public class VerifyResult {
    boolean isOk();           // 是否通过
    boolean isFail();         // 是否失败
    String getErrorMsg();     // 错误信息(validateAll 时为多行汇总)
    String getFieldName();    // v3.1:首个失败字段名

    List<FieldVerifyError> getErrors();  // v3.1:全部错误项

    static VerifyResult ok();
    static VerifyResult fail(String errorMsg);
    static VerifyResult fail(String fieldName, String errorMsg);
    static VerifyResult aggregate(List<FieldVerifyError> errors);
}
java 复制代码
public class FieldVerifyError {
    String getFieldName();
    String getErrorMsg();
}

与 Room 数据库配合(重要)

Person 等实体常同时作为 Room @Entity 与表单 Bean,推荐做法:

1. 持久化字段:保留 @ColumnInfo

java 复制代码
@Entity
@VerifyEntity(sort = true)
public class Person extends BaseDaoBean {
    @VerifySort(1)
    @ColumnInfo
    private String name;

    @VerifySort(7)
    @ColumnInfo
    private String mobile;
    // ...
}

不要 为演示校验随意新增 @ColumnInfo 字段,否则会触发 Room Migration。

2. 仅 UI / 校验用的字段:使用 @Ignore

java 复制代码
/** 不参与 Room 持久化,仅用于表单与 Default 分组演示 */
@Ignore
@VerifySort(5)
@VerifyCrossField(refField = "birthday", operator = CrossFieldOperator.GREATER_THAN_OR_EQUAL,
        dateFormat = "yyyy-MM-dd", group = VerifyGroup.Default.class,
        errorMsg = "开学时间不能早于生日!")
private String schoolStartTime;

@Ignore
@VerifyWhen(refField = "age", operator = ConditionOperator.GREATER_THAN_OR_EQUAL, value = "18",
        group = VerifyGroup.Create.class)
@VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Create.class,
        errorMsg = "成年人请填写紧急联系人!")
private String emergencyContact;

@Ignore
private List<Uri> imageList;

3. 嵌套对象、非入库集合

java 复制代码
@Valid(notNull = true, group = VerifyGroup.Create.class)
@Ignore
public Family family;

@ColumnInfo
@Ignore   // 历史写法:若不入库可仅 @Ignore
public List<Family> familyList;

4. 原则小结

需求 做法
入库字段 + 校验 @ColumnInfo + 校验注解
仅表单展示/校验 @Ignore + 校验注解
不想改数据库版本 新增校验字段一律 @Ignore
父类 BaseDaoBean 字段 v3.1 自动扫描父类字段上的注解

完整实战示例 Person

以下为框架 Demo 中 Person 的核心片段(省略 getter/setter):

java 复制代码
@Entity
@VerifyEntity(sort = true)
public class Person extends BaseDaoBean {

    @VerifyField({
            @VerifyParams(type = VerifyType.NOT_EMPTY,
                    group = {VerifyGroup.Default.class, VerifyGroup.Create.class}, errorMsg = "姓名为空!"),
            @VerifyParams(type = VerifyType.LENGTH_RANGE_EQUAL,
                    group = {VerifyGroup.Default.class, VerifyGroup.Create.class},
                    minLength = 2, maxLength = 10, errorMsg = "姓名输入错误!"),
            @VerifyParams(type = VerifyType.EQUALS,
                    group = VerifyGroup.Default.class, errorMsg = "您只能填张三!", equalStr = "张三")
    })
    @VerifySort(1)
    @ColumnInfo
    private String name;

    @Ignore
    @VerifyCrossField(refField = "birthday", operator = CrossFieldOperator.GREATER_THAN_OR_EQUAL,
            dateFormat = "yyyy-MM-dd", group = VerifyGroup.Default.class,
            errorMsg = "开学时间不能早于生日!")
    @VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Default.class,
            errorMsg = "请选择开学时间!")
    @VerifySort(5)
    private String schoolStartTime;

    @VerifyField({
            @VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Default.class,
                    errorMsg = "请填写固话号码!"),
            @VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Create.class,
                    errorMsg = "女性用户请填写座机号码!",
                    when = @VerifyWhen(refField = "sex", operator = ConditionOperator.EQUALS, value = "女")),
            @VerifyParams(type = VerifyType.TEL_PHONE, group = VerifyGroup.Create.class,
                    errorMsg = "座机号码格式不正确!",
                    when = @VerifyWhen(refField = "sex", operator = ConditionOperator.EQUALS, value = "女"))
    })
    @VerifySort(8)
    @ColumnInfo
    private String tel;

    @VerifyCrossField(refField = "height", operator = CrossFieldOperator.LESS_THAN,
            group = VerifyGroup.Create.class, errorMsg = "体重数值应小于身高")
    @VerifySort(10)
    @ColumnInfo
    private String weight;

    @Ignore
    @VerifyWhen(refField = "age", operator = ConditionOperator.GREATER_THAN_OR_EQUAL, value = "18",
            group = VerifyGroup.Create.class)
    @VerifyParams(type = VerifyType.NOT_EMPTY, group = VerifyGroup.Create.class,
            errorMsg = "成年人请填写紧急联系人!")
    private String emergencyContact;

    @Valid(notNull = true, group = VerifyGroup.Create.class, errorMsg = "请选择您的家庭信息!")
    @Ignore
    public Family family;
}

两个 Demo 页面对照

框架 app 模块提供两个对照页面,建议对照源码阅读:

页面 类名 分组 演示重点
普通表单校验 VerifyActivity Default validateAll() 收集全部错误;@Ignore 日期/时间跨字段
顶部标签表单 VerifyTopActivity Create @VerifyWhen 条件校验;@VerifyCrossField 数值比较;@Valid 嵌套 Family

VerifyActivity 关键代码:

java 复制代码
VerifyResult verifyResult = EntityValidator.validateAll(binding.getData());
showToast(formatVerifyResult(verifyResult)); // 展示 fieldName + errorMsg 列表

VerifyTopActivity 关键代码:

java 复制代码
VerifyResult verifyResult = EntityValidator.validate(binding.getData(), VerifyGroup.Create.class);
showToast(formatCreateVerifyResult(verifyResult)); // 验证失败[fieldName]:errorMsg

建议动手验证:

  1. VerifyActivity:将开学时间改为早于生日 → 跨字段失败
  2. VerifyTopActivity:性别改「女」并清空座机 → 条件校验失败
  3. VerifyTopActivity:体重改大于身高 → 跨字段失败
  4. VerifyTopActivity:清空 family → 嵌套校验失败

ProGuard 混淆配置

Release 包请在 proguard-rules.pro 保留注解与校验器:

proguard 复制代码
-keep @interface io.coderf.arklab.annotation.annotation.**
-keep @interface io.coderf.arklab.annotation.format.FormatDecimal
-keep class io.coderf.arklab.annotation.bean.** { *; }
-keep class io.coderf.arklab.annotation.verify.EntityValidator { *; }
-keep class io.coderf.arklab.annotation.enums.** { *; }
-keep class io.coderf.arklab.annotation.inter.** { *; }
-keepattributes *Annotation*

版本升级说明(2.x → 3.1)

变更项 旧版 3.1
排序注解名 @VerifyFieldSort @VerifySort(旧名请全局替换)
空值布尔属性 @VerifyParams(notNull/notEmpty) 使用 VerifyType.NOTNULL / NOT_EMPTY
校验异常 静默返回成功 返回 fail("验证过程发生异常:...")
嵌套 @Valid 分组 丢失,回落 Default 正确传递当前分组
父类字段 不扫描 自动扫描
条件 / 跨字段 不支持 @VerifyWhen / @VerifyCrossField
批量错误 不支持 validateAll() + getErrors()
失败字段名 不支持 getFieldName()
新校验类型 --- 身份证、URL、邮编、年龄、日期时间等
Maven 版本 2.0.0 3.1.0

升级建议:

  1. 依赖版本改为 3.1.0
  2. 全局替换 VerifyFieldSortVerifySort
  3. notNull = true 规则改为 type = VerifyType.NOTNULL
  4. notEmpty = true 规则改为 type = VerifyType.NOT_EMPTY
  5. 原有 EntityValidator.validate(entity) 无需修改,完全兼容

常见问题 FAQ

Q1:没有 @VerifyEntity 会校验吗?

不会,直接返回 VerifyResult.ok()

Q2:字段 private 能校验吗?

可以,内部会 field.setAccessible(true)

Q3:为什么有的规则空值不报错?

NOTNULL / NOT_EMPTY 外,其他类型默认「无值跳过」;需要必填请单独加 NOT_EMPTY

Q4:同一字段 @VerifyField 里规则顺序?

按数组声明顺序依次校验,遇到第一个失败即停止(validate 模式)。

Q5:Room Entity 新增校验字段会改表结构吗?

只要加 @Ignore 就不会;需要入库才加 @ColumnInfo 并做 Migration。

Q6:@FormatDecimal 还能用吗?

@Deprecated,建议业务层自行格式化;APT 处理器仍保留兼容。

Q7:如何自定义 VerifyGroup?

在项目中定义空接口即可,例如 public interface VerifyGroup { interface Submit {} },然后在 @VerifyParams(group = Submit.class) 中使用。


写在最后

从旧文「手写 if + 反射入门」到 v3.1 独立 Module,这套注解校验框架已经覆盖:

  • ✅ 多规则、多 errorMsg、多分组
  • ✅ 排序校验、嵌套对象
  • ✅ 条件校验、跨字段比较
  • ✅ 批量错误、字段名定位
  • ✅ Room 友好、父类字段、异常安全

代码仓库 Demo:VerifyActivity / VerifyTopActivity / Person / Family

如在 CSDN 阅读本文,欢迎收藏、点赞,有问题评论区留言。


相关链接


文档版本:2026-06,对应 annotation module v3.1.0