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

相关推荐
ha204289419419 分钟前
Linux操作系统学习记录之----自定义协议(网络计算器)
linux·网络·学习
想唱rap24 分钟前
MYSQL在ubuntu下的安装
linux·数据库·mysql·ubuntu
糖~醋排骨27 分钟前
DHCP服务的搭建
linux·服务器·网络
dust_and_stars1 小时前
ubuntu24使用apt安装VS-code-server code-server
linux·服务器·windows
码农小韩1 小时前
基于Linux的C++学习——循环
linux·c语言·开发语言·c++·算法
ling-451 小时前
Linux-day09 11
linux·运维·服务器
202321336054 刘1 小时前
Linux常用命令分类整理
linux·运维·数据库
南工孙冬梅1 小时前
【久久派】 新世界系统安装
linux
zbguolei1 小时前
Debian提示:“用户名” 不是 sudoers 文件
linux·服务器·debian
梦星辰.1 小时前
超大 JSONL 数据集交互式查看器 Linux便捷工具
linux·windows·microsoft