当前位置:首页 > 技术 > 正文内容

Spring AOP 切面编程详解

访客 技术 2026年6月15日 1

在软件开发中,我们经常需要对业务逻辑进行统一处理,例如日志记录、权限验证、性能监控等。传统的方式是在每个业务方法中重复编写这些代码,导致代码冗余且难以维护。AOP(面向切面编程)就是为了解决这类问题而生的。

本文将详细介绍 Spring AOP 的核心概念、实现步骤以及底层原理。

一、AOP 核心概念

1.1 切面(Aspect)

切面是 AOP 的核心元素,它定义了"做什么"的问题。切面由切点和通知组成,既包含了横切逻辑的定义,也包括了连接点的定义。在 Spring 中,切面通常是一个使用 @Aspect 注解标注的类。

1.2 切点(Pointcut)

切点解决了"在哪里执行"的问题。切点提供了一套规则(使用 AspectJ 切点表达式语言描述),用于匹配满足条件的连接点。只有被切点匹配到的连接点,才会执行相应的通知。

1.3 通知(Advice)

通知定义了切面"何时执行"以及"执行什么"。Spring 框架提供了五种通知类型:

  • @Before 前置通知:在目标方法执行之前执行
  • @After 后置通知:在目标方法执行之后执行,无论是否抛出异常
  • @AfterReturning 返回通知:在目标方法成功返回后执行
  • @AfterThrowing 异常通知:在目标方法抛出异常后执行
  • @Around 环绕通知:包裹目标方法,可以在方法执行前后自定义行为

1.4 连接点(Join Point)

连接点是应用执行过程中可以插入切面的点,通常是方法的调用处、异常抛出处或字段修改处。切点匹配的所有连接点都会被通知。

1.5 织入(Weaving)

织入是将切面应用到目标对象的过程,可以在编译期、类加载期或运行时完成。Spring AOP 主要在运行时完成织入。

二、Spring AOP 快速入门

2.1 添加依赖

在 Spring Boot 项目中,只需添加 starter 依赖即可启用 AOP 支持:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 定义切点和切面

首先定义一个简单的控制器类:

package com.test.sample.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/product")
public class ProductController {

    @GetMapping("/list")
    public String listProducts() {
        System.out.println("查询产品列表");
        return "product list";
    }

    @PostMapping("/add")
    public String addProduct() {
        System.out.println("添加产品");
        return "add success";
    }

    @PostMapping("/delete")
    public String deleteProduct() {
        System.out.println("删除产品");
        return "delete success";
    }
}

接下来定义切面类:

package com.test.sample.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspect {

    // 定义切点:拦截 ProductController 中所有方法
    @Pointcut("execution(* com.test.sample.controller.ProductController.*(..))")
    public void pointcut() {
        // 空方法,仅作为标识作用
    }
}

切点表达式说明:

  • execution:表示执行方法时触发
  • *:匹配任意返回类型
  • com.test.sample.controller.ProductController:完全限定类名
  • .*:匹配该类中所有方法
  • (..):匹配任意参数列表

2.3 通知类型实践

前置通知

@Aspect
@Component
public class LogAspect {

    @Pointcut("execution(* com.test.sample.controller.ProductController.*(..))")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void beforeAdvice() {
        System.out.println("=== 前置通知:开始执行 ===");
    }
}

测试对比:访问 http://localhost:8080/product/list(被拦截)和 http://localhost:8080/user/info(未被拦截)

后置通知

@Aspect
@Component
public class LogAspect {

    @Pointcut("execution(* com.test.sample.controller.ProductController.*(..))")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void beforeAdvice() {
        System.out.println("=== 前置通知 ===");
    }

    @After("pointcut()")
    public void afterAdvice() {
        System.out.println("=== 后置通知 ===");
    }
}

环绕通知

环绕通知功能最强大,可以控制目标方法的执行流程:

@Aspect
@Component
public class LogAspect {

    @Pointcut("execution(* com.test.sample.controller.ProductController.*(..))")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        System.out.println("环绕通知:方法执行前");
        
        // 执行目标方法
        Object result = joinPoint.proceed();
        
        long endTime = System.currentTimeMillis();
        System.out.println("环绕通知:方法执行后");
        System.out.println("方法执行耗时:" + (endTime - startTime) + "ms");
        
        return result;
    }
}

三、动态代理原理

Spring AOP 基于动态代理实现,理解其原理有助于更好地使用 AOP。

3.1 静态代理 vs 动态代理

静态代理在编译时就确定了代理关系,需要为每个被代理类创建代理类,代码冗余度高。

动态代理在运行时动态生成代理类,无需预先定义代理类,代码复用性高。

举例说明:假设一个班级有三名学生同时提交作业,没有动态代理时,需要老师逐一收取;有动态代理时,课代表(代理)一次性收取所有作业。

3.2 JDK 动态代理

JDK 动态代理要求目标类必须实现接口,通过 java.lang.reflect.Proxy 和 InvocationHandler 实现:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class WechatPayInvocationHandler implements InvocationHandler {
    
    private Object target;
    
    public WechatPayInvocationHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 前置处理
        System.out.println("验证签名");
        System.out.println("记录日志");
        
        // 调用目标方法
        Object result = method.invoke(target, args);
        
        // 后置处理
        System.out.println("统计耗时");
        
        return result;
    }
    
    public static void main(String[] args) {
        PayService target = new WechatPayService();
        
        InvocationHandler handler = new WechatPayInvocationHandler(target);
        
        PayService proxy = (PayService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            new Class[]{PayService.class},
            handler
        );
        
        proxy.processPayment();
    }
}

特点:

  • 优点:实现简单,无需额外依赖
  • 缺点:只能代理实现接口的类

3.3 CGLIB 动态代理

CGLIB 通过继承方式实现代理,无需目标类实现接口:

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class PaymentMethodInterceptor implements MethodInterceptor {
    
    private Object target;
    
    public PaymentMethodInterceptor(Object target) {
        this.target = target;
    }
    
    @Override
    public Object intercept(Object obj, Method method, Object[] args, 
                           MethodProxy proxy) throws Throwable {
        System.out.println("验证签名");
        System.out.println("记录日志");
        
        Object result = proxy.invoke(target, args);
        
        System.out.println("统计耗时");
        
        return result;
    }
    
    public static void main(String[] args) {
        PayService target = new WechatPayService();
        
        PayService proxy = (PayService) Enhancer.create(
            target.getClass(),
            new PaymentMethodInterceptor(target)
        );
        
        proxy.processPayment();
    }
}

特点:

  • 优点:可代理未实现接口的类
  • 缺点:无法代理 final 类和 final 方法

3.4 两种代理方式对比

特性 JDK 动态代理 CGLIB 动态代理
来源 Java 标准库 第三方库
实现方式 接口实现 继承子类
代理范围 实现接口的类 任意类
final 类 支持 不支持
性能(JDK 7+) 略优 略差

Spring AOP 自动选择代理方式:实现了接口的类使用 JDK 代理,否则使用 CGLIB 代理。

四、总结

Spring AOP 提供了一种优雅的方式来实现横切关注点的分离,通过切面、切点、通知等概念,可以将日志、权限、事务等通用逻辑与业务逻辑分离。理解动态代理的实现原理有助于更好地使用和调试 AOP 相关代码。

标签: SpringAOP

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。