Spring Boot 实现AOP日志切面全流程教程

Spring Boot 实现AOP日志切面全流程教程

AOP(面向切面编程)是Spring框架的核心特性之一,常用于日志记录、权限校验、事务管理等场景。本文将手把手教你如何在Spring Boot项目中实现AOP日志切面功能,包括依赖引入、切点定义、切面实现、注解自定义等内容。

下载地址

百度网盘: pan.baidu.com/s/1GFKlonKt...

蓝奏云: wwrh.lanzoul.com/iaHbU30ksxb...

效果

切入com.anfioo下的所有controller层

切入com.anfioo下的所有service层

切入自定义注解@LogRecord

全都开启

可以使用aop切片更好的打印这个方法信息,而不污染原有的方法

一、引入依赖

首先,确保你的Spring Boot项目已经引入了AOP相关依赖。Spring Boot Starter通常已经包含了AOP依赖,但你可以在pom.xml中显式添加:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

二、配置AOP属性(可选)

为了灵活控制切面的开启与关闭,我们可以通过配置文件添加自定义属性。例如:

yaml 复制代码
# application.yaml
aop:
  debug:
    controller: true   # 控制Controller切面是否开启
    service: false     # 控制Service切面是否开启
    log-record: true   # 控制LogRecord注解切面是否开启

并通过@ConfigurationProperties将其绑定到Java类:

java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "aop.debug")
public class AopDebugProperties {
    private Boolean controller = true;
    private Boolean service = false;
    private Boolean logRecord = true;
}

三、定义切点(Pointcut)

切点用于指定哪些类或方法会被AOP拦截。常见的切点表达式有:

  • execution(public * com.example..controller..*(..)):拦截所有controller包下的公共方法
  • @annotation(com.example.LogRecord):拦截所有被自定义注解标记的方法

四、实现切面(Aspect)

以Controller层日志为例,实现一个切面类:

java 复制代码
package com.anfioo.common.log;

import com.anfioo.common.bean.AopDebugProperties;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * 全局 Controller 层日志记录切面
 */
@Aspect
@Component
@Slf4j
public class ControllerLogAspect {
    @Autowired
    private AopDebugProperties aopDebugProperties;

    /**
     * 定义切点,匹配 com.anfioo 包下所有子包中的 controller 类的公共方法
     */
    @Pointcut("execution(public * com.anfioo..controller..*(..))")
    public void controllerMethods() {
    }

    /**
     * 环绕通知,用于记录 controller 层方法的请求和响应信息
     *
     * @param joinPoint 切入点对象,包含被拦截方法的信息
     * @return 被拦截方法的执行结果
     * @throws Throwable 如果执行过程中出现异常
     */
    @Around("controllerMethods()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        if (Boolean.FALSE.equals(aopDebugProperties.getController())) {
            // 不开启,直接执行原方法
            return joinPoint.proceed();
        }

        // 记录开始时间
        long start = System.currentTimeMillis();
        // 获取目标类的 Class 对象
        Class<?> targetClass = joinPoint.getTarget().getClass();
        // 获取目标类的 Logger 对象
        Logger logger = LoggerFactory.getLogger(targetClass);

        // 获取当前请求的 HttpServletRequest 对象
        ServletRequestAttributes attributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 提取请求的 URL、方法、IP 地址、处理方法和参数
        String url = request.getRequestURL().toString();
        String method = request.getMethod();
        String ip = request.getRemoteAddr();
        String classMethod = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();


        String red = "\u001B[31m"; // ANSI 红色
        String line = IntStream.range(0, 200)  // 200 可以改成任意长度
                .mapToObj(i -> "#")
                .collect(Collectors.joining());
        System.out.println(red + line);


        // 记录请求信息
        logger.info("\n====== 🌐 请求信息 ======\n" +
                        "📌 URL        : {}\n" +
                        "🔁 Method     : {}\n" +
                        "👤 IP         : {}\n" +
                        "🎯 Handler    : {}\n" +
                        "🧾 Parameters : {}\n" +
                        "==========================",
                url, method, ip, classMethod, Arrays.toString(args));

        // 尝试执行目标方法,并记录执行结果或异常信息
        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Exception e) {
            // 记录异常信息,并重新抛出异常
            logger.error("\n====== 🌐 ❌ 异常信息 ======\n" +
                            "🎯 Handler    : {}\n" +
                            "🛑 Error      : {}\n" +
                            "==========================",
                    classMethod, e.getMessage(), e);
            throw e;
        }

        // 记录结束时间,并计算耗时
        long end = System.currentTimeMillis();
        // 记录响应信息
        logger.info("\n====== 🌐 ✅ 响应信息 ======\n" +
                        "🎯 Handler    : {}\n" +
                        "📤 Response   : {}\n" +
                        "⏱️ 耗时        : {} ms\n" +
                        "==========================",
                classMethod, result, end - start);

        return result;
    }
}

说明

  • @Aspect:声明该类为切面
  • @Pointcut:定义切点
  • @Around:环绕通知,可在方法执行前后插入逻辑
  • ProceedingJoinPoint:用于获取方法信息、参数、执行目标方法等

五、自定义注解与切面

有时我们希望只对特定方法记录日志,可以自定义注解:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRecord {
    String value() default "";
}

并实现对应的切面:

java 复制代码
package com.anfioo.common.log;

import com.anfioo.common.bean.AopDebugProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 记录 @LogRecord 注解标记的方法调用日志
 */
@Aspect
@Component
@Slf4j
public class LogRecordAspect {

    @Autowired
    private AopDebugProperties aopDebugProperties;

    /**
     * 切点:拦截所有带有 @LogRecord 注解的方法
     */
    @Pointcut("@annotation(com.anfioo.common.log.LogRecord)")
    public void logRecordMethods() {
    }

    /**
     * 环绕通知:记录方法执行详情
     */
    @Around("logRecordMethods() && @annotation(logRecord)")
    public Object around(ProceedingJoinPoint joinPoint, LogRecord logRecord) throws Throwable {
        if (Boolean.FALSE.equals(aopDebugProperties.getLogRecord())) {
            return joinPoint.proceed(); // 不开启则跳过
        }

        long start = System.currentTimeMillis();
        Class<?> targetClass = joinPoint.getTarget().getClass();
        Logger logger = LoggerFactory.getLogger(targetClass);

        String description = logRecord.value(); // 注解中的描述信息
        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();

        logger.info("\n====== 📝 LogRecord 方法调用 ======\n" +
                        "📌 描述        : {}\n" +
                        "🔧 方法        : {}\n" +
                        "🧾 参数        : {}\n" +
                        "=====================================",
                description, methodName, Arrays.toString(args));

        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Exception e) {
            logger.error("\n====== 📝❌ LogRecord 异常 ======\n" +
                            "🔧 方法        : {}\n" +
                            "🛑 异常信息     : {}\n" +
                            "=====================================",
                    methodName, e.getMessage(), e);
            throw e;
        }

        long end = System.currentTimeMillis();
        logger.info("\n====== 📝✅ LogRecord 返回结果 ======\n" +
                        "🔧 方法        : {}\n" +
                        "📤 返回值      : {}\n" +
                        "⏱️ 耗时        : {} ms\n" +
                        "=====================================",
                methodName, result, end - start);

        return result;
    }
}

六、Service层切面实现

同理,可以为Service层实现切面:

java 复制代码
package com.anfioo.common.log;

import com.anfioo.common.bean.AopDebugProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * Service 层方法日志切面(记录业务逻辑调用)
 */
@Aspect
@Component
@Slf4j
public class ServiceLogAspect {

    @Autowired
    private AopDebugProperties aopDebugProperties;


    /**
     * 切入 com.anfioo 包下所有 service 的方法
     */
    @Pointcut("execution(* com.anfioo..service..*(..))")
    public void serviceMethods() {
    }

    /**
     * 环绕通知记录 service 层方法的调用情况
     */
    @Around("serviceMethods()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        if (Boolean.FALSE.equals(aopDebugProperties.getService())) {
            // 不开启,直接执行原方法
            return joinPoint.proceed();
        }


        long start = System.currentTimeMillis();
        Class<?> targetClass = joinPoint.getTarget().getClass();
        Logger logger = LoggerFactory.getLogger(targetClass);

        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();

        logger.info("\n====== ⚙️ Service 方法调用 ======\n" +
                    "🔧 Method     : {}\n" +
                    "🧾 Parameters : {}\n" +
                    "===============================",
                methodName, Arrays.toString(args));

        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Exception e) {
            logger.error("\n====== ⚙️ ❌ Service 异常 ======\n" +
                         "🔧 Method     : {}\n" +
                         "🛑 Error      : {}\n" +
                         "=============================",
                    methodName, e.getMessage(), e);
            throw e;
        }

        long end = System.currentTimeMillis();
        logger.info("\n====== ⚙️ ✅ Service 返回结果 ======\n" +
                    "🔧 Method     : {}\n" +
                    "📤 Result     : {}\n" +
                    "⏱️ 耗时        : {} ms\n" +
                    "===============================",
                methodName, result, end - start);

        return result;
    }
}

七、完整流程总结

  1. 引入依赖 :确保spring-boot-starter-aop已添加
  2. 配置属性:通过配置文件灵活控制切面开关
  3. 定义切点:用表达式或注解指定拦截范围
  4. 实现切面 :用@Aspect@Around等实现日志逻辑
  5. 自定义注解:实现更细粒度的日志控制
  6. 应用注解:在需要记录日志的方法上加上自定义注解

八、常见问题

  • 切面不生效? 检查@Component@Aspect是否加上,切点表达式是否正确,AOP依赖是否引入。
  • 日志打印不全? 检查日志级别、切面逻辑是否被条件限制跳过。

AOP 的主要好处

关注点分离(Separation of Concerns)

AOP 允许你将与业务逻辑无关的"横切关注点"(如日志记录、安全控制、异常处理、事务管理)从核心业务代码中分离出去,使业务逻辑更专注、更清晰。


避免重复代码,提高可维护性

常见的重复操作如打印日志、性能统计、权限校验,如果分散在各个方法中,会导致维护困难。AOP 将这些重复逻辑集中在一个地方,修改一次即可生效全局。

不侵入业务代码

通过 AOP,你可以在不修改原方法的前提下,增强其功能(如记录参数、返回值、异常信息等),实现"开闭原则":对扩展开放,对修改封闭。


增强系统的可观测性与调试能力

配合 AOP 自动记录方法调用轨迹、执行耗时、输入输出信息,极大提升问题排查和性能分析的效率。

灵活可配置

结合注解、自定义属性、开关控制(如 AopDebugProperties),你可以根据环境或条件动态启用/禁用某些切面逻辑,适应多种部署或调试场景。


提高开发效率

开发人员无需手动添加日志、异常处理等模板式代码,只需专注于业务逻辑,其余交由统一切面处理,显著提高开发效率和代码一致性。


总结一句话

AOP 帮你在"不碰业务代码"的前提下,实现系统级增强,让代码更干净、功能更强大、维护更轻松。

通过AOP切面,你可以优雅地实现日志记录、权限校验等横切关注点,极大提升代码的可维护性和可扩展性。希望本文能帮助你快速上手Spring Boot的AOP切面开发!

相关推荐
小刘爱搬砖11 小时前
SpringBoot3 + GraalVM安装和初次打包
spring boot·graalvm
_UMR_12 小时前
springboot集成Jasypt实现配置文件启动时自动解密-ENC
java·spring boot·后端
蓝色王者13 小时前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
hashiqimiya15 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
因我你好久不见15 小时前
Windows部署springboot jar支持开机自启动
windows·spring boot·jar
无关868816 小时前
SpringBootApplication注解大解密
spring boot
追梦者12318 小时前
springboot整合minio
java·spring boot·后端
帅气的你18 小时前
Spring Boot 集成 AOP 实现日志记录与接口权限校验
java·spring boot
计算机毕设VX:Fegn089519 小时前
计算机毕业设计|基于springboot + vue在线音乐播放系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
计算机毕设VX:Fegn089519 小时前
计算机毕业设计|基于springboot + vue博物馆展览与服务一体化系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计