如何通过自定义注解实现零代码侵入的方法日志记录

如何通过自定义注解实现零代码侵入的方法日志记录

  • 方案一:动态代理 + 反射
  • 利用 Java 原生动态代理和反射机制,对标记自定义注解的方法做代理增强:通过代理类拦截目标方法执行,在方法执行前后 / 异常时解析注解信息并输出日志,全程仅依赖 Java 原生 API,无第三方框架依赖。
  1. 先创建自定义注解
java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    // 日志描述
    String value() default "";
}
  1. 定义需要实现的测试接口
java 复制代码
public interface UserService {
    Integer testMyLog(int a1, int a2);
}
  1. 对该接口的具体实现并加上自定义注解
java 复制代码
import com.example.annotation.MyLog;
import com.example.user.UserService;

public class UserServiceImpl implements UserService {
    @Override
    @MyLog("测试自定义注解")
    public Integer testMyLog(int a1, int a2) {
        return a1+a2;
    }
}
  1. 接下来实现方法的动态代理,以及在实现具体日志的输出
java 复制代码
import com.example.annotation.MyLog;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

@Slf4j
public class LogProxyInterceptor implements InvocationHandler {

    private final Object target;

    public LogProxyInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // 步骤1:从实现类方法上获取@MyLog注解
        Method implMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
        MyLog myLog = implMethod.getAnnotation(MyLog.class);
        //判断是否有自定义注解,若有则实现相应的日志输出
        boolean hasLog = myLog != null;

        String methodName = method.getName();
        Object result = null;

        try {
            // 步骤2:方法执行前记录日志
            if (hasLog) {
                logBefore(methodName, myLog.value(), args);
            }
            
            // 步骤3:执行目标方法
            result = method.invoke(target, args);

            // 步骤4:方法执行成功后记录日志
            if (hasLog) {
                logAfter(methodName, result);
            }
            
            return result;
        } catch (Exception e) {
            // 步骤5:方法执行异常时记录日志
            if (hasLog) {
                logException(methodName, e);
            }
            throw e;
        }
    }

    /**
     * 记录方法执行前的日志
     * @param methodName 方法名
     * @param description 注解描述
     * @param args 方法参数
     */
    private void logBefore(String methodName, String description, Object[] args) {

        if (args == null || args.length == 0) {
            // 无参数的情况
            log.info("========== 方法调用开始 ==========\n" +
                     "操作描述: {}\n" +
                     "方法名称: {}\n" +
                     "参数信息: 无参数\n" +
                     "==================================", 
                     description, methodName);
        } else {
            // 有参数的情况
            log.info("========== 方法调用开始 ==========\n" +
                     "操作描述: {}\n" +
                     "方法名称: {}\n" +
                     "参数个数: {}\n" +
                     "参数详情: {}\n" +
                     "==================================", 
                     description, methodName, args.length, Arrays.toString(args));
        }
    }

    /**
     * 记录方法执行成功后的日志
     * @param methodName 方法名
     * @param result 方法返回值
     */
    private void logAfter(String methodName, Object result) {
        log.info("========== 方法调用成功 ==========\n" +
                 "方法名称: {}\n" +
                 "返回结果: {}\n" +
                 "==================================", 
                 methodName, result);
    }

    /**
     * 记录方法执行异常的日志
     * @param methodName 方法名
     * @param e 异常对象
     */
    private void logException(String methodName, Exception e) {
        log.error("========== 方法调用异常 ==========\n" +
                  "方法名称: {}\n" +
                  "异常类型: {}\n" +
                  "异常信息: {}\n" +
                  "==================================", 
                  methodName, e.getClass().getSimpleName(), e.getMessage());
    }

    /**
     * 创建代理对象
     * @param target 目标对象
     * @return 代理对象
     */
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogProxyInterceptor(target)
        );
    }
}
  • 方案二:基于Spring AOP
  • 利用 Spring AOP 的 "面向切面" 特性,将日志记录逻辑封装为独立切面,通过@annotation(myLog)切入点匹配所有标记@MyLog的方法,实现日志统一增强,依托 Spring 容器自动管理代理对象。
  1. 引入AOP依赖
java 复制代码
    <!-- Spring Boot AOP Starter(核心依赖) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  1. 创建自定义注解
java 复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
    //方法描述
    String description() default "";
}
  1. 创建测试方法
java 复制代码
import com.example.annotation.MyLog;
import org.springframework.stereotype.Service;

@Service
public class TestService {

    @MyLog(description = "测试方法日志")
    public void testMyLog() {
        System.out.println("testMyLog");
    }

    @MyLog(description = "带参数的方法")
    public String testWithParams(String name, int age) {
        System.out.println("testWithParams: " + name + ", " + age);
        return "result: " + name;
    }

    @MyLog(description = "有返回值的方法")
    public String testWithReturn() {
        System.out.println("testWithReturn");
        return "Hello, AOP!";
    }

    @MyLog(description = "会抛出异常的方法")
    public void testWithException() {
        System.out.println("testWithException");
        throw new RuntimeException("测试异常");
    }
}
  1. 创建切面类
java 复制代码
import com.example.annotation.MyLog;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Aspect
@Component
@Slf4j
public class MethodLogAspect {

    /**
     * 环绕通知:拦截带有@MyLog注解的方法
     * 在方法执行前后记录日志信息
     *
     * @param joinPoint 连接点,包含方法执行的上下文信息
     * @param myLog     自定义日志注解实例
     * @return 方法执行结果
     * @throws Throwable 方法执行异常时抛出
     */
    @Around("@annotation(myLog)")
    public Object around(ProceedingJoinPoint joinPoint, MyLog myLog) throws Throwable {

        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        Object result = null;

        try {
            // 步骤1:方法执行前记录日志
            logBefore(methodName, myLog.description(), joinPoint.getArgs());
            
            // 步骤2:执行目标方法
            result = joinPoint.proceed();

            // 步骤3:方法执行成功后记录日志
            logAfter(methodName, result);
            
            return result;
        } catch (Throwable throwable) {
            // 步骤4:方法执行异常时记录日志
            logException(methodName, throwable);
            throw throwable;
        }
    }

    /**
     * 记录方法执行前的日志
     * 
     * @param methodName  方法名
     * @param description 注解描述
     * @param args        方法参数
     */
    private void logBefore(String methodName, String description, Object[] args) {
        if (args == null || args.length == 0) {
            // 无参数的情况
            log.info("========== 方法调用开始 ==========\n" +
                     "操作描述: {}\n" +
                     "方法名称: {}\n" +
                     "参数信息: 无参数\n" +
                     "==================================", 
                     description, methodName);
        } else {
            // 有参数的情况
            log.info("========== 方法调用开始 ==========\n" +
                     "操作描述: {}\n" +
                     "方法名称: {}\n" +
                     "参数个数: {}\n" +
                     "参数详情: {}\n" +
                     "==================================", 
                     description, methodName, args.length, Arrays.toString(args));
        }
    }

    /**
     * 记录方法执行成功后的日志
     * 
     * @param methodName 方法名
     * @param result     方法返回值
     */
    private void logAfter(String methodName, Object result) {
        log.info("========== 方法调用成功 ==========\n" +
                 "方法名称: {}\n" +
                 "返回结果: {}\n" +
                 "==================================", 
                 methodName, result);
    }

    /**
     * 记录方法执行异常的日志
     * 
     * @param methodName 方法名
     * @param throwable  异常对象
     */
    private void logException(String methodName, Throwable throwable) {
        log.error("========== 方法调用异常 ==========\n" +
                  "方法名称: {}\n" +
                  "异常类型: {}\n" +
                  "异常信息: {}\n" +
                  "==================================", 
                  methodName, throwable.getClass().getSimpleName(), throwable.getMessage());
    }
}
对比维度 动态代理 + 反射 Spring AOP
框架依赖 无(纯 Java 原生) 强依赖 Spring
代理方式 JDK 动态代理(仅支持接口) 默认 JDK 代理,可配置 CGLIB(支持类)
对象管理 手动创建代理对象 Spring 容器自动管理
侵入性 零侵入,但需手动替换业务对象 完全零侵入(Spring 自动代理)
扩展性 扩展需修改代理类,灵活性低 切面解耦,扩展灵活
适用场景 非 Spring 项目、轻量级 Java 项目 Spring Boot/Spring MVC 项目、企业级应用
  • 两种方案均通过 "自定义注解 + 代理增强" 实现零侵入日志记录
    • 若项目无 Spring 依赖、追求轻量,选「动态代理 + 反射」;
    • 若项目基于 Spring 生态、需灵活扩展,选「Spring AOP」。
相关推荐
橙子不要熬夜2 小时前
构建一个会调用工具和定时任务的AI智能助手
后端
Gopher_HBo2 小时前
图解Go语言逃逸
后端
MiNG MENS2 小时前
Spring Boot + Vue 全栈开发实战指南
vue.js·spring boot·后端
2601_949816582 小时前
Spring Boot--@PathVariable、@RequestParam、@RequestBody
java·spring boot·后端
SimonKing2 小时前
轻量级富文本编辑器Quill,保姆级教程,5分钟快速上手
java·后端·程序员
做个文艺程序员2 小时前
生产级 AI 服务:限流、鉴权与可观测性【OpenClAW + Spring Boot 系列 第6篇·终章】
人工智能·spring boot·后端
Ares-Wang2 小时前
flask》》信号
后端·python·flask
IT_陈寒2 小时前
JavaScript性能优化完全指南
前端·人工智能·后端
xyyaihxl2 小时前
springboot系列--自动配置原理
java·spring boot·后端