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

相关推荐
Q_Q19632884754 小时前
python+vue的在线租房 房屋租赁系统
开发语言·vue.js·spring boot·python·django·flask·node.js
养海绵宝宝的小蜗4 小时前
Linux 例行性工作任务(定时任务)知识点总结
linux·运维·服务器
摇滚侠4 小时前
Spring Boot 3零基础教程,WEB 开发 内容协商 接口返回 YAML 格式的数据 笔记35
spring boot·笔记·后端
乌萨奇也要立志学C++4 小时前
【Linux】基础IO(二)深入理解“一切皆文件” 与缓冲区机制:从原理到简易 libc 实现
linux·运维·服务器
Ronin3054 小时前
【Linux网络】封装Socket
linux·网络·socket·网络通信
一个处女座的暖男程序猿4 小时前
若依微服务 nacos的配置文件
微服务·云原生·架构
不会写DN4 小时前
用户头像文件存储功能是如何实现的?
java·linux·后端·golang·node.js·github
---学无止境---5 小时前
Linux中slab缓存初始化kmem_cache_init函数和定时回收函数的实现
linux
java水泥工5 小时前
旅游管理系统|基于SpringBoot和Vue的旅游管理系统(源码+数据库+文档)
spring boot·vue·计算机毕业设计·java毕业设计·旅游管理系统