💡 一则也许对你有用的小广告 🏆

欢迎飞飞程序员   ,你将获得:专属的实战项目(已更新的所有会员标识的项目都能学习) / 1v1 提问 / Java 学习路线 / PHP 学习路线 / 学习打卡 / 社群讨论

  • 正在进行中的项目:《FFBlog知识付费博客项目》 正在持续更新中,基于 Spring Boot 3.x + JDK 21...,点击查看 ;
  • 《从零开发:FFBlog知识付费博客项目(全栈开发)》 演示链接: https://ffblog.ffcxy.com/  ;

截止目前, 飞飞  正在疯狂爆肝实战项目,后续还会上新更多项目,目标是将所学知识开发成项目并且分享给大家,如知识付费系统, Ai系统, CMS系统,在线商城系统,等等 ,欢迎点击围观

下面是一个完整的、可直接运行的 Spring Boot 示例,实现 自定义注解 + AOP 切面记录 API 请求日志。


一、项目结构

src/main/java/com/example/demo/
├── DemoApplication.java              // 启动类
├── annotation/
│   └── ApiLog.java                   // 自定义注解
├── aspect/
│   └── ApiLogAspect.java             // 日志切面
├── util/
│   └── JsonUtil.java                 // JSON 工具类
└── controller/
    └── UserController.java           // 测试接口

二、引入依赖

pom.xml:

<dependencies>
    <!-- Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- AOP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aspectj</artifactId>
    </dependency>

    <!-- Lombok(可选,简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

如果不用 Lombok,手动写 Logger 即可。


三、自定义注解 @ApiLog

package com.example.demo.annotation;

import java.lang.annotation.*;

/**
 * API 操作日志注解
 * 标注在方法上,用于记录接口请求日志
 */
@Target(ElementType.METHOD)          // 只能标在方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时可被反射读取(AOP 必需)
@Documented
public @interface ApiLog {

    /**
     * API 功能描述
     */
    String value() default "";
}

四、JSON 工具类

package com.example.demo.util;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * JSON 工具类
 */
public class JsonUtil {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private JsonUtil() {}

    public static String toJsonString(Object obj) {
        if (obj == null) {
            return null;
        }
        try {
            return OBJECT_MAPPER.writeValueAsString(obj);
        } catch (Exception e) {
            return obj.toString();
        }
    }
}

五、日志切面 ApiLogAspect

package com.example.demo.aspect;

import com.example.demo.annotation.ApiLog;
import com.example.demo.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;

/**
 * API 日志切面
 * 拦截所有标注 @ApiLog 的方法,自动打印请求日志
 */
@Aspect
@Component
@Slf4j
public class ApiLogAspect {

    /**
     * 切点:所有标注 @ApiLog 的方法
     */
    @Pointcut("@annotation(com.example.demo.annotation.ApiLog)")
    public void apiLogPointcut() {}

    /**
     * 环绕通知:方法执行前后记录日志
     */
    @Around("apiLogPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

        // 1. 开始时间
        long startTime = System.currentTimeMillis();

        // 2. 获取类名、方法名
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();

        // 3. 获取入参并转 JSON
        Object[] args = joinPoint.getArgs();
        String argsJson = Arrays.stream(args)
                .map(JsonUtil::toJsonString)
                .collect(Collectors.joining(", "));

        // 4. 获取注解上的描述
        String description = getDescription(joinPoint);

        // 5. 打印请求开始日志
        log.info("====== 请求开始: [{}], 类: {}, 方法: {}, 入参: {} ======",
                description, className, methodName, argsJson);

        // 6. 执行真正的业务方法
        Object result = joinPoint.proceed();

        // 7. 计算耗时并打印结束日志
        long cost = System.currentTimeMillis() - startTime;
        log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} ======",
                description, cost, JsonUtil.toJsonString(result));

        return result;
    }

    /**
     * 从注解中读取 description
     */
    private String getDescription(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        ApiLog apiLog = method.getAnnotation(ApiLog.class);
        return apiLog != null ? apiLog.value() : "";
    }
}

六、测试接口 UserController

package com.example.demo.controller;

import com.example.demo.annotation.ApiLog;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 加了 @ApiLog 注解,会被切面拦截并记录日志
     */
    @ApiLog("用户注册")
    @PostMapping("/register")
    public Map<String, Object> register(@RequestBody Map<String, Object> user) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "注册成功");
        result.put("data", user);
        return result;
    }

    /**
     * 没有加 @ApiLog 注解,不会被切面拦截
     */
    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring Boot!";
    }
}

七、启动类

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

八、测试验证

1. 请求带 @ApiLog 的接口

请求:

POST http://localhost:8080/user/register
Content-Type: application/json

{
  "name": "张三",
  "age": 18
}

控制台日志:

====== 请求开始: [用户注册], 类: UserController, 方法: register, 入参: {"name":"张三","age":18} ======
====== 请求结束: [用户注册], 耗时: 5ms, 出参: {"code":200,"msg":"注册成功","data":{"name":"张三","age":18}} ======

2. 请求不带 @ApiLog 的接口

请求:

GET http://localhost:8080/user/hello

控制台日志: 无(因为没有被切面拦截)。


九、核心流程总结

用户请求
   ↓
Controller 方法(标注 @ApiLog)
   ↓
AOP 切面拦截(@Around)
   ↓
① 记录开始时间、类名、方法名、入参
② joinPoint.proceed() 执行业务方法
③ 计算耗时、记录出参
   ↓
返回结果给前端

十、关键点回顾

关键点 说明
@Target(METHOD) 注解只能标在方法上
@Retention(RUNTIME) 运行时可被反射读取,AOP 必需
@Pointcut("@annotation(...)") 切点匹配所有标注该注解的方法
@Around 环绕通知,能控制方法执行前后
joinPoint.proceed() 执行真正的业务方法
MethodSignature 用于获取方法上的注解
JsonUtil.toJsonString 把入参/出参转成 JSON 打印

十一、扩展建议

扩展方向 说明
加 traceId 用 MDC.put("traceId", UUID) 给每个请求生成唯一标识
异常日志 在 @Around 中 try-catch,捕获异常并记录 log.error
异步日志 用 @Async 或消息队列,避免日志影响接口性能
脱敏处理 对密码、手机号等敏感字段做脱敏后再打印
持久化 把日志存到数据库或 ELK,便于查询和分析

一句话总结:只要在方法上加 @ApiLog("描述"),AOP 切面就会自动帮你打印请求的入参、出参、耗时和功能描述,无需在业务代码里手写一行日志。

上一篇 什么是 AOP 面向切面编程?
下一篇 SpringBoot中MDC 是什么?

全部评论(0)

头像
😃 😁 😅 😂 😍 😜 😝 🤑 🥵 🥰 😙 😎 😵 😭 😱 😖 🥳 👽 🙈 🤡 😤 💣 💯 💢 ❤️ 👍 👏 👋 👌 🤏 🙏
还没有任何评论哟~
微信扫一扫
微信二维码
微信客服
QQ 咨询
QQ二维码
工作日 9:00 - 18:00 在线
QQ客服
在线工单
提交问题工单,我们会尽快回复处理。
提交工单
在线工单