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切面开发!

相关推荐
JH30733 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
qq_12498707536 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_6 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_818732067 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
汤姆yu10 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶10 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
biyezuopinvip11 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide12 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf12 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
zhangyi_viva12 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端