跳到主要内容Java 注解与反射实战:自定义日志与参数校验实现 | 极客日志Javajava
Java 注解与反射实战:自定义日志与参数校验实现
Java 注解与反射技术可解决开发中的重复逻辑问题。通过两个实战案例,演示了如何自定义日志记录与参数校验注解。核心在于定义运行时保留的元注解,利用反射拦截方法调用并解析属性。案例涵盖日志注解自动记录耗时与异常,以及参数校验注解强制规则检查。最后总结设计原则与优化方向,帮助开发者减少重复代码,提升框架扩展能力。
全栈工匠2 浏览 引言
日常开发中,重复的样板代码往往让人头疼。比如每个重要方法都要写'开始执行'、'参数是 xxx'、'执行结束'的日志记录;或者判断输入是否为 null、年龄是否在合理范围等参数校验。
注解配合反射正是解决这类问题的利器——用注解标记需要处理的地方,用反射自动执行逻辑,实现'一次定义,多处复用'。下面通过两个实战案例,带你从零实现自定义日志注解和参数校验注解。
案例一:自定义日志注解 @Log
需求分析
我们需要一个注解,标记在方法上后能自动完成:
- 记录方法开始执行的时间
- 打印方法参数(可选)
- 记录方法执行耗时
- 打印返回结果(可选)
- 捕获并记录方法抛出的异常
步骤 1:定义 @Log 注解
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;
}
元注解说明:
@Target(ElementType.METHOD):限制注解仅用于方法,符合日志记录场景。
@Retention(RetentionPolicy.RUNTIME):必须保留到运行时,否则反射无法获取。
步骤 2:创建使用 @Log 注解的业务类
public {
String {
(amount <= ) {
();
}
{
Thread.sleep();
} (InterruptedException e) {
e.printStackTrace();
}
+ System.currentTimeMillis();
}
{
(orderId == || orderId.isEmpty()) {
();
}
{
Thread.sleep();
} (InterruptedException e) {
e.printStackTrace();
}
}
}
class
OrderService
@Log(description = "创建订单", recordParams = true, recordResult = true)
public
createOrder
(String userId, double amount)
if
0
throw
new
IllegalArgumentException
"金额必须大于 0"
try
100
catch
return
"ORDER_"
@Log(description = "取消订单", recordParams = true, recordResult = false)
public
void
cancelOrder
(String orderId)
if
null
throw
new
IllegalArgumentException
"订单 ID 不能为空"
try
50
catch
步骤 3:实现注解解析器(核心逻辑)
通过反射拦截方法调用,解析 @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);
String description = logAnnotation.description();
boolean recordParams = logAnnotation.recordParams();
boolean recordResult = logAnnotation.recordResult();
boolean recordException = logAnnotation.recordException();
long startTime = System.currentTimeMillis();
System.out.println("\n===== 【日志开始】" + (description.isEmpty() ? methodName : description) + " =====");
if (recordParams) {
System.out.println("参数:" + Arrays.toString(args));
}
Object result;
try {
result = method.invoke(target, args);
} catch (Exception e) {
if (recordException) {
System.out.println("执行异常:" + e.getCause().getMessage());
}
throw e;
}
long endTime = System.currentTimeMillis();
System.out.println("耗时:" + (endTime - startTime) + "ms");
if (recordResult) {
System.out.println("返回值:" + result);
}
System.out.println("===== 【日志结束】" + (description.isEmpty() ? methodName : description) + " =====");
return result;
} catch (Exception e) {
throw new RuntimeException("方法执行失败:" + e.getMessage(), e);
}
}
}
步骤 4:测试日志注解效果
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) {
}
}
}
执行结果
===== 【日志开始】创建订单 =====
参数:[user_001, 99.9]
耗时:101ms
返回值:ORDER_1698765432100
===== 【日志结束】创建订单 =====
===== 【日志开始】取消订单 =====
参数:[ORDER_123456]
耗时:50ms
===== 【日志结束】取消订单 =====
===== 【日志开始】创建订单 =====
参数:[user_002, -10.0]
执行异常:金额必须大于 0
===== 【日志结束】创建订单 =====
案例二:自定义参数校验注解
需求分析
@NotNull:标记参数不能为 null
@Range:标记数值参数必须在指定范围内(如年龄 1-120 岁)
实现效果:方法调用时自动校验参数,不符合规则则抛出明确的异常信息。
步骤 1:定义校验注解
1.1 @NotNull 注解
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "参数不能为 null";
}
1.2 @Range 注解
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Range {
long min() default 0;
long max() default Long.MAX_VALUE;
String message() default "参数不在指定范围内";
}
步骤 2:创建使用校验注解的业务类
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);
}
}
步骤 3:实现参数校验器(核心逻辑)
通过反射获取方法参数上的注解,逐个校验参数是否符合注解规则:
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);
}
}
步骤 4:测试参数校验效果
public class ValidatorTest {
public static void main(String[] args) {
UserService userService = new UserService();
try {
ParamValidator.validate(userService, "addUser", "user_001", 25);
} catch (Exception e) {
System.out.println("错误:" + e.getMessage());
}
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());
}
}
}
执行结果
新增用户成功:userId=user_001, age=25
错误:用户 ID 不能为空
错误:年龄必须在 1-120 岁之间
错误:积分不能为负数
实战总结:设计原则与优化方向
1. 注解设计三要素
- 明确作用范围:用
@Target 严格限制注解的使用场景(如日志注解只用于方法)。
- 合理生命周期:需要反射解析的注解必须用
@Retention(RUNTIME)。
- 属性精简实用:只保留必要的属性(如日志注解的
description、校验注解的 message),并设置合理默认值。
2. 反射解析优化技巧
- 缓存反射结果:
Method、Parameter 等对象的获取有性能开销,可通过 ConcurrentHashMap 缓存注解与方法的映射关系。
- 批量处理注解:用
getAnnotations() 一次性获取所有注解,避免多次反射调用。
- 异常友好提示:校验或日志失败时,异常信息要明确(如 "年龄必须在 1-120 岁之间" 而非 "参数错误")。
3. 与现有框架的对比
- 日志注解:Spring 的
@Log、Lombok 的 @Log4j2 功能更完善,但自定义注解可灵活适配业务需求(如对接特定日志系统)。
- 参数校验:JSR-303 规范(
javax.validation)提供了丰富的校验注解,但自定义注解可实现框架不支持的特殊校验(如手机号格式、身份证号规则)。
结语
注解是'声明式编程'的载体,让代码更简洁、意图更明确;反射是注解的'执行引擎',让标记转化为实际逻辑。这对组合在框架开发(如 Spring、MyBatis)中无处不在,掌握它们能让你从'使用者'升级为'设计者'。
可以尝试扩展本文案例 —— 给日志注解增加 level 属性(INFO/ERROR),实现不同级别日志的输出;或新增 @Pattern 注解,校验字符串是否符合正则表达式(如手机号、邮箱)。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online