题目
实现基于注解的字段非空校验框架
信息
- 类型:问答
- 难度:⭐⭐
考点
自定义注解定义, 反射读取注解信息, 动态字段校验, 异常处理, 框架设计思想
快速回答
实现步骤:
- 定义
@NotNull注解标记需要校验的字段 - 通过反射获取目标对象的所有字段
- 检查字段是否被
@NotNull注解标记 - 对标记字段进行可访问性处理并校验值
- 抛出包含字段名的自定义异常
核心代码:
public static void validate(Object obj) throws IllegalAccessException {
for (Field field : obj.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(NotNull.class)) {
field.setAccessible(true);
if (field.get(obj) == null) {
throw new ValidationException(field.getName() + " cannot be null");
}
}
}
}
## 解析
问题场景
在实际开发中,经常需要对Java对象的字段进行校验(如非空检查)。通过注解+反射实现通用校验框架,可避免重复代码并提升可维护性。
完整实现方案
1. 定义注解
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.RUNTIME) // 必须设置为运行时
@Target(ElementType.FIELD) // 仅允许标注字段
public @interface NotNull {
String message() default "Field cannot be null"; // 可定制错误信息
}2. 实现校验器
import java.lang.reflect.Field;
public class Validator {
public static void validate(Object target) throws ValidationException {
Class<?> clazz = target.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(NotNull.class)) {
try {
field.setAccessible(true); // 突破私有字段限制
Object value = field.get(target);
if (value == null) {
NotNull annotation = field.getAnnotation(NotNull.class);
throw new ValidationException(
field.getName() + ": " + annotation.message()
);
}
} catch (IllegalAccessException e) {
throw new ValidationException("Access failed for " + field.getName(), e);
}
}
}
}
}
// 自定义异常
public class ValidationException extends Exception {
public ValidationException(String message) { super(message); }
public ValidationException(String message, Throwable cause) { super(message, cause); }
}3. 使用示例
public class User {
@NotNull(message = "用户名不能为空")
private String username;
@NotNull
private String email;
// 构造器及getter/setter省略
}
// 测试代码
public class Main {
public static void main(String[] args) {
User user = new User();
user.setEmail("test@example.com"); // username未赋值
try {
Validator.validate(user);
} catch (ValidationException e) {
System.out.println("校验失败: " + e.getMessage());
// 输出: 校验失败: username: 用户名不能为空
}
}
}核心原理
- 注解保留策略:
@Retention(RetentionPolicy.RUNTIME)使注解在运行时可通过反射读取 - 反射操作:
getDeclaredFields()获取所有字段,isAnnotationPresent()检查注解 - 字段访问控制:
setAccessible(true)绕过private修饰符的访问限制(需考虑安全管理器) - 动态取值:
field.get(obj)获取字段当前值
最佳实践
- 性能优化:对Class对象使用缓存,避免重复解析注解
- 安全考虑:在开放环境中使用需检查
field.setAccessible()的权限 - 扩展性设计:可结合
@Constraint元注解实现JSR-303风格的校验框架 - 链式校验:支持收集所有错误而非遇到第一个错误就终止
常见错误
| 错误类型 | 示例 | 解决方案 |
|---|---|---|
| 注解保留策略错误 | 使用@Retention(SOURCE) | 必须设置为RUNTIME |
| 未处理访问异常 | 忽略IllegalAccessException | 合理封装异常信息 |
| 未重置访问权限 | 修改accessible标志后未还原 | 使用try-finally重置状态 |
| 性能问题 | 每次校验都解析注解 | 缓存Class的注解信息 |
扩展知识
- 组合注解:通过
@Constraint(validatedBy=...)实现校验器绑定 - 类型适配:处理基础类型(int等)时,需注意默认值0不会触发非空校验
- 框架集成:Spring的
Validator接口底层采用类似机制 - 字节码增强:Lombok使用APT在编译时生成代码,避免运行时反射开销