引言
日常开发中,日志记录和参数校验往往充斥着重复代码。每个方法都要写'开始执行'、'参数是 xxx',或者判断 null、范围是否合法。这些工作机械且冗余。注解 + 反射正是解决这类问题的利器——用注解标记需要处理的地方,用反射自动执行逻辑,实现'一次定义,多处复用'。
本文将带你从零实现两个实用案例:
- 自定义日志注解
@Log:自动记录方法调用细节; - 自定义参数校验注解
@NotNull、@Range:自动校验方法参数合法性。
案例一:自定义日志注解 @Log
需求分析
我们需要一个注解,标记在方法上后,能自动完成:记录方法开始执行的时间、打印方法参数(可选)、记录方法执行耗时、打印返回结果(可选)、捕获并记录方法抛出的异常。
定义注解
首先,定义 @Log 注解。关键在于元注解的配置:@Target(ElementType.METHOD) 限制注解仅用于方法,@Retention(RetentionPolicy.RUNTIME) 必须保留到运行时,否则反射无法获取。
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String description() default "";
boolean recordParams() default true;
boolean recordResult() default true;
boolean recordException() default true;
}
业务类使用
接下来创建一个业务类来使用这个注解。注意看 createOrder 方法,我们标记了描述、参数记录和返回值记录。
public class OrderService {
@Log(description = "创建订单", recordParams = true, recordResult = true)
public String createOrder(String userId, double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("金额必须大于 0");
}
try { Thread.sleep(100); } catch (InterruptedException e) {}
return "ORDER_" + System.currentTimeMillis();
}
@Log(description = "取消订单", recordParams = true, recordResult = false)
public void cancelOrder(String orderId) {
if (orderId == null || orderId.isEmpty()) {
throw new IllegalArgumentException("订单 ID 不能为空");
}
try { Thread.sleep(50); } catch (InterruptedException e) {}
}
}
核心处理器实现
这是最关键的一步。通过反射拦截方法调用,解析 @Log 注解并执行日志记录逻辑。这里要注意异常的处理,我们捕获异常但继续抛出,不掩盖业务逻辑。
import java.lang.reflect.Method;
import java.util.Arrays;
public class LogAnnotationProcessor {
public static Object executeWithLog(Object target, String methodName, Object... args) {
try {
Class<?>[] paramTypes = Arrays.stream(args)
.map(arg -> arg == null ? Object.class : arg.getClass())
.toArray(Class[]::new);
Method method = target.getClass().getMethod(methodName, paramTypes);
if (!method.isAnnotationPresent(Log.class)) {
return method.invoke(target, args);
}
Log logAnnotation = method.getAnnotation(Log.class);
long startTime = System.currentTimeMillis();
System.out.println("\n===== 【日志开始】" + (logAnnotation.description().isEmpty() ? methodName : logAnnotation.description()) + " =====");
if (logAnnotation.recordParams()) {
System.out.println("参数:" + Arrays.toString(args));
}
Object result;
try {
result = method.invoke(target, args);
} catch (Exception e) {
if (logAnnotation.recordException()) {
System.out.println("执行异常:" + e.getCause().getMessage());
}
throw e;
}
long endTime = System.currentTimeMillis();
System.out.println("耗时:" + (endTime - startTime) + "ms");
if (logAnnotation.recordResult()) {
System.out.println("返回值:" + result);
}
System.out.println("===== 【日志结束】" + (logAnnotation.description().isEmpty() ? methodName : logAnnotation.description()) + " =====");
return result;
} catch (Exception e) {
throw new RuntimeException("方法执行失败:" + e.getMessage(), e);
}
}
}
验证效果
运行测试代码,观察控制台输出。可以看到参数、耗时、返回值都被自动记录了,异常情况也被捕获。
public class LogTest {
public static void main(String[] args) {
OrderService orderService = new OrderService();
LogAnnotationProcessor.executeWithLog(orderService, "createOrder", "user_001", 99.9);
LogAnnotationProcessor.executeWithLog(orderService, "cancelOrder", "ORDER_123456");
try {
LogAnnotationProcessor.executeWithLog(orderService, "createOrder", "user_002", -10.0);
} catch (Exception e) {}
}
}
[图示:日志注解执行流程图]
案例二:自定义参数校验注解
需求分析
我们需要两个注解:@NotNull 标记参数不能为 null;@Range 标记数值参数必须在指定范围内。实现效果是方法调用时自动校验参数,不符合规则则抛出明确的异常信息。
定义校验注解
这两个注解都标记在参数上,所以 @Target 设为 ElementType.PARAMETER。
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "参数不能为 null";
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Range {
long min() default 0;
long max() default Long.MAX_VALUE;
String message() default "参数不在指定范围内";
}
业务类使用
在 UserService 中应用这些注解,比如新增用户时校验 ID 和年龄。
public class UserService {
public void addUser(
@NotNull(message = "用户 ID 不能为空") String userId,
@Range(min = 1, max = 120, message = "年龄必须在 1-120 岁之间") int age) {
System.out.println("新增用户成功:userId=" + userId + ", age=" + age);
}
public void updatePoints(
@NotNull String userId,
@Range(min = 0, message = "积分不能为负数") int points) {
System.out.println("更新积分成功:userId=" + userId + ", points=" + points);
}
}
校验器实现
通过反射获取方法参数上的注解,逐个校验参数是否符合注解规则。注意对数值类型的转换处理。
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
public class ParamValidator {
public static void validate(Object target, String methodName, Object... args) throws Exception {
Class<?>[] paramTypes = Arrays.stream(args)
.map(arg -> arg == null ? Object.class : arg.getClass())
.toArray(Class[]::new);
Method method = target.getClass().getMethod(methodName, paramTypes);
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter param = parameters[i];
Object arg = args[i];
if (param.isAnnotationPresent(NotNull.class)) {
NotNull notNull = param.getAnnotation(NotNull.class);
if (arg == null) {
throw new IllegalArgumentException(notNull.message());
}
}
if (param.isAnnotationPresent(Range.class) && arg instanceof Number) {
Range range = param.getAnnotation(Range.class);
long value = ((Number) arg).longValue();
if (value < range.min() || value > range.max()) {
throw new IllegalArgumentException(range.message());
}
}
}
method.invoke(target, args);
}
}
验证效果
测试正常参数、null 参数、超出范围参数等情况。
public class ValidatorTest {
public static void main(String[] args) {
UserService userService = new UserService();
try { ParamValidator.validate(userService, "addUser", "user_001", 25); } catch (Exception e) {}
try { ParamValidator.validate(userService, "addUser", null, 25); } catch (Exception e) { System.out.println("错误:" + e.getMessage()); }
try { ParamValidator.validate(userService, "addUser", "user_002", 150); } catch (Exception e) { System.out.println("错误:" + e.getMessage()); }
try { ParamValidator.validate(userService, "updatePoints", "user_003", -10); } catch (Exception e) { System.out.println("错误:" + e.getMessage()); }
}
}
[图示:参数校验流程图]
设计原则与优化
注解设计三要素
明确作用范围,用 @Target 严格限制场景;合理生命周期,需要反射解析的注解必须用 @Retention(RUNTIME);属性精简实用,只保留必要属性并设置默认值。
反射解析优化技巧
实际项目中,Method、Parameter 等对象的获取有性能开销,可通过 ConcurrentHashMap 缓存注解与方法的映射关系。批量处理注解,用 getAnnotations() 一次性获取。异常友好提示,校验或日志失败时,异常信息要明确。
与现有框架对比
日志注解方面,Spring 的 @Log、Lombok 的 @Log4j2 功能更完善,但自定义注解可灵活适配业务需求。参数校验方面,JSR-303 规范提供了丰富的校验注解,但自定义注解可实现框架不支持的特殊校验。
总结
注解是'声明式编程'的载体,让代码更简洁、意图更明确;反射是注解的'执行引擎',让标记转化为实际逻辑。这对组合在框架开发中无处不在,掌握它们能让你从'使用者'升级为'设计者'。
扩展思考
尝试扩展本文案例:给日志注解增加 level 属性(INFO/ERROR),实现不同级别日志的输出;新增 @Pattern 注解,校验字符串是否符合正则表达式。

