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 实现横切逻辑

相关推荐
CC.GG2 分钟前
【Linux】进程控制(二)----进程程序替换、编写自主Shell命令行解释器(简易版)
linux·服务器·数据库
H Journey32 分钟前
Linux 下添加用户相关
linux·运维·服务器·添加用户
Trouvaille ~1 小时前
【Linux】线程同步与互斥(三):生产者消费者模型实战
linux·运维·c++·信号量·阻塞队列·生产者消费者模型·环形队列
遇见火星1 小时前
Linux Screen 命令入门指南
linux·运维·服务器
猿小羽1 小时前
深入理解 Microservice Control Proxy(MCP) 的 AI 实战指南
微服务·ai·推荐系统·service mesh·microservice·mcp·ai 实战
Doro再努力2 小时前
【Linux操作系统06】深入理解权限掩码与粘滞位
linux·运维·服务器
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][dma]stm32-dma
linux·笔记·学习
mzhan0172 小时前
[Linux] vdso 32bit vs 64bit
linux·运维·服务器
旖旎夜光2 小时前
Linux(13)(上)
linux·网络
忧郁的橙子.2 小时前
26期_01_Pyhton linux基本命令
linux·运维·服务器