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

好的,我们来一步步手写两个非常实用的自定义注解 + 反射实现:

  1. @LogExecution:方法执行前后自动打印日志(入参 + 返回值 + 耗时)
  2. @NotBlankParam:方法参数非空/非空白校验(类似 @NotBlank 但作用于方法参数,且用纯反射实现,不依赖 Spring/Hibernate Validator)

我们先用最原始的反射方式实现(不依赖 AOP / Spring),适合理解原理、工具类、测试框架、轻量 SDK 等场景。然后简单提一下如何升级到 Spring AOP 方式。

第一部分:@LogExecution 日志注解(反射实现)

1. 定义注解

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogExecution {

    String value() default "";          // 可自定义日志前缀

    boolean logArgs() default true;     // 是否打印入参

    boolean logResult() default true;   // 是否打印返回值

    boolean logTime() default true;     // 是否打印耗时
}

2. 反射工具类(核心调用逻辑)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class LogExecutionUtil {

    private static final Logger log = LoggerFactory.getLogger(LogExecutionUtil.class);

    /**
     * 包装执行目标方法,自动加日志
     */
    public static Object invokeWithLogging(Object target, Method method, Object[] args) throws Throwable {
        LogExecution anno = method.getAnnotation(LogExecution.class);
        if (anno == null) {
            // 没有注解 → 直接执行
            return method.invoke(target, args);
        }

        String prefix = anno.value().isEmpty() ? method.getName() : anno.value();
        StringBuilder logPrefix = new StringBuilder("[").append(prefix).append("] ");

        // 打印入参
        if (anno.logArgs()) {
            String argsStr = args == null ? "null" : Arrays.toString(args);
            log.info("{}开始执行 → 参数: {}", logPrefix, argsStr);
        }

        long start = System.nanoTime();

        try {
            Object result = method.invoke(target, args);

            // 打印返回值
            if (anno.logResult()) {
                log.info("{}执行完成 → 返回: {}", logPrefix, result);
            }

            // 打印耗时
            if (anno.logTime()) {
                double ms = (System.nanoTime() - start) / 1_000_000.0;
                log.info("{}耗时: {} ms", logPrefix, String.format("%.3f", ms));
            }

            return result;
        } catch (Throwable e) {
            log.error("{}执行异常", logPrefix, e);
            throw e;
        }
    }
}

3. 使用示例

public class UserService {

    @LogExecution(value = "创建用户", logArgs = true, logResult = true, logTime = true)
    public User createUser(String username, int age) {
        if (age < 0) throw new IllegalArgumentException("年龄不能为负");
        return new User(username, age);
    }

    @LogExecution("批量删除")  // 使用默认值
    public void batchDelete(List<Long> ids) {
        System.out.println("删除 " + ids.size() + " 条记录");
    }
}

4. 如何调用(代理 / 手动包装)

最简单的方式:写一个代理类或在调用处手动用 invokeWithLogging

// 手动调用方式
UserService service = new UserService();
Method method = UserService.class.getMethod("createUser", String.class, int.class);
LogExecutionUtil.invokeWithLogging(service, method, new Object[]{"alice", 25});

更常见的是结合 动态代理(JDK Proxy / ByteBuddy / CGLIB)使用。

第二部分:@NotBlankParam 参数非空校验(反射实现)

1. 定义注解(可重复)

import java.lang.annotation.*;

@Repeatable(NotBlankParams.class)
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotBlankParam {

    int index() default -1;           // 参数下标(从0开始),-1表示自动推断(推荐)

    String message() default "参数不能为空且不能为纯空白";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotBlankParams {
    NotBlankParam[] value();
}

2. 校验工具类

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Objects;

public class ParamValidator {

    public static void validateParams(Object target, Method method, Object[] args) {
        Parameter[] parameters = method.getParameters();
        Annotation[][] paramAnnos = method.getParameterAnnotations();

        for (int i = 0; i < parameters.length; i++) {
            for (Annotation anno : paramAnnos[i]) {
                if (anno instanceof NotBlankParam) {
                    NotBlankParam notBlank = (NotBlankParam) anno;
                    int idx = notBlank.index() >= 0 ? notBlank.index() : i;

                    if (idx >= args.length) {
                        throw new IllegalArgumentException("注解 index 越界");
                    }

                    Object value = args[idx];
                    if (value == null || (value instanceof String && ((String) value).trim().isEmpty())) {
                        throw new IllegalArgumentException(notBlank.message() + " (参数位置: " + idx + ")");
                    }
                }
            }
        }
    }
}

3. 使用示例

public class OrderService {

    public void createOrder(
            @NotBlankParam String orderNo,
            @NotBlankParam(message = "收货人姓名必填") String receiverName,
            String remark) {   // remark 不校验

        System.out.println("创建订单:" + orderNo);
    }
}

4. 调用方式(同样结合代理)

// 在方法执行前先校验
ParamValidator.validateParams(target, method, args);
// 再执行方法
method.invoke(target, args);

第三部分:升级方案(生产级常用方式)

场景推荐方式优点实现复杂度
学习/理解原理纯反射 + 手动代理最清晰,理解最透彻★★★☆☆
轻量工具类/SDKJDK动态代理 / ByteBuddy无 Spring 依赖,体积小★★★★☆
Spring Boot 项目Spring AOP + @Aspect零侵入、最优雅★★☆☆☆
高性能场景Annotation Processor + APT / 字节码生成编译期生成代码,无反射开销★★★★★

Spring AOP 快速示例(@LogExecution)

@Aspect
@Component
public class LogExecutionAspect {

    @Around("@annotation(logAnno)")
    public Object around(ProceedingJoinPoint joinPoint, LogExecution logAnno) throws Throwable {
        // 前置日志、入参
        // proceed()
        // 后置日志、耗时、返回值
    }
}

小结 & 进阶方向

  • 学会了 @Retention(RUNTIME) + 反射读取注解 的基本套路
  • 掌握了 方法参数注解 的获取方式(method.getParameterAnnotations()
  • 明白了自定义注解 + 反射的两种主要用途:日志追踪参数/返回值校验
  • 能自己写出简易的 方法切面工具(不依赖框架)

接下来想深入哪个方向?

A. 写一个完整的 JDK动态代理 包装器(自动扫描 @LogExecution / @NotBlankParam)
B. 实现 Spring AOP 版本(AspectJ风格)
C. 做参数校验的 支持分组 / 支持自定义消息国际化
D. 用 Annotation Processor 在编译期校验注解使用是否合法
E. 结合 @Around 做更复杂的场景(事务 + 日志 + 校验 一条龙)

告诉我字母,我继续带你写代码!

文章已创建 4138

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部