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***71013 小时前
Spring Boot(快速上手)
java·spring boot·后端
n***84073 小时前
十七:Spring Boot依赖 (2)-- spring-boot-starter-web 依赖详解
前端·spring boot·后端
better_liang5 小时前
每日Java面试场景题知识点之-分布式事务处理
java·微服务·面试·springcloud·分布式事务
爱学习的小可爱卢6 小时前
JavaEE进阶——SpringMVC响应处理详解
spring boot·postman·javaee
7***68437 小时前
Spring Boot 从 2.7.x 升级到 3.3注意事项
数据库·hive·spring boot
L***d6707 小时前
Spring Boot 各种事务操作实战(自动回滚、手动回滚、部分回滚)
java·数据库·spring boot
凌波粒7 小时前
Springboot基础教程(3)--自动装配原理/静态资源处理/欢迎页
java·spring boot·后端
java_logo7 小时前
MySQL Server Docker 容器化部署指南
linux·运维·数据库·docker·容器
凌波粒7 小时前
SpringBoot基础教程(2)--yaml/配置文件注入/数据校验/多环境配置
java·spring boot·后端·spring