跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java 注解与反射实战:自定义日志与参数校验

Java 注解结合反射机制可解决重复代码问题。本文通过自定义日志注解与参数校验注解两个实战案例,演示了如何声明式地处理方法调用细节与参数合法性校验。内容包括注解元数据定义、反射解析核心逻辑、异常捕获及性能优化建议。掌握该组合能提升框架开发能力,从使用者进阶为设计者。

神经兮兮发布于 2026/3/23更新于 2026/10/2397 浏览
Java 注解与反射实战:自定义日志与参数校验

背景与动机

日常开发中,我们常遇到两类重复工作:日志记录(每个方法都要写开始、参数、结束代码)和参数校验(判断 null、范围、格式等)。这些机械且冗余。

注解 + 反射正是解决这类问题的利器——用注解标记需要处理的地方,用反射自动执行逻辑,实现'一次定义,多处复用'。本文将带你从零实现两个实用案例:自定义日志注解 @Log 和自定义参数校验注解 @NotNull、@Range。全程实战,代码可直接运行。

场景一:自定义日志注解 @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):必须保留到运行时,否则反射无法获取。

使用示例

public class OrderService {
    
    
     String  {
         (amount <= ) {
              ();
        }
         {
            Thread.sleep();
        }  (InterruptedException e) {}
          + System.currentTimeMillis();
    }

    
    
       {
         (orderId ==  || orderId.isEmpty()) {
              ();
        }
         {
            Thread.sleep();
        }  (InterruptedException e) {}
    }
}
// 标记日志:记录参数和返回值,描述为"创建订单"
@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

核心逻辑:注解解析器

通过反射拦截方法调用,解析 @Log 注解并执行日志记录逻辑:

import java.lang.reflect.Method;
import java.util.Arrays;

public class LogAnnotationProcessor {
    /**
     * 执行带日志注解的方法
     * @param target 目标对象
     * @param methodName 方法名
     * @param args 方法参数
     * @return 方法返回值
     */
    public static Object executeWithLog(Object target, String methodName, Object... args) {
        try {
            // 1. 获取方法对应的 Class 对象和参数类型
            Class<?>[] paramTypes = Arrays.stream(args)
                    .map(arg -> arg == null ? Object.class : arg.getClass())
                    .toArray(Class[]::new);
            Method method = target.getClass().getMethod(methodName, paramTypes);

            // 2. 检查方法是否有@Log 注解,无则直接执行
            if (!method.isAnnotationPresent(Log.class)) {
                return method.invoke(target, args);
            }

            // 3. 解析注解属性
            Log logAnnotation = method.getAnnotation(Log.class);
            String description = logAnnotation.description();
            boolean recordParams = logAnnotation.recordParams();
            boolean recordResult = logAnnotation.recordResult();
            boolean recordException = logAnnotation.recordException();

            // 4. 记录方法开始日志
            long startTime = System.currentTimeMillis();
            System.out.println("\n===== 【日志开始】" + (description.isEmpty() ? methodName : description) + " =====");
            if (recordParams) {
                System.out.println("参数:" + Arrays.toString(args));
            }

            // 5. 执行目标方法(捕获异常并记录)
            Object result;
            try {
                result = method.invoke(target, args);
            } catch (Exception e) {
                // 记录异常信息
                if (recordException) {
                    System.out.println("执行异常:" + e.getCause().getMessage());
                }
                throw e; // 继续抛出异常,不掩盖业务逻辑
            }

            // 6. 记录方法结束日志
            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);
        }
    }
}

测试效果

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 岁)。

实现效果:方法调用时自动校验参数,不符合规则则抛出明确的异常信息。

定义校验注解

import java.lang.annotation.*;

@Target(ElementType.PARAMETER) // 仅用于方法参数
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
    // 校验失败的提示信息
    String message() default "参数不能为 null";
}
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 "参数不在指定范围内";
}

使用示例

public class UserService {
    // 新增用户:ID 不能为空,年龄必须 1-120 岁
    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);
    }

    // 更新积分:积分必须≥0
    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 {
    /**
     * 校验方法参数是否符合注解规则
     * @param target 目标对象
     * @param methodName 方法名
     * @param args 方法参数
     * @throws IllegalArgumentException 校验失败时抛出
     */
    public static void validate(Object target, String methodName, Object... args) throws Exception {
        // 1. 获取方法对象
        Class<?>[] paramTypes = Arrays.stream(args)
                .map(arg -> arg == null ? Object.class : arg.getClass())
                .toArray(Class[]::new);
        Method method = target.getClass().getMethod(methodName, paramTypes);

        // 2. 获取方法参数列表(包含注解信息)
        Parameter[] parameters = method.getParameters();

        // 3. 遍历参数,执行校验
        for (int i = 0; i < parameters.length; i++) {
            Parameter param = parameters[i];
            Object arg = args[i];

            // 3.1 校验@NotNull 注解
            if (param.isAnnotationPresent(NotNull.class)) {
                NotNull notNull = param.getAnnotation(NotNull.class);
                if (arg == null) {
                    throw new IllegalArgumentException(notNull.message());
                }
            }

            // 3.2 校验@Range 注解(仅对数值类型生效)
            if (param.isAnnotationPresent(Range.class) && arg instanceof Number) {
                Range range = param.getAnnotation(Range.class);
                long value = ((Number) arg).longValue(); // 转为 long 统一处理
                if (value < range.min() || value > range.max()) {
                    throw new IllegalArgumentException(range.message());
                }
            }
        }

        // 4. 校验通过,执行方法
        method.invoke(target, args);
    }
}

测试效果

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());
        }
        // 测试 null 参数(userId 为 null)
        try {
            ParamValidator.validate(userService, "addUser", null, 25);
        } catch (Exception e) {
            System.out.println("错误:" + e.getMessage());
        }
        // 测试年龄超出范围(150 岁)
        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 岁之间 
错误:积分不能为负数 

设计原则与优化

注解设计三要素

  • 明确作用范围:用 @Target 严格限制注解的使用场景(如日志注解只用于方法);
  • 合理生命周期:需要反射解析的注解必须用 @Retention(RUNTIME);
  • 属性精简实用:只保留必要的属性(如日志注解的 description、校验注解的 message),并设置合理默认值。

反射解析优化技巧

  • 缓存反射结果:Method、Parameter 等对象的获取有性能开销,可通过 ConcurrentHashMap 缓存注解与方法的映射关系;
  • 批量处理注解:用 getAnnotations() 一次性获取所有注解,避免多次反射调用;
  • 异常友好提示:校验或日志失败时,异常信息要明确(如 "年龄必须在 1-120 岁之间" 而非 "参数错误")。

与现有框架的对比

  • 日志注解:Spring 的 @Log、Lombok 的 @Log4j2 功能更完善,但自定义注解可灵活适配业务需求(如对接特定日志系统);
  • 参数校验:JSR-303 规范(javax.validation)提供了丰富的校验注解,但自定义注解可实现框架不支持的特殊校验(如手机号格式、身份证号规则)。

总结

注解是'声明式编程'的载体,让代码更简洁、意图更明确;反射是注解的'执行引擎',让标记转化为实际逻辑。这对组合在框架开发(如 Spring、MyBatis)中无处不在,掌握它们能让你从'使用者'升级为'设计者'。

动手练习:尝试扩展本文案例 ——

  1. 给日志注解增加 level 属性(INFO/ERROR),实现不同级别日志的输出;
  2. 新增 @Pattern 注解,校验字符串是否符合正则表达式(如手机号、邮箱)。

更多推荐文章

查看全部
  • Virt-A-Mate v1.22 中文汉化整合版介绍
  • AI Agent 学习路线与核心文章精选
  • C++11 新特性:Lambda 表达式、可变参数模板与包装器
  • NewStarCTF2025 Week2 Web 题目解析
  • Darknet 预测分类:图像数据格式转换与 GPU 加速
  • 采摘机器人毕业设计实战:从机械控制到感知决策的全栈实现
  • Java 日期差计算函数 Bug 导致游戏道具异常发放
  • VSCode GitHub Copilot 插件无模型问题解决方案
  • MySQL 免安装版 Windows 部署配置指南
  • VSCode 中通过扩展使用 OpenAI 兼容模型配置 GitHub Copilot
  • ROS2 功能包创建与 CMake 编译链路详解
  • DeepSeek-R1-Distill-Llama-8B 实战:快速搭建智能问答系统
  • Zotero 8.0.1 英文文献批量下载与自动化管理实战
  • VS Code 前端开发必备插件清单
  • 大模型提示工程实战指南:掌握提问驱动 AI
  • 基于 MCP 协议辅助 JS 逆向分析与调试
  • Qwen3-4B-Instruct-2507 智能写作助手部署与优化
  • Layui 框架下 Unity WebGL Tab 切换黑屏解决方案
  • 软件测试引入人工智能:核心场景、工具与落地实践
  • ComfyUI 是什么:可视化节点式 AI 绘画工具解析

相关免费在线工具

  • 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