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

相关推荐
paopaokaka_luck2 小时前
智能推荐社交分享小程序(websocket即时通讯、协同过滤算法、时间衰减因子模型、热度得分算法)
数据库·vue.js·spring boot·后端·websocket·小程序
柒七爱吃麻辣烫3 小时前
八股文系列-----SpringBoot自动配置的流程
java·spring boot·rpc
头发那是一根不剩了4 小时前
Spring Boot 多数据源切换:AbstractRoutingDataSource
数据库·spring boot·后端
草履虫建模5 小时前
Redis:高性能内存数据库与缓存利器
java·数据库·spring boot·redis·分布式·mysql·缓存
苹果醋35 小时前
Vue3组合式API应用:状态共享与逻辑复用最佳实践
java·运维·spring boot·mysql·nginx
chanalbert6 小时前
从单体到微服务:Spring Cloud 开篇与微服务设计
spring boot·spring·spring cloud
psjasf13146 小时前
使用Ideal创建一个spring boot的helloWorld项目
java·spring boot·后端
风象南8 小时前
SpringBoot 与 HTMX:现代 Web 开发的高效组合
java·spring boot·后端
小七mod18 小时前
【MyBatis】MyBatis与Spring和Spring Boot整合原理
spring boot·spring·mybatis