乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

AOP核心概念

定义与作用

AOP(Aspect Oriented Programming,面向切面编程) 是对 OOP(Object Oriented Programming,面向对象编程)的补充,用于将**横切关注点(Cross-cutting Concerns)**从业务逻辑中解耦。

在传统的面向对象编程中,日志记录、权限校验、事务管理、性能监控等功能往往散落在各个业务类中,导致代码重复度高、维护困难。AOP 通过将这些横切关注点抽取为独立的切面(Aspect),在程序运行期动态织入到目标方法中,实现"业务代码专注于业务,切面代码专注于增强"的清晰分层。

Spring AOP 基于代理模式实现,默认只对运行时的方法调用进行拦截。它与 AspectJ 的关系是:Spring 使用 AspectJ 的注解语法(如 @Aspect、@Pointcut)定义切面,但运行时仍基于 Spring AOP 代理完成织入,而非 AspectJ 的编译期或加载期织入。

AOP核心术语

术语英文含义
切面Aspect横切关注点的模块化,包含 Advice 和 Pointcut
连接点Join Point程序执行过程中的一个点,如方法调用、异常抛出
通知Advice切面在特定连接点采取的动作
切点Pointcut匹配连接点的谓词,定义 Advice 何时触发
引入Introduction为现有类型声明额外的方法或字段
织入Weaving将切面应用到目标对象创建代理对象的过程
代理Proxy包装目标对象的中间对象,客户端通过代理间接调用目标方法
目标对象Target被切面织入通知的原始业务对象

Aspect(切面)

切面是横切关注点的模块化封装。一个切面类通常包含两类核心成员:

  • Pointcut(切入点):定义"在哪里切入",即匹配哪些方法
  • Advice(通知):定义"切入后做什么",即在匹配的方法上执行何种增强逻辑

JoinPoint(连接点)

连接点是程序执行过程中的某个特定点。在 Spring AOP 中,由于基于代理实现,只支持方法级别的连接点,不支持字段修改、构造器调用等更细粒度的连接点(完整支持需使用 AspectJ)。

Advice(通知/增强)

Advice 是切面在特定连接点执行的动作。Spring AOP 支持五种 Advice,对应方法执行的不同阶段:

Advice 类型注解执行时机
Before@Before方法执行前
After@After方法执行后(无论是否异常)
AfterReturning@AfterReturning方法成功返回后
AfterThrowing@AfterThrowing方法抛出异常后
Around@Around包围方法执行,可控制是否执行原方法

Pointcut(切入点)

Pointcut 是匹配连接点的谓词表达式,定义了 Advice 应该在哪些方法上触发。Spring 支持多种 Pointcut 表达式:

表达式含义
execution(* com.example.service.*.*(..))匹配 service 包下所有类的所有方法
execution(public * *(..))匹配所有 public 方法
@annotation(com.example.Loggable)匹配带有 @Loggable 注解的方法
within(com.example.service.*)匹配 service 包下所有类的所有方法
args(java.lang.String)匹配参数为 String 类型的方法
bean(*Service)匹配 Bean 名以 Service 结尾的方法

Weaving(织入)

织入是将切面应用到目标对象以创建代理对象的过程。Spring AOP 在运行时通过代理完成织入(Runtime Weaving),而 AspectJ 支持编译期织入(Compile-time Weaving)和加载期织入(Load-time Weaving)。

Proxy(代理)

代理是包装目标对象的中间对象。客户端通过代理间接调用目标方法,从而在不修改源码的情况下插入增强逻辑。Spring AOP 的代理方式有两种:JDK 动态代理和 CGLIB 代理。

核心原理:Spring AOP代理机制

AOP核心概念关系图

JDK动态代理 vs CGLIB代理

Spring AOP 基于代理模式实现,根据目标类的特性选择不同的代理方式:

代理方式实现机制适用场景限制
JDK 动态代理目标类实现接口时,生成接口代理面向接口编程只能代理接口方法,无法代理类内部 this 调用
CGLIB 代理生成目标类的子类,通过方法重写拦截无接口的类无法代理 final 类和方法

Spring Boot 2.x+ 默认配置:spring.aop.proxy-target-class=true,即优先使用 CGLIB。即使实现了接口,也可强制使用 JDK 代理:@EnableAspectJAutoProxy(proxyTargetClass = false)。

Advice执行顺序时序图

当多个 Advice 同时作用于一个方法时,Spring 按照严格的顺序执行:

执行顺序总结(正常返回):

@Around(proceed 前)→ @Before → 目标方法 → @AfterReturning → @After → @Around(proceed 后)

执行顺序总结(异常抛出):

@Around(proceed 前)→ @Before → 目标方法 → @AfterThrowing → @After → @Around(捕获异常)

完整示例:飞翔科技学生管理系统日志切面

场景简述

广州飞翔科技公司的技术部正在开发一套学生成绩管理系统。架构师白歌提出:每个 Service 层方法的执行耗时都需要记录,以便大翔(CTO)在系统上线后排查性能瓶颈。小崔(后端开发)决定使用 AOP 实现这一需求,避免在每个业务方法中重复编写计时代码。

操作前代码/配置

在未使用 AOP 之前,小崔需要在每个 Service 方法中手动编写日志和计时逻辑:

@Service
public class StudentScoreService {

    public Score queryScore(Long studentId) {
        long start = System.currentTimeMillis();
        System.out.println("[LOG] 开始查询学生成绩,studentId=" + studentId);
        
        // 业务逻辑:查询数据库
        Score score = scoreDao.findByStudentId(studentId);
        
        long duration = System.currentTimeMillis() - start;
        System.out.println("[LOG] 查询完成,耗时 " + duration + "ms");
        return score;
    }

    public void updateScore(Long studentId, Double newScore) {
        long start = System.currentTimeMillis();
        System.out.println("[LOG] 开始更新学生成绩,studentId=" + studentId);
        
        // 业务逻辑:更新数据库
        scoreDao.updateScore(studentId, newScore);
        
        long duration = System.currentTimeMillis() - start;
        System.out.println("[LOG] 更新完成,耗时 " + duration + "ms");
    }
}

问题:每个方法都重复了计时和日志代码,业务逻辑被淹没在横切关注点中。如果大翔要求增加"记录操作人"功能,所有方法都需要再次修改。

使用AOP的完整代码

第一步:开启 AspectJ 自动代理

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.feixiang.student")
public class AppConfig {
}

第二步:定义切面类

@Aspect
@Component
public class PerformanceLogAspect {

    // 定义切点:Service 层所有方法
    @Pointcut("execution(* com.feixiang.student.service.*.*(..))")
    public void serviceLayer() {}

    // 环绕通知:记录方法耗时
    @Around("serviceLayer()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();

        System.out.printf("[飞翔科技-性能监控] %s.%s 开始执行%n", className, methodName);

        try {
            Object result = joinPoint.proceed();
            long duration = System.currentTimeMillis() - start;
            System.out.printf("[飞翔科技-性能监控] %s.%s 执行成功,耗时 %dms%n", 
                className, methodName, duration);
            return result;
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - start;
            System.err.printf("[飞翔科技-性能监控] %s.%s 执行失败,耗时 %dms,异常:%s%n", 
                className, methodName, duration, e.getMessage());
            throw e;
        }
    }
}

第三步:纯净的业务 Service

@Service
public class StudentScoreService {

    private final ScoreDao scoreDao;

    public StudentScoreService(ScoreDao scoreDao) {
        this.scoreDao = scoreDao;
    }

    public Score queryScore(Long studentId) {
        // 纯粹的业务逻辑,无任何日志/计时代码
        return scoreDao.findByStudentId(studentId);
    }

    public void updateScore(Long studentId, Double newScore) {
        // 纯粹的业务逻辑
        scoreDao.updateScore(studentId, newScore);
    }
}

操作后运行结果及分析

运行测试代码:

public class AopDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        
        StudentScoreService service = ctx.getBean(StudentScoreService.class);
        service.queryScore(2024001L);
        
        ctx.close();
    }
}

控制台输出:

[飞翔科技-性能监控] StudentScoreService.queryScore 开始执行
[飞翔科技-性能监控] StudentScoreService.queryScore 执行成功,耗时 23ms

分析:

  • 业务代码中完全消除了计时和日志逻辑,StudentScoreService 只关注"查询成绩"和"更新成绩"
  • 切面 PerformanceLogAspect 统一处理所有 Service 方法的性能监控
  • 当大翔需要增加新功能(如记录操作人 IP)时,只需修改切面类,无需触碰业务代码
  • 符合开闭原则(Open/Closed Principle):对扩展开放,对修改关闭

易错场景与面试考点

易错场景一:同类内部 this.method() 调用不触发 AOP

这是 Spring AOP 代理机制下最常见也最容易踩坑的问题。

反例代码:

@Service
public class StudentScoreService {

    public void batchUpdate(List<StudentScore> scores) {
        for (StudentScore score : scores) {
            // 危险!这里调用的是目标对象本身,而非代理对象
            this.updateScore(score.getStudentId(), score.getNewScore());
        }
    }

    public void updateScore(Long studentId, Double newScore) {
        // 此方法上有 AOP 权限校验切面
        scoreDao.updateScore(studentId, newScore);
    }
}

问题:batchUpdate 中通过 this.updateScore() 调用同类方法时,不会触发 AOP 拦截。因为 this 指向的是目标对象本身,而非 Spring 容器生成的代理对象。代理对象上的权限校验、日志记录等 Advice 全部失效。

解决方案:

方案一:注入自身代理(推荐用于循环依赖已解决的场景)

@Service
public class StudentScoreService {

    @Autowired
    private StudentScoreService self;

    public void batchUpdate(List<StudentScore> scores) {
        for (StudentScore score : scores) {
            // 通过代理对象调用,AOP 生效
            self.updateScore(score.getStudentId(), score.getNewScore());
        }
    }

    public void updateScore(Long studentId, Double newScore) {
        scoreDao.updateScore(studentId, newScore);
    }
}

方案二:使用 AopContext.currentProxy()

@Service
public class StudentScoreService {

    public void batchUpdate(List<StudentScore> scores) {
        for (StudentScore score : scores) {
            // 获取当前代理对象
            ((StudentScoreService) AopContext.currentProxy())
                .updateScore(score.getStudentId(), score.getNewScore());
        }
    }

    public void updateScore(Long studentId, Double newScore) {
        scoreDao.updateScore(studentId, newScore);
    }
}

使用 AopContext.currentProxy() 时,需在 @EnableAspectJAutoProxy 上设置 exposeProxy = true:

@EnableAspectJAutoProxy(exposeProxy = true)

方案三:重构避免内部调用

将 updateScore 抽取到另一个 Service 中,通过依赖注入调用:

@Service
public class StudentScoreService {

    private final ScoreUpdateHelper helper;

    public StudentScoreService(ScoreUpdateHelper helper) {
        this.helper = helper;
    }

    public void batchUpdate(List<StudentScore> scores) {
        for (StudentScore score : scores) {
            helper.updateScore(score.getStudentId(), score.getNewScore());
        }
    }
}

面试高频考点

考点一:JDK 动态代理和 CGLIB 代理的区别?Spring 如何选择?

JDK 代理要求目标类实现接口,基于 InvocationHandler.invoke() 拦截;CGLIB 生成目标类的子类,基于 MethodInterceptor.intercept() 拦截。Spring 默认优先使用 JDK 代理(目标有接口时),但 Spring Boot 2.x+ 默认 proxyTargetClass=true 优先使用 CGLIB。CGLIB 无法代理 final 类和方法,JDK 代理只能代理接口方法。

考点二:AOP 的五种 Advice 分别在什么时候执行?

Before(方法前)、After(方法后,无论异常)、AfterReturning(成功返回后)、AfterThrowing(异常抛出后)、Around(包围整个方法执行,可控制是否执行原方法)。正常返回顺序:Around(前)→ Before → 目标方法 → AfterReturning → After → Around(后)。异常顺序:Around(前)→ Before → 目标方法 → AfterThrowing → After → Around(捕获)。

考点三:同类内部方法调用为什么 AOP 不生效?如何解决?

因为 this.method() 调用的是目标对象本身,而非代理对象。解决方案:① 注入自身代理;② 使用 AopContext.currentProxy()(需 exposeProxy = true);③ 重构避免内部调用,将方法抽取到另一个 Bean 中。

上一页
AOP面向切面编程
下一页
@EnableAspectJAutoProxy