Android 自定义注解实现实体类一键校验(完整版 v3.1)
本文是在原博客 Android自定义注解实现一键校验实体类参数 基础上的 完整重写与能力升级。
旧版侧重「从零手写反射校验」的思路讲解;新版将能力沉淀为独立 Module
io.coderf.arklab.annotation:annotation,在保留全部原有 API 习惯的同时,新增 条件校验、跨字段比较、批量错误收集、父类字段扫描、Room 友好实践 等能力。当前版本:
3.1.0| Java 17 | Maven 坐标见下文
目录
- 前言与适用场景
- 快速开始
- 核心设计思路(继承旧文)
- [注解 API 全览](#注解 API 全览)
- [VerifyType 校验类型大全](#VerifyType 校验类型大全)
- [分组校验 VerifyGroup](#分组校验 VerifyGroup)
- [条件校验 @VerifyWhen(v3.1 新增)](#条件校验 @VerifyWhen(v3.1 新增))
- [跨字段校验 @VerifyCrossField(v3.1 新增)](#跨字段校验 @VerifyCrossField(v3.1 新增))
- [嵌套对象校验 @Valid](#嵌套对象校验 @Valid)
- [校验顺序 @VerifySort](#校验顺序 @VerifySort)
- [EntityValidator 调用方式](#EntityValidator 调用方式)
- [VerifyResult 结果对象](#VerifyResult 结果对象)
- [与 Room 数据库配合(重要)](#与 Room 数据库配合(重要))
- [完整实战示例 Person](#完整实战示例 Person)
- [两个 Demo 页面对照](#两个 Demo 页面对照)
- [ProGuard 混淆配置](#ProGuard 混淆配置)
- [版本升级说明(2.x → 3.1)](#版本升级说明(2.x → 3.1))
- [常见问题 FAQ](#常见问题 FAQ)
- 写在最后
前言与适用场景
表单提交前,我们往往要写大量 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),本库外部无法使用,大家可以自行发布项目里已提供好脚本:
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.NOTNULL 与 VerifyType.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.NOTNULL与VerifyType.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:对象本身不能为 nullnotEmpty = 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:30 或 08:30:00 |
ID_CARD |
身份证号 | --- |
URL |
URL 地址 | --- |
POSTAL_CODE |
国内 6 位邮编 | --- |
AGE |
年龄 0~120 | --- |
空值处理约定(重要)
NOTNULL/NOT_EMPTY:无论其他条件,空值直接失败。- 其他类型 :值为 null / 空字符串 / 空集合 / 空 Map 时 跳过该校验(视为「未填写则不校验格式」)。
- 若业务要求「未填写也要报错」,请显式增加一条
NOT_EMPTY或NOTNULL规则。
分组校验 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(自动)、NUMBER、STRING、DATE。
多条件 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
建议动手验证:
- VerifyActivity:将开学时间改为早于生日 → 跨字段失败
- VerifyTopActivity:性别改「女」并清空座机 → 条件校验失败
- VerifyTopActivity:体重改大于身高 → 跨字段失败
- 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 |
升级建议:
- 依赖版本改为
3.1.0 - 全局替换
VerifyFieldSort→VerifySort - 将
notNull = true规则改为type = VerifyType.NOTNULL - 将
notEmpty = true规则改为type = VerifyType.NOT_EMPTY - 原有
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 阅读本文,欢迎收藏、点赞,有问题评论区留言。
相关链接
- 旧版入门文章:https://blog.csdn.net/fzkf9225/article/details/132714575
- Maven 坐标:
io.coderf.arklab.annotation:annotation:3.1.0 - 作者:青丶穗(fz)
文档版本:2026-06,对应 annotation module v3.1.0