题目
实现自定义注解验证器:通过反射检查对象属性的合法性
信息
- 类型:问答
- 难度:⭐⭐
考点
自定义注解定义, 反射机制应用, 注解处理器设计, 对象属性验证
快速回答
实现步骤:
- 定义验证注解(如
@NotNull,@Range) - 创建验证工具类,通过反射获取字段注解
- 遍历字段并检查注解规则
- 收集验证错误并返回结果
关键点:
- 使用
Field.getDeclaredAnnotations()获取注解 - 通过
field.setAccessible(true)访问私有字段 - 注意基本类型和包装类型的处理
问题场景
在实际开发中,经常需要对 Java 对象的属性进行合法性校验(如非空检查、数值范围等)。要求通过自定义注解和反射机制实现一个轻量级验证框架,避免重复代码。
解决方案
1. 定义自定义注解
// 非空校验注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotNull {
String message() default "Field cannot be null";
}
// 数值范围注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
long min() default Long.MIN_VALUE;
long max() default Long.MAX_VALUE;
String message() default "Value out of range";
}2. 实现验证工具类
public class Validator {
public static List<String> validate(Object obj) {
List<String> errors = new ArrayList<>();
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true); // 允许访问私有字段
// 处理 @NotNull
if (field.isAnnotationPresent(NotNull.class)) {
try {
Object value = field.get(obj);
if (value == null) {
errors.add(field.getName() + ": " +
field.getAnnotation(NotNull.class).message());
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Field access error", e);
}
}
// 处理 @Range
if (field.isAnnotationPresent(Range.class)) {
try {
Object value = field.get(obj);
if (value != null && Number.class.isAssignableFrom(value.getClass())) {
Range range = field.getAnnotation(Range.class);
long num = ((Number) value).longValue();
if (num < range.min() || num > range.max()) {
errors.add(field.getName() + ": " + range.message() +
" [min=" + range.min() + ", max=" + range.max() + "]");
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Field access error", e);
}
}
}
return errors;
}
}3. 使用示例
public class User {
@NotNull(message = "用户名不能为空")
private String username;
@Range(min = 18, max = 60, message = "年龄必须在18-60岁之间")
private Integer age;
// 构造方法及getter/setter省略
}
// 测试验证
User user = new User(null, 15);
List<String> errors = Validator.validate(user);
// 输出: [username: 用户名不能为空, age: 年龄必须在18-60岁之间 [min=18, max=60]]核心原理
- 注解保留策略:必须使用
@Retention(RetentionPolicy.RUNTIME)使注解在运行时可见 - 反射机制:通过
Class.getDeclaredFields()获取所有字段,Field.get()读取值 - 类型处理:对基本类型(如 int)需特殊处理,示例中使用包装类型避免此问题
最佳实践
- 性能优化:缓存类的字段信息,避免每次验证都反射获取
- 扩展性:使用策略模式,将不同注解的验证逻辑分离到独立处理器中
- 安全访问:在 finally 块重置
field.setAccessible(false)(Java 9+ 需考虑模块系统限制) - 支持嵌套验证:递归处理对象内部的嵌套对象属性
常见错误
| 错误类型 | 后果 | 解决方案 |
|---|---|---|
忘记 field.setAccessible(true) | 无法访问私有字段 | 显式设置访问权限 |
| 未处理基本类型(如 int) | NPE 当字段值为默认值 0 时 | 统一使用包装类型 |
| 忽略注解继承 | 无法验证父类字段 | 递归调用 clazz.getSuperclass() |
扩展知识
- 标准方案对比:Hibernate Validator 使用更完善的规范(JSR 380),支持分组验证、跨参数校验等
- 动态代理进阶:可结合动态代理实现 AOP 验证,在方法调用前后自动执行校验
- 注解处理器:编译期处理注解(如 Lombok),但无法实现运行时动态验证