Spring Boot 中使用自定义注解和 AOP 实现微服务日志记录(包含 URL、状态码和耗时信息)

目录

在日常的业务开发中,我们经常需要对某些关键方法进行统一的日志记录、调用耗时统计以及输入输出参数的追踪。如果每个方法都手动加日志代码,不仅冗余,还容易出错。

本文将通过 自定义注解 + Spring AOP 切面 的方式,实现一种轻量化的"方法导出器"(MethodExporter),可以对标记的方法自动进行日志上报。


一、需求背景

在很多系统中,尤其是微服务架构下,运维和排查问题时,经常需要知道:

  • 哪个接口被调用了?
  • 输入参数是什么?
  • 返回结果如何?
  • 耗时多少?

传统的做法是在方法里手写 log.info(...),但这样代码会变得非常冗余。

于是,我们可以自定义一个注解 @MethodExporter,只要在方法上标注它,就能自动捕捉调用信息,统一输出日志。


二、定义注解 @MethodExporter

首先我们需要一个自定义注解,作用在方法上,运行时生效:

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 MethodExporter {
}

这个注解本身不包含任何业务逻辑,只是一个"标签",方便 AOP 进行拦截。


三、控制层使用注解

Controller 中,我们只要在需要上报的方法上加上 @MethodExporter

java 复制代码
@RestController
@Slf4j
public class MethodExporterController {

    // http://localhost:24618/method/list?page=1&rows=7
    @GetMapping("/method/list")
    @MethodExporter
    public Map list(@RequestParam(value = "page", defaultValue = "1") int page,
                    @RequestParam(value = "rows", defaultValue = "5") int rows) {

        Map<String, String> result = new LinkedHashMap<>();
        result.put("code", "200");
        result.put("message", "success");

        // 模拟随机耗时
        try {
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return result;
    }

    @GetMapping("/method/get")
    @MethodExporter
    public Map get() {
        Map<String, String> result = new LinkedHashMap<>();
        result.put("code", "404");
        result.put("message", "not-found");

        try {
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return result;
    }

    @GetMapping("/method/update")
    public String update() {
        System.out.println("update method without @MethodExporter");
        return "ok update";
    }
}

这里的 listget 方法会触发 AOP,而 update 方法不会。


四、AOP 切面实现

切面是核心,它会拦截所有带有 @MethodExporter 注解的方法,记录输入参数、返回结果、耗时。

java 复制代码
@Aspect
@Component
@Slf4j
public class MethodExporterAspect {

    @Around("@annotation(com.donglin.interview2.annotations.MethodExporter)")
    public Object methodExporter(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object retValue;

        long startTime = System.currentTimeMillis();
        System.out.println("-----@Around环绕通知AAA-methodExporter");

        // 执行目标方法
        retValue = proceedingJoinPoint.proceed();

        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;

        // 获取方法签名
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();

        // 判断是否有 MethodExporter 注解
        MethodExporter methodExporterAnnotation = method.getAnnotation(MethodExporter.class);

        if (methodExporterAnnotation != null) {
            // 获取输入参数
            StringBuilder jsonInputParam = new StringBuilder();
            Object[] args = proceedingJoinPoint.getArgs();
            DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
            String[] parameterNames = discoverer.getParameterNames(method);

            if (parameterNames != null) {
                for (int i = 0; i < parameterNames.length; i++) {
                    jsonInputParam.append(parameterNames[i])
                            .append("=")
                            .append(args[i].toString())
                            .append(";");
                }
            }

            // 序列化返回结果
            String jsonResult = retValue != null ?
                    new ObjectMapper().writeValueAsString(retValue) : "null";

            log.info("\n方法分析上报中 " +
                            "\n类名方法名: {}.{}()" +
                            "\n执行耗时: {} 毫秒" +
                            "\n输入参数: {}" +
                            "\n返回结果: {}" +
                            "\nover",
                    proceedingJoinPoint.getTarget().getClass().getName(),
                    proceedingJoinPoint.getSignature().getName(),
                    costTime,
                    jsonInputParam,
                    jsonResult
            );

            System.out.println("-----@Around环绕通知BBB-methodExporter");
        }

        return retValue;
    }
}

五、效果展示

调用 list 接口

访问:

复制代码
http://localhost:24618/method/list?page=1&rows=7

日志输出:

调用 update 接口

访问:

复制代码
http://localhost:24618/method/update

输出:

说明 AOP 没有生效。


六、总结与扩展

通过 @MethodExporter 注解 + AOP,我们实现了对方法的统一拦截和日志上报。这样可以大大减少样板代码,同时让日志收集更可控。

未来可以扩展的功能:

  • 将日志结果存入数据库 / Kafka / ELK 进行分析。
  • 配合 @MyRedisCache 等注解,做缓存层封装。
  • 结合 Spring Boot Actuator,进一步做监控指标上报。

这是一种通用的设计模式:用注解声明,用 AOP 实现横切逻辑

相关推荐
---学无止境---8 小时前
Linux内存回收与TLB管理:高效释放与缓存刷新的精密协作
linux
硬核子牙8 小时前
硬盘第一关:MBR VS GPT
linux
LCG元8 小时前
Linux 日志分析全攻略:快速从海量日志中定位问题
linux
_Power_Y8 小时前
Linux&git入门&设计模式(常考点)
linux·git·设计模式
海蓝可知天湛8 小时前
Ubuntu24.10禁用该源...+vmware无法复制黏贴“天坑闭环”——从 DNS 诡异解析到 Ubuntu EOL 引发的 apt 404排除折腾记
linux·服务器·安全·ubuntu·aigc·bug
vvw&8 小时前
如何在 Ubuntu 24.04 上安装和使用 AdGuard
linux·运维·服务器·ubuntu·adguard
遇见火星9 小时前
Linux 网络配置实战:RHEL/CentOS 7+ 永久静态路由配置与优先级调整全攻略
linux·网络·centos·静态路由·centos 7
一 乐9 小时前
医疗管理|医院医疗管理系统|基于springboot+vue医疗管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·医疗管理系统
华仔啊10 小时前
SpringBoot 2.x 和 3.x 的核心区别,这些变化你必须知道
java·spring boot·后端
安审若无10 小时前
linux怎么检查磁盘是否有坏道
linux·运维·服务器