本文最后更新于 2025-11-27,文章内容可能已经过时。

  1. 精准拦截:@annotation + @within 覆盖方法级类级注解(不用写两套切点)。
  2. 灵活配置:通过注解参数控制是否记录参数/结果(避免日志爆炸)。
  3. 日志清晰:输出格式包含类型(类级/方法级)、耗时、参数、结果,一眼看懂。
  4. 无侵入:只需在方法/类上加@Loggable,不用改业务逻辑。

简单实现代码

1、创建自定义注解(Loggable.java)

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义日志注解:方法/类级别使用,自动记录调用日志
 * 使用示例:
 *
 * @Loggable // 方法级
 * @Loggable // 类级(作用于类中所有方法)
 */
@Target({ElementType.METHOD, ElementType.TYPE}) // 作用于方法或类
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface Loggable {
    boolean recordParams() default true; // 是否记录参数
    boolean recordResult() default true; // 是否记录返回值
}

2、使用示例(UserService.java)

import org.springframework.stereotype.Service;

@Service
@Loggable // 类级注解
public class UserService {

    @Loggable(recordParams = false) // 方法级覆盖
    public String getUserInfo(Long id) { // 自动记录日志
        return "User " + id;
    }

    public String getOrders(Long id) { // 自动记录日志
        return "Orders for " + id;
    }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/user/{id}")
    public String getUser(@PathVariable Long id) {
        return userService.getUserInfo(id);
    }

    @GetMapping("/orders/{id}")
    public String getOrders(@PathVariable Long id) {
        return userService.getOrders(id);
    }
}

3、实现AOP切面(LogAspect.java)

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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

/**
 * 自定义日志切面:精准实现
 * 1. 类上注解 → 类中所有方法自动记录
 * 2. 方法上注解 → 仅当前方法记录(覆盖类级配置)
 * 3. 优先级:方法级 > 类级
 */
@Aspect
@Component // 让Spring管理这个切面
public class LogAspect {

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

    /**
     * 切点:拦截所有被@Loggable标注的方法或类
     * @annotation:方法级注解
     * @within:类级注解(类上标注@Loggable)
     */
    @Around("@annotation(com.nn3n.bootdemolog.anno.Loggable) || @within(com.nn3n.bootdemolog.anno.Loggable)")
    public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {

        // ====== 关键逻辑:优先获取方法级注解,其次类级注解 ======
        Loggable loggable = null;
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        // 1. 优先尝试获取方法级注解
        if (method.isAnnotationPresent(Loggable.class)) {
            loggable = method.getAnnotation(Loggable.class);
        }
        // 2. 如果方法级没有,尝试获取类级注解
        else if (joinPoint.getTarget().getClass().isAnnotationPresent(Loggable.class)) {
            loggable = joinPoint.getTarget().getClass().getAnnotation(Loggable.class);
        }
        // 3. 无任何注解 → 跳过日志记录(安全兜底)
        if (loggable == null) {
            return joinPoint.proceed();
        }

        // ====== 记录调用信息(按注解配置) ======
        long startTime = System.currentTimeMillis();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = method.getName();

        // 参数记录(按注解配置)
        String params = loggable.recordParams()
                ? Arrays.toString(joinPoint.getArgs())
                : "参数记录已关闭(方法级配置)";

        // 执行目标方法
        Object result = joinPoint.proceed();

        // 返回值记录(按注解配置)
        String resultStr = loggable.recordResult()
                ? (result != null ? result.toString() : "null")
                : "结果记录已关闭(方法级配置)";

        // ====== 输出日志(关键!区分方法级/类级) ======
        long executionTime = System.currentTimeMillis() - startTime;
        log.info("【{}】{}.{} | 耗时:{}ms | 参数:{} | 返回:{}",
                method.isAnnotationPresent(Loggable.class) ? "方法级" : "类级",
                className, methodName,
                executionTime, params, resultStr);

        return result;
    }
}

4、使用效果示例

【类级】UserService.getUserInfo | 耗时:0ms | 参数:[456] | 返回:User 456
【类级】UserService.getOrders | 耗时:0ms | 参数:[456] | 返回:Orders for 456
【方法级】UserService.getUserInfo | 耗时:0ms | 参数:参数记录已关闭(方法级配置) | 返回:User 456
【类级】UserService.getOrders | 耗时:0ms | 参数:[456] | 返回:Orders for 456

本方案通过自定义@Loggable注解结合Spring AOP实现方法调用日志记录功能,支持类级和方法级注解灵活配置。核心逻辑:类上添加注解时自动拦截所有方法,方法上添加注解时仅拦截当前方法且优先级高于类级配置;通过AOP切面精准控制参数/结果记录开关,日志输出包含调用类型(类级/方法级)、耗时、参数及返回值,并提供安全兜底机制防止无注解时触发异常。