Spring boot AOP 代码-----14

 

 

覆盖「痛点原理 → 5 种通知类型 → execution 切点 → 注解式 AOP」全内容,采用行业通用最佳实践写法,和你的 jobportal 项目无缝衔接,复制即可运行。

一、前置准备:引入依赖

Spring AOP 是独立起步依赖,只需在 pom.xml 新增一行:
xml
 
 
<!-- Spring AOP 面向切面编程 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 

 
✅ 对应 147 集:解决的痛点
 
业务代码里到处重复写「日志、耗时统计、权限校验、异常记录」,这些和业务无关的横切关注点,用 AOP 统一抽出来,一次编写,全局生效,业务代码更干净。

核心术语大白话(对应 148 集)

表格
 
术语通俗解释
Aspect 切面 装增强逻辑的类,比如「日志切面」「权限切面」
Pointcut 切点 规则:哪些方法需要被增强
Advice 通知 时机:什么时候执行增强逻辑
JoinPoint 连接点 被拦截到的具体方法

二、项目结构

plaintext
 
 
com.example.jobportal
├── aspect
│   ├── ControllerLogAspect.java   # 控制器统一日志+耗时切面(execution切点)
│   └── ServiceLogAspect.java      # 演示4种基础通知类型
└── annotation
    └── MethodLog.java             # 自定义注解:注解式AOP用
 

三、切面 1:控制器统一日志 + 耗时统计(最常用)

对应课程:149、150、151 集
 
使用 @Around 环绕通知 + execution() 切点,是项目里最常用的写法,一次配置所有 Controller 接口自动打日志、统计耗时。
java
 
运行
 
 
package com.example.jobportal.aspect;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 控制器层统一日志切面
 * 功能:所有Controller接口自动打印入参、出参、执行耗时
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class ControllerLogAspect {

    private final ObjectMapper objectMapper;

    /**
     * 切点定义:匹配 controller 包下所有类的所有方法
     * 对应150集:execution() 切点表达式精确匹配
     * 语法:execution(返回值 包名.类名.方法名(参数))
     */
    @Pointcut("execution(* com.example.jobportal.controller..*.*(..))")
    public void controllerPointcut() {}

    /**
     * 环绕通知:方法执行前后都能做逻辑
     * 对应149、151集:@Around 最强通知,完全掌控方法执行
     */
    @Around("controllerPointcut()")
    public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 方法执行前:记录开始时间、方法名、入参
        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();

        try {
            log.info("【接口开始】{},入参:{}", methodName, objectMapper.writeValueAsString(args));
        } catch (Exception e) {
            log.info("【接口开始】{},参数解析失败", methodName);
        }

        // 2. 执行原方法(必须调用!否则业务方法不会跑)
        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            // 异常原样抛出,不能吞掉业务异常
            long cost = System.currentTimeMillis() - startTime;
            log.error("【接口异常】{},耗时:{}ms,异常:{}", methodName, cost, e.getMessage());
            throw e;
        }

        // 3. 方法执行后:记录返回值、耗时
        long costTime = System.currentTimeMillis() - startTime;
        try {
            log.info("【接口结束】{},耗时:{}ms,出参:{}", methodName, costTime, objectMapper.writeValueAsString(result));
        } catch (Exception e) {
            log.info("【接口结束】{},耗时:{}ms", methodName, costTime);
        }

        return result;
    }
}
 

 

execution 表达式常用写法(150 集核心)

plaintext
 
 
// 匹配 controller 包下所有类所有方法
execution(* com.example.jobportal.controller..*.*(..))

// 匹配所有 public 方法
execution(public * com.example.jobportal.service.*.*(..))

// 匹配所有以 save 开头的方法
execution(* com.example.jobportal.service.*.save*(..))

// 匹配返回值是 String 的方法
execution(String com.example.jobportal.service.*.*(..))

 

 

四、切面 2:4 种基础通知完整演示

对应课程:152、153、154 集
 
一次性演示 @Before@AfterReturning@AfterThrowing@After 四种通知,清晰看到执行时机。
java
 
运行
 
 
package com.example.jobportal.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * Service层切面:演示4种通知的执行时机
 */
@Slf4j
@Aspect
@Component
public class ServiceLogAspect {

    // 切点:匹配 service 包下所有方法
    @Pointcut("execution(* com.example.jobportal.service..*.*(..))")
    public void servicePointcut() {}

    /**
     * 1. 前置通知:方法执行之前运行
     * 对应152集:适合做参数校验、权限检查、日志记录
     */
    @Before("servicePointcut()")
    public void beforeMethod(JoinPoint joinPoint) {
        log.info("【Before】方法执行前:{}", joinPoint.getSignature().toShortString());
    }

    /**
     * 2. 返回后通知:方法正常返回后运行
     * 对应153集:可以拿到返回值,适合做返回日志、数据脱敏
     */
    @AfterReturning(pointcut = "servicePointcut()", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        log.info("【AfterReturning】方法正常返回:{},返回值:{}",
                joinPoint.getSignature().toShortString(), result);
    }

    /**
     * 3. 异常后通知:方法抛出异常后运行
     * 对应154集:集中记录异常日志,不用每个方法try-catch打日志
     */
    @AfterThrowing(pointcut = "servicePointcut()", throwing = "e")
    public void afterThrowingMethod(JoinPoint joinPoint, Exception e) {
        log.error("【AfterThrowing】方法抛出异常:{},异常信息:{}",
                joinPoint.getSignature().toShortString(), e.getMessage());
    }

    /**
     * 4. 最终通知:无论正常还是异常,方法结束后一定会执行
     * 类似 try-finally 里的 finally
     */
    @After("servicePointcut()")
    public void afterMethod(JoinPoint joinPoint) {
        log.info("【After】方法结束(无论成功失败):{}", joinPoint.getSignature().toShortString());
    }
}
 

 

5 种通知执行顺序

plaintext
 
 
@Around 前置逻辑 → @Before → 执行业务方法 → @AfterReturning/@AfterThrowing → @After → @Around 后置逻辑

 

 

五、注解式 AOP(最灵活、最推荐的写法)

对应课程:155 集
 
自定义一个注解,只有加了这个注解的方法才会被增强,比 execution 更精准、可读性更强,是企业级项目首选方案。

1. 自定义注解 MethodLog.java

java
 
运行
 
 
package com.example.jobportal.annotation;

import java.lang.annotation.*;

/**
 * 自定义方法日志注解
 * 加在方法上,就会自动被切面拦截
 */
@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface MethodLog {
    // 方法描述,自定义业务名称
    String value() default "";
}
 

 

2. 注解式切面

java
 
运行
 
 
package com.example.jobportal.aspect;

import com.example.jobportal.annotation.MethodLog;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

/**
 * 注解式AOP切面
 * 对应155集:更干净、可读性更强,只增强加了@MethodLog的方法
 */
@Slf4j
@Aspect
@Component
public class AnnotationLogAspect {

    /**
     * 切点:所有加了 @MethodLog 注解的方法
     */
    @Around("@annotation(methodLog)")
    public Object aroundAnnotation(ProceedingJoinPoint joinPoint, MethodLog methodLog) throws Throwable {
        // 拿到注解上的描述
        String bizName = methodLog.value();
        String methodName = joinPoint.getSignature().toShortString();
        long start = System.currentTimeMillis();

        log.info("【注解日志-开始】业务:{},方法:{}", bizName, methodName);

        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            long cost = System.currentTimeMillis() - start;
            log.error("【注解日志-异常】业务:{},耗时:{}ms", bizName, cost);
            throw e;
        }

        long cost = System.currentTimeMillis() - start;
        log.info("【注解日志-结束】业务:{},耗时:{}ms", bizName, cost);
        return result;
    }
}

 

 

3. 使用方式

在你想增强的方法上加注解即可,比如:
java
 
运行
 
 
@MethodLog("保存用户留言")
public Contact saveContact(ContactDTO dto) {
    // 业务代码
}

 

 

六、最佳实践总结

  1. 优先用注解式 AOP
    • 精准控制,不会误切入;代码可读性强,看注解就知道有增强逻辑
    • execution 适合全局统一规则(比如所有 Controller 日志)
  2. @Around 是最强大的通知,但不要滥用
    • 只做日志、耗时、权限这类无侵入逻辑
    • 绝对不要吞掉原方法的异常,必须原样抛出,否则业务异常会丢失
  3. 切点粒度要精确
    • 不要写 execution(* *.*(..)) 切所有方法,会严重影响性能
    • 精确到具体包、具体类、具体前缀
  4. 切面职责单一
    • 日志切面只做日志,权限切面只做权限,不要一个切面塞一堆逻辑
  5. 复杂逻辑优先用 AOP,简单重复代码直接写工具类
    • 跨多个层级、全局生效的逻辑用 AOP
    • 局部工具方法不用硬套 AOP

七、课程对应表

表格
 
集数核心内容对应代码
147 AOP 解决的痛点:重复代码、横切关注点 前置说明部分
148 核心概念:Aspect、Advice、Pointcut 术语大白话表格
149 @Around 环绕通知,完全掌控方法 ControllerLogAspect 的 @Around
150 execution () 切点表达式精确匹配 切点表达式语法说明
151 @Around 实战:日志 + 性能统计 ControllerLogAspect 完整实现
152 @Before 前置通知 ServiceLogAspect 的 @Before
153 @AfterReturning 返回后通知 ServiceLogAspect 的 @AfterReturning
154 @AfterThrowing 集中异常日志 ServiceLogAspect 的 @AfterThrowing
155 注解式 AOP,更干净易读 @MethodLog 注解 + AnnotationLogAspect

 

 

 

posted @ 2026-08-01 01:45  漫漫长路</>  阅读(3)  评论(0)    收藏  举报