🎓 总站 🏠 本课目录 01 概述 02 软件过程 03 需求工程 04 系统建模 05 概要设计 06 报告与高可信 07 交互与状态图 08 详细设计 09 AOP
软件系统综合课程设计 · 第9章

面向切面编程 AOP

从 AOP 概念出发,系统讲解切面、连接点、通知、切点、织入等核心概念,以及 Spring AOP 的实现与应用。

📚 学习进度
0%

🎯学习目标

  • 理解 AOP 的基本概念,掌握 AOP 与 OOP 的关系和互补性;
  • 掌握 AOP 的五大核心概念:切面、连接点、通知、切点、织入;
  • 了解 AOP 的三种实现方式:编译时织入、类加载时织入、运行时织入;
  • 掌握 Spring AOP 的注解配置和 XML 配置方式;
  • 能够运用 AOP 解决日志记录、事务管理、权限控制等实际问题。

1AOP 概述

💡 通俗理解:什么是 AOP?

想象你在经营一家餐厅:

  • OOP 思维:每个厨师负责做菜(核心业务),但每个厨师还要自己洗菜、切菜、打扫卫生(横切关注点)
  • AOP 思维:专门雇一个"后勤团队",统一负责洗菜、切菜、打扫,厨师只管做菜

AOP(面向切面编程)就是把那些"散落在各处的公共功能"(如日志、事务、安全)抽取出来,统一管理,让核心业务代码更纯粹。

什么是 AOP?

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在将横切关注点(Cross-Cutting Concerns)从业务逻辑中分离出来,实现关注点的模块化。

传统 OOP 方式:横切关注点分散在各模块中 业务模块 A 业务模块 B 业务模块 C 日志 日志 日志 事务 事务 事务 安全 安全 安全 ↓ AOP 将横切关注点抽取为独立模块 切面模块(统一管理)
图1 · AOP 将分散的横切关注点抽取为独立的切面模块

为什么需要 AOP?

问题说明AOP 解决方案
代码重复日志、事务等代码在每个方法中重复出现抽取为切面,一处定义,多处使用
职责不清业务代码与非业务代码混杂,难以维护业务逻辑与横切关注点分离
耦合度高修改日志格式需要改动所有业务类修改切面即可,不影响业务代码
扩展困难新增功能(如性能监控)需要侵入式修改通过新增切面实现,对原有代码无侵入

AOP 与 OOP 的关系

🧱

OOP:纵向抽象

通过类、继承、多态实现"自上而下"的功能分解。擅长处理业务逻辑的层次结构,但对"横跨多个模块"的公共功能力不从心。

✂️

AOP:横向抽象

通过切面将横切关注点模块化,实现"横向上"的功能整合。是 OOP 的有力补充,二者结合才能实现更彻底的模块化。

⭐ 核心关系AOP 不是 OOP 的替代品,而是 OOP 的补充。OOP 解决业务逻辑的模块化,AOP 解决横切关注点的模块化。二者结合 = 完整的模块化方案。

2AOP 核心概念

切面 Aspect 连接点 JoinPoint 切点 Pointcut 通知 Advice前/后/环绕/异常 织入 Weaving 切面 = 切点 + 通知;织入 = 将切面应用到目标对象的过程
图2 · AOP 五大核心概念及其关系
📐

切面 Aspect

关注点的模块化
切面是横切关注点的模块化,是"切入点 + 通知"的组合。一个切面可以包含多个通知和切入点。在 Spring 中,用 @Aspect 注解的类就是一个切面。
📍

连接点 Join Point

程序执行的某个点
程序执行过程中能够插入切面的点,如方法调用、异常处理、字段修改等。Spring AOP 中连接点仅支持方法执行(方法级别的连接点)。
🎯

切点 Pointcut

匹配连接点的规则
匹配连接点的断言/表达式,用于定义"在哪些地方"应用通知。通过切入点表达式(如 execution(* com.example.*.*(..)))来匹配连接点。
📢

通知 Advice

切面的具体动作
在切点匹配的连接点上执行的动作(代码)。分为:前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)、环绕通知(Around)。
💡 织入 Weaving

织入是将切面应用到目标对象并创建新的代理对象的过程。根据织入时机不同,分为:

  • 编译时织入:在编译 Java 源码时织入(如 AspectJ 编译器)
  • 类加载时织入:在 JVM 加载类时织入(如 AspectJ LTW)
  • 运行时织入:在运行期通过动态代理织入(如 Spring AOP)

五种通知类型详解

通知类型执行时机注解典型用途
前置通知方法执行前@Before参数校验、权限检查、日志记录
后置通知方法执行后(无论是否异常)@After资源清理、审计记录
返回通知方法正常返回后@AfterReturning处理返回值、成功日志
异常通知方法抛出异常后@AfterThrowing异常处理、错误日志、告警
环绕通知方法执行前后(包裹)@Around性能监控、事务管理、缓存

3AOP 实现方式

根据织入时机的不同,AOP 有三种主要实现方式:

AOP 织入时机对比 编译时织入 编译 Java 源码时 生成的 .class 文件 已包含切面代码 代表:AspectJ CTW 类加载时织入 JVM 加载类时 通过自定义类加载器 在字节码层面织入 代表:AspectJ LTW 运行时织入 程序运行期间 通过动态代理技术 生成代理对象 代表:Spring AOP
图3 · AOP 三种织入方式对比
织入方式优点缺点适用场景
编译时织入性能最好,无运行时开销需要特殊编译器,构建复杂对性能要求极高的场景
类加载时织入对代码无侵入,部署灵活需要配置 Agent,调试困难无法修改源码的第三方库
运行时织入使用简单,Spring 原生支持有一定性能开销(代理)大多数企业应用场景
⭐ Spring AOP vs AspectJ
  • Spring AOP:基于代理,运行时织入,仅支持方法级连接点,使用简单,适合大多数场景
  • AspectJ:基于字节码修改,编译时/类加载时织入,支持所有连接点(方法、字段、构造器),功能强大但复杂

选择建议:Spring 项目优先用 Spring AOP,需要更细粒度控制时再考虑 AspectJ。

4Spring AOP ⭐

Spring AOP 是 Spring 框架的核心模块之一,通过动态代理技术实现 AOP 功能。

Spring AOP 的代理机制

🔗

JDK 动态代理

基于接口的代理。目标类实现了接口时,Spring 默认使用 JDK 动态代理。通过 java.lang.reflect.Proxy 创建代理对象。

🧬

CGLIB 代理

基于继承的代理。目标类没有实现接口时,Spring 使用 CGLIB 生成目标类的子类作为代理。通过字节码技术创建代理类。

注解配置方式(推荐)⭐

// 1. 定义切面
@Aspect
@Component
public class LoggingAspect {

    // 定义切点:匹配 service 包下所有类的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}

    // 前置通知:方法执行前
    @Before("serviceLayer()")
    public void beforeAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("调用方法: " + methodName + ",参数: " + Arrays.toString(args));
    }

    // 后置通知:方法执行后(无论是否异常)
    @After("serviceLayer()")
    public void afterAdvice(JoinPoint joinPoint) {
        System.out.println("方法执行完毕: " + joinPoint.getSignature().getName());
    }

    // 返回通知:方法正常返回后
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        System.out.println("方法返回值: " + result);
    }

    // 异常通知:方法抛出异常后
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
        System.out.println("方法异常: " + ex.getMessage());
    }

    // 环绕通知:包裹方法执行
    @Around("serviceLayer()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();  // 执行目标方法
        long time = System.currentTimeMillis() - start;
        System.out.println("方法耗时: " + time + "ms");
        return result;
    }
}

切入点表达式

表达式说明示例
execution()匹配方法执行execution(* com.example.service.*.*(..))
within()匹配指定类型内的方法within(com.example.service.*)
@annotation()匹配带指定注解的方法@annotation(com.example.Loggable)
args()匹配指定参数类型的方法args(java.lang.String,..)
bean()匹配指定名称的 Beanbean(userService)
💡 execution 表达式格式

execution(修饰符? 返回类型 类名?方法名(参数) 异常?)

  • *:匹配任意返回类型
  • com.example.service.*:匹配 service 包下所有类
  • .*(..):匹配任意方法名、任意参数
  • com.example..*:匹配 com.example 及其子包

XML 配置方式

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 定义切面 Bean -->
    <bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>

    <!-- AOP 配置 -->
    <aop:config>
        <!-- 定义切点 -->
        <aop:pointcut id="servicePointcut"
            expression="execution(* com.example.service.*.*(..))"/>

        <!-- 定义切面 -->
        <aop:aspect ref="loggingAspect">
            <aop:before method="beforeAdvice" pointcut-ref="servicePointcut"/>
            <aop:after method="afterAdvice" pointcut-ref="servicePointcut"/>
            <aop:around method="aroundAdvice" pointcut-ref="servicePointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

启用 AOP 支持

// 方式1:注解启用
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

// 方式2:XML 启用
<aop:aspectj-autoproxy/>

5AOP 应用场景

📝

日志记录

最经典的应用场景
统一记录方法调用日志,包括方法名、参数、返回值、执行时间。无需在每个业务方法中手动添加日志代码。
🔄

事务管理

Spring 声明式事务
通过 @Transactional 注解实现声明式事务。Spring AOP 在方法执行前开启事务,正常返回时提交,异常时回滚。
🔒

权限控制

方法级安全检查
在方法执行前检查用户权限,无权限则抛出异常。常用于 RBAC(基于角色的访问控制)场景。
⏱️

性能监控

方法执行时间统计
记录每个方法的执行时间,识别性能瓶颈。通过环绕通知实现,在方法前后记录时间戳并计算差值。

完整示例:日志切面

@Aspect
@Component
@Slf4j
public class LogAspect {

    @Pointcut("execution(* com.example.controller.*.*(..))")
    public void controllerLayer() {}

    @Around("controllerLayer()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        String className = pjp.getTarget().getClass().getSimpleName();
        String methodName = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();

        log.info("[{}] 方法开始执行,参数: {}", className + "." + methodName, args);

        long start = System.currentTimeMillis();
        try {
            Object result = pjp.proceed();
            long time = System.currentTimeMillis() - start;
            log.info("[{}] 方法执行成功,耗时: {}ms,返回: {}",
                     className + "." + methodName, time, result);
            return result;
        } catch (Exception e) {
            long time = System.currentTimeMillis() - start;
            log.error("[{}] 方法执行异常,耗时: {}ms,异常: {}",
                      className + "." + methodName, time, e.getMessage());
            throw e;
        }
    }
}

完整示例:权限校验切面

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
    String value();
}

// 权限切面
@Aspect
@Component
public class AuthAspect {

    @Around("@annotation(requireRole)")
    public Object checkAuth(ProceedingJoinPoint pjp, RequireRole requireRole) throws Throwable {
        // 获取当前用户
        User currentUser = SecurityContext.getCurrentUser();
        if (currentUser == null) {
            throw new UnauthorizedException("用户未登录");
        }

        // 检查角色
        String requiredRole = requireRole.value();
        if (!currentUser.hasRole(requiredRole)) {
            throw new ForbiddenException("权限不足,需要角色: " + requiredRole);
        }

        return pjp.proceed();
    }
}

// 使用方式
@Service
public class UserService {
    @RequireRole("ADMIN")
    public void deleteUser(Long userId) {
        // 只有管理员才能执行
    }
}

6AOP 最佳实践

⚠️ 常见误区
  • 过度使用 AOP:不是所有横切功能都需要 AOP,简单的工具方法即可解决的场景不需要引入 AOP
  • 切面逻辑过重:切面应该保持轻量,复杂逻辑应该委托给专门的服务类
  • 切入点表达式过于宽泛:避免使用 execution(* *.*(..)) 这种匹配所有方法的表达式
  • 忽略代理限制:Spring AOP 基于代理,同类方法调用不会触发 AOP(自调用问题)

最佳实践清单

实践说明
保持切面单一职责一个切面只关注一个横切关注点(如日志切面只做日志)
使用命名切点将切入点表达式定义为 @Pointcut 方法,便于复用和维护
避免循环依赖切面之间不要相互依赖,会导致 Spring 容器启动失败
注意执行顺序多个切面作用于同一方法时,使用 @Order 指定优先级
处理自调用问题同类方法调用不走代理,可通过注入自身或使用 AopContext 解决
测试切面逻辑切面也需要单元测试,确保通知逻辑正确执行

切面执行顺序

@Aspect
@Component
@Order(1)  // 数字越小,优先级越高
public class SecurityAspect {
    // 安全校验(先执行)
}

@Aspect
@Component
@Order(2)
public class LoggingAspect {
    // 日志记录(后执行)
}
💡 自调用问题解决方案
// 方案1:注入自身
@Service
public class UserService {
    @Autowired
    private UserService self;  // 注入代理对象

    public void methodA() {
        self.methodB();  // 通过代理调用,触发 AOP
    }

    @Transactional
    public void methodB() { }
}

// 方案2:启用 exposeProxy
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig { }

// 使用方式
public void methodA() {
    ((UserService) AopContext.currentProxy()).methodB();
}

重点例题

例题1:理解 AOP 核心概念 题目:在 Spring AOP 中,以下代码定义了什么?
@Pointcut("execution(* com.example.service.UserServiceImpl.*(..))")
public void userServiceMethods() {}
分析:
  • 这是一个切入点定义(Pointcut),不是通知
  • 表达式 execution(* com.example.service.UserServiceImpl.*(..)) 匹配 UserServiceImpl 类的所有方法
  • *:任意返回类型
  • .*(..):任意方法名、任意参数
  • userServiceMethods() 是切入点的方法名,可在通知中引用
关键点:@Pointcut 定义"在哪里"执行,@Before/@After 等定义"做什么"。
例题2:选择合适的通知类型 题目:需要记录每个 Service 方法的执行时间,应该使用哪种通知类型? 分析:
  • 前置通知 @Before:只能记录开始时间,无法计算耗时
  • 后置通知 @After:只能记录结束时间,无法计算耗时
  • 返回通知 @AfterReturning:只能在方法正常返回后执行,异常时无法记录
  • 环绕通知 @Around:✅ 可以在方法前后分别记录时间,计算差值,且能处理异常情况
结论:性能监控场景应使用 @Around 通知。
例题3:AOP 与 OOP 结合设计 题目:设计一个电商系统的订单服务,需要:创建订单、查询订单、取消订单。要求添加日志记录和事务管理。 分析:
  • OOP 部分:OrderService 类实现业务逻辑(创建、查询、取消)
  • AOP 部分
    • 日志切面:记录所有方法的调用日志
    • 事务切面:在创建和取消方法上开启事务(@Transactional)
关键点:业务代码保持纯粹,横切关注点通过 AOP 实现,修改日志格式或事务策略不影响业务代码。

🎯自测(点击展开)

什么是 AOP?它与 OOP 是什么关系?
AOP(面向切面编程)是将横切关注点从业务逻辑中分离出来的编程范式。AOP 是 OOP 的补充,OOP 解决业务逻辑的模块化,AOP 解决横切关注点的模块化,二者结合实现更彻底的模块化。
AOP 的五大核心概念是什么?
切面(Aspect)= 切入点 + 通知,是横切关注点的模块化;连接点(Join Point)程序执行中可插入切面的点;切点(Pointcut)匹配连接点的表达式;通知(Advice)在切点上执行的动作;织入(Weaving)将切面应用到目标对象的过程。
Spring AOP 有哪几种通知类型?各有什么用途?
@Before(前置,参数校验)、@After(后置,资源清理)、@AfterReturning(返回,处理返回值)、@AfterThrowing(异常,错误处理)、@Around(环绕,性能监控、事务管理)。
Spring AOP 的代理机制是什么?
Spring AOP 基于动态代理:目标类实现了接口时使用 JDK 动态代理(基于接口);目标类没有实现接口时使用 CGLIB 代理(基于继承,生成子类)。
什么是自调用问题?如何解决?
自调用问题:同类中方法 A 调用方法 B,不会触发 AOP,因为调用绕过了代理对象。解决方案:1. 注入自身代理对象;2. 启用 exposeProxy 并使用 AopContext.currentProxy()。
AOP 有哪些典型应用场景?
日志记录(统一方法调用日志)、事务管理(声明式事务 @Transactional)、权限控制(方法级安全检查)、性能监控(方法执行时间统计)、缓存管理、异常处理等。

📝强化题库

选择题点选即时判分;填空题输入后"检查"或"显示答案"。

已答 0/0答对 0正确率
已答 0/0答对 0