Java-Spring入门指南(十二)SpringAop的三种实现方式
Java-Spring入门指南(十二)SpringAOP的三种实现方式
- 前言
- 一、AOP是什么,有什么用,作用是什么?
- 二、AOP的三种实现方式
- 方式一:基于Spring Advice接口实现
- 1. 项目目录结构
- 2. 核心代码实现
- (1)业务接口与实现类
- (2)切面类(实现Advice接口)
- (3)Spring配置文件(applicationContext.xml)
- 核心知识点总结
- 3. 测试代码与结果
- 方式一总结
- 方式二:自定义切面实现(不依赖Spring接口)
- 1. 项目目录结构
- 2. 核心代码实现
- (1)业务接口与实现类
- (2)自定义切面类(DiyPointCut)
- (3)Spring配置文件(applicationContext.xml)
- 3. 测试代码与结果
- 方式三:注解驱动实现(@Aspect注解)
- 1. 项目目录结构
- 2. 核心代码实现
- (1)业务接口与实现类
- (2)注解式切面类(AnnotationPointCut)
- (3)Spring配置文件(applicationContext.xml)
- 3. 测试代码与结果
- 三种实现方式对比
前言
-
在前一篇博客中,我们已经掌握了代理模式的核心逻辑,并初步认识了Spring AOP的概念——通过“切面”为业务方法动态添加增强功能,解决横切关注点(如日志、事务)与核心业务的耦合问题。
-
而在实际开发中,Spring AOP提供了多种灵活的实现方式,适配不同的场景需求。
-
本篇将聚焦Spring AOP的三种核心实现方式,从基于接口的通知实现、自定义切面实现,到注解驱动实现,逐步拆解代码逻辑,帮助你掌握不同方式的配置细节与适用场景,真正将AOP落地到项目开发中。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的Java-Spring入门指南知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_13040333.html?spm=1001.2014.3001.5482
Spring的官方AOP讲解网站
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop
一、AOP是什么,有什么用,作用是什么?
在正式讲解实现方式前,我们先快速回顾上一篇的核心内容。
-
AOP是什么:全称
Aspect Oriented Programming
(面向切面编程),是一种编程思想。它将分散在多个业务方法中的“通用逻辑”(如日志、权限校验)抽取为“切面(Aspect)”,在指定时机(如方法执行前/后)动态“织入”到目标方法中,无需修改业务代码。 -
AOP有什么用:解决“横切关注点”问题。横切关注点是指与核心业务无关,但需重复出现在多个方法中的逻辑(如记录每个业务方法的执行日志)。AOP让这些逻辑只需编写一次,即可作用于多个方法,减少代码冗余、降低耦合、便于统一维护。
-
AOP的核心作用场景:
- 日志记录:自动记录方法调用信息(参数、返回值、执行时间);
- 事务管理:方法执行前开启事务,执行成功提交、失败回滚;
- 权限校验:方法执行前校验用户是否有权限操作;
- 异常处理:统一捕获方法执行中的异常并处理。
-
AOP核心组件回顾:
- 切面(Aspect):封装横切逻辑的类(如“日志切面”);
- 通知(Advice):切面中的具体增强逻辑(如“方法执行前打印日志”);
- 切入点(Pointcut):通过表达式指定“哪些方法需要被增强”;
- 织入(Weaving):将通知动态植入目标方法的过程(Spring在运行时完成)。
二、AOP的三种实现方式
Spring AOP的实现方式围绕“如何定义切面与通知”展开,核心分为基于Spring接口的实现、自定义切面实现、注解驱动实现三种。
使用Spring AOP需添加
aspectjweaver
依赖:
<!-- AspectJ织入依赖:支持AOP功能的核心依赖 -->
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.9.1</version>
</dependency>
方式一:基于Spring Advice接口实现
这种方式需让“切面类”实现Spring提供的Advice
系列接口(如MethodBeforeAdvice
前置通知、AfterReturningAdvice
后置通知),通过接口方法定义增强逻辑。Spring会自动识别实现类为“通知类”,再通过XML配置绑定“通知”与“切入点”。
1. 项目目录结构
先明确代码组织,将该方式的类放在com.niit.aop1
包下:
spring_aop
└── src└── main├── java│ └── com.niit│ └── aop1 # 方式一的代码包│ ├── LogBefore.java # 切面类(实现MethodBeforeAdvice)│ ├── StudentService.java # 业务接口│ └── StudentServiceImpl.java # 业务实现类└── resources└── applicationContext.xml # Spring核心配置文件
2. 核心代码实现
(1)业务接口与实现类
首先定义业务逻辑(学生管理的增删改查),这是需要被AOP增强的“目标对象”:
- StudentService.java(业务接口)
// 学生业务接口:定义核心业务方法
public interface StudentService {void add(); // 添加学生void del(); // 删除学生void update(); // 修改学生void query(); // 查询学生
}
- StudentServiceImpl.java(业务实现类)
import org.springframework.stereotype.Component;// 注册为Spring Bean,id为"ssi"(便于后续从容器中获取)
@Component("ssi")
public class StudentServiceImpl implements StudentService {// 核心业务逻辑:仅关注“做什么”,不关心日志等增强逻辑@Overridepublic void add() {System.out.println("【核心业务】添加学生");}@Overridepublic void del() {System.out.println("【核心业务】删除学生");}@Overridepublic void update() {System.out.println("【核心业务】修改学生");}@Overridepublic void query() {System.out.println("【核心业务】查询学生");}
}
(2)切面类(实现Advice接口)
创建LogBefore
类,实现MethodBeforeAdvice
接口(Spring提供的“前置通知”接口),在before
方法中定义“方法执行前”的增强逻辑(打印日志):
- LogBefore.java(切面类)
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;// 注册为Spring Bean(让Spring管理切面类)
@Component
// 实现MethodBeforeAdvice:代表“目标方法执行前”的增强
public class LogBefore implements MethodBeforeAdvice {/*** 前置通知的核心逻辑:目标方法执行前会自动调用此方法* @param method 被增强的目标方法(如add()、del())* @param args 目标方法的参数(本例中无参数,为null)* @param target 被增强的目标对象(如StudentServiceImpl实例)*/@Overridepublic void before(Method method, Object[] args, Object target) throws Throwable {// 增强逻辑:打印“哪个类的哪个方法即将执行”System.out.println("【AOP前置增强】" + "类:" + target.getClass().getName() + ",方法:" + method.getName() + " 即将执行");}
}
(3)Spring配置文件(applicationContext.xml)
通过XML配置“切入点”(指定增强哪些方法)和“通知绑定”(将LogBefore的增强逻辑织入切入点):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><!-- 1. 组件扫描:扫描com.niit包下的@Component注解,注册Bean --><context:component-scan base-package="com.niit"/><!-- 2. AOP核心配置:方式一(基于Advice接口) --><aop:config proxy-target-class="false"><aop:pointcut id="pointcut" expression="execution(* com.niit.aop1.*.add())"/><aop:advisor advice-ref="logBefore" pointcut-ref="pointcut"></aop:advisor></aop:config></beans>
- 核心代码解析
1. 根标签 aop:config
- 作用:是Spring AOP XML配置的根标签,所有AOP相关的配置(切入点、通知、切面关联等)都需要定义在该标签内部。
- 属性
proxy-target-class
:
用于指定Spring AOP生成代理的方式:proxy-target-class="false"
(默认值):使用JDK动态代理,要求目标类必须实现接口,代理对象是接口的实现类。proxy-target-class="true"
:使用CGLIB代理,可以代理没有实现接口的类(通过继承目标类生成代理子类)。
2. 切入点配置 aop:pointcut
- 作用:定义"切入点"(Pointcut),即AOP要拦截的方法(哪些方法需要被增强)。
- 属性详解:
id="pointcut"
:给切入点起一个唯一标识,方便后续引用(如在<aop:advisor>
中关联)。expression
:切入点表达式,用于精确匹配需要拦截的方法,核心语法是execution()
(最常用的表达式类型)。
切入点表达式 execution(* com.niit.aop1.*.add()) 解析
execution()
用于匹配方法执行的连接点,语法结构为:
execution(修饰符 返回值类型 包名.类名.方法名(参数列表))
*
:第一个*
表示"任意返回值类型"(如void、int、Object等)。com.niit.aop1.*
:com.niit.aop1
是包名,后面的*
表示"该包下的所有类"。.add()
:表示匹配类中的add()
方法,且无参数(如果有参数,需写成add(参数类型)
,如add(int)
)。
整体含义:拦截com.niit.aop1
包下所有类中的add()
无参方法。
3. 切面关联 aop:advisor
-
作用:将"通知(Advice)"和"切入点(Pointcut)“关联起来,形成一个完整的"切面(Aspect)”。
(这里的advisor
是Spring AOP中基于Advice
接口的特殊切面形式,一个advisor
只能关联一个通知和一个切入点) -
属性详解:
advice-ref="logBefore"
:引用一个"通知Bean"(id为logBefore
),该Bean需实现Spring的Advice
接口(如MethodBeforeAdvice
前置通知、AfterReturningAdvice
后置通知等)。pointcut-ref="pointcut"
:引用前面定义的切入点(id为pointcut
),表示"通知"要作用在这个切入点匹配的方法上。
核心知识点总结
-
AOP核心概念:
- 切入点(Pointcut):要拦截的方法(通过表达式定义)。
- 通知(Advice):拦截方法后要执行的逻辑(如日志、事务、权限校验等),需实现
Advice
接口。 - 切面(Aspect):切入点 + 通知的组合(这里通过
<aop:advisor>
实现)。
-
基于Advice接口的特点:
这是Spring早期的AOP配置方式,通知必须严格实现Spring提供的Advice
相关接口(如MethodBeforeAdvice
、AfterReturningAdvice
),灵活性较低。现在更多使用基于AspectJ的注解方式(如@Before
、@After
)。 -
代理方式选择:
- JDK代理(默认):依赖接口,性能较好。
- CGLIB代理:不依赖接口,可代理任意类,但会生成子类,性能略低。
3. 测试代码与结果
编写Junit测试类,从Spring容器中获取业务Bean,调用方法验证AOP是否生效:
- 测试类代码
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class AopTest {@Testpublic void test1() {// 1. 加载Spring配置文件,初始化容器ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");// 2. 获取业务Bean:注意!若proxy-target-class=false(JDK代理),需按接口获取StudentService studentService = (StudentService) ac.getBean("ssi");// 3. 调用方法:验证增强效果System.out.println("-----调用add()方法(会被增强)-----");studentService.add(); // add()在切入点中,会触发前置通知System.out.println("-----调用del()方法(不被增强)-----");studentService.del(); // del()不在切入点中,无增强逻辑System.out.println("-----调用query()方法(不被增强)-----");studentService.query(); // query()不在切入点中,无增强逻辑// 4. 关闭容器ac.close();}
}
- 测试结果
结果分析:仅add()
方法触发了前置增强(打印日志),del()
和query()
未被增强,符合切入点表达式的配置,说明AOP生效。
方式一总结
- 核心逻辑:切面类实现Spring的
Advice
接口,通过<aop:advisor>
绑定通知与切入点; - 代理方式:支持JDK(基于接口)和CGLIB(基于子类),由
proxy-target-class
控制; - 优缺点:实现简单,适合简单的通知场景;但需依赖Spring接口,灵活性较低(若需多个通知,需实现多个接口)。
方式二:自定义切面实现(不依赖Spring接口)
这种方式无需实现Spring的Advice
接口,而是自定义切面类(包含before、after等增强方法),再通过XML配置将自定义方法指定为“通知”,灵活性更高。
1. 项目目录结构
将该方式的类放在com.niit.aop2
包下,与方式一隔离:
spring_aop
└── src└── main├── java│ └── com.niit│ └── aop2 # 方式二的代码包│ ├── DiyPointCut.java # 自定义切面类(含增强方法)│ ├── StudentService.java # 业务接口(与方式一一致)│ └── StudentServiceImpl.java # 业务实现类└── resources└── applicationContext.xml # Spring配置文件
2. 核心代码实现
(1)业务接口与实现类
与方式一完全一致,仅Bean id改为“ssi2”(避免与方式一的Bean重名):
- StudentServiceImpl.java
package com.niit.aop2;import org.springframework.stereotype.Component;// Bean id改为"ssi2",与方式一的"ssi"区分
@Component("ssi2")
public class StudentServiceImpl implements StudentService {@Overridepublic void add() {System.out.println("【核心业务】添加学生");}@Overridepublic void del() {System.out.println("【核心业务】删除学生");}@Overridepublic void update() {System.out.println("【核心业务】修改学生");}@Overridepublic void query() {System.out.println("【核心业务】查询学生");}
}
(2)自定义切面类(DiyPointCut)
自定义类,直接编写before
(前置增强)、after
(后置增强)方法,无需实现任何接口:
- DiyPointCut.java
import org.springframework.stereotype.Component;// 注册为Spring Bean
@Component
// 自定义切面类:包含多个增强方法(before、after)
public class DiyPointCut {// 前置增强方法:目标方法执行前调用public void before() {System.out.println("【自定义前置增强】方法即将执行,准备参数校验...");}// 后置增强方法:目标方法执行后调用(无论是否抛出异常都会执行)public void after() {System.out.println("【自定义后置增强】方法执行完成,记录操作日志...");}// 环绕增强方法(可选):包裹目标方法,可在执行前后添加逻辑public void around() {System.out.println("【自定义环绕增强】方法执行前的准备工作...");// 注意:环绕增强若需执行目标方法,需配合ProceedingJoinPoint(后续方式三会演示)System.out.println("【自定义环绕增强】方法执行后的清理工作...");}
}
(3)Spring配置文件(applicationContext.xml)
重点使用<aop:aspect>
标签配置自定义切面,将切面中的方法绑定为“通知”:
<!-- 继续在applicationContext.xml中添加方式二的配置 -->
<!-- 方式二--><aop:config proxy-target-class="true"><!-- 这里用CGLIB代理(true),可直接按类获取Bean,false默认接口 --><aop:aspect ref="diyPointCut"> <!-- 1. 定义切面:ref指向自定义切面类(DiyPointCut的Bean id)diyPointCut是DiyPointCut类的默认Bean id(首字母小写) --><!-- 2. 定义切入点:增强com.niit.aop2包下的del()方法 --><aop:pointcut id="mydiyPointCut" expression="execution(* com.niit.aop2.*.del())"/><aop:after method="after" pointcut-ref="mydiyPointCut"></aop:after><aop:before method="before" pointcut-ref="mydiyPointCut"></aop:before> <!-- 3. 绑定增强方法与切入点:指定切面中的方法作为通知 --></aop:aspect></aop:config>
3. 测试代码与结果
- 测试类代码
@Test
public void test2() {ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");// 因proxy-target-class=true(CGLIB代理),可直接按类获取BeanStudentServiceImpl studentService = ac.getBean(StudentServiceImpl.class);System.out.println("-----调用del()方法(会被增强)-----");studentService.del(); // del()在切入点中,触发前置+后置增强System.out.println("-----调用add()方法(不被增强)-----");studentService.add(); // add()不在切入点中,无增强ac.close();
}
- 测试结果
方式三:注解驱动实现(@Aspect注解)
这是实际开发中最常用的方式——通过@Aspect
、@Before
、@After
等注解直接在切面类中定义“切面”和“通知”,无需复杂的XML配置,仅需开启注解驱动即可。
1. 项目目录结构
将该方式的类放在com.niit.aop3
包下:
spring_aop
└── src└── main├── java│ └── com.niit│ └── aop3 # 方式三的代码包│ ├── AnnotationPointCut.java # 注解式切面类│ ├── StudentService.java # 业务接口│ └── StudentServiceImpl.java # 业务实现类└── resources└── applicationContext.xml # Spring配置文件
2. 核心代码实现
(1)业务接口与实现类
Bean id改为“ssi3”,避免重名:
- StudentServiceImpl.java
package com.niit.aop3;import org.springframework.stereotype.Component;@Component("ssi3")
public class StudentServiceImpl implements StudentService {@Overridepublic void add() {System.out.println("【核心业务】添加学生");}@Overridepublic void del() {System.out.println("【核心业务】删除学生");}@Overridepublic void update() {System.out.println("【核心业务】修改学生");}@Overridepublic void query() {System.out.println("【核心业务】查询学生");}
}
(2)注解式切面类(AnnotationPointCut)
使用@Aspect
声明切面,@Before
、@After
、@Around
等注解定义通知,execution
表达式直接写在注解中:
- AnnotationPointCut.java
package com.niit.aop3;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;// 1. 注册为Spring Bean
@Component
// 2. @Aspect:声明当前类是“切面类”
@Aspect
public class AnnotationPointCut {// 3. @Before:前置通知,增强com.niit.aop3包下的add()方法@Before(value = "execution(* com.niit.aop3.*.add())")public void before() {System.out.println("【注解前置通知】add()方法即将执行");}// 4. @After:后置通知(目标方法执行后,无论是否异常)@After(value = "execution(* com.niit.aop3.*.add())")public void after() {System.out.println("【注解后置通知】add()方法执行完成");}// 5. @Around:环绕通知(最灵活,包裹目标方法,需手动执行目标方法)@Around(value = "execution(* com.niit.aop3.*.add())")public void around(ProceedingJoinPoint point) throws Throwable {System.out.println("【注解环绕通知】方法执行前:开启事务");// 关键:调用point.proceed()执行目标方法(add())point.proceed();System.out.println("【注解环绕通知】方法执行后:提交事务");}// 6. @AfterReturning:返回通知(目标方法正常执行完成后触发)@AfterReturning(value = "execution(* com.niit.aop3.*.add())")public void afterReturning() {System.out.println("【注解返回通知】add()方法正常返回,无异常");}
}
关键说明:
@Around
通知必须传入ProceedingJoinPoint
参数,通过point.proceed()
手动触发目标方法执行;- 通知执行顺序:环绕前 → 前置 → 目标方法 → 后置 → 环绕后 → 返回通知(若无异常)。
(3)Spring配置文件(applicationContext.xml)
无需复杂的<aop:config>
配置,仅需开启注解驱动的AOP即可:
<!-- 方式三:注解驱动AOP -->
<!-- 1. 组件扫描:扫描com.niit包下的@Component和@Aspect注解 -->
<context:component-scan base-package="com.niit"/><!-- 2. 开启AspectJ注解驱动:让Spring识别@Aspect、@Before等注解 -->
<aop:aspectj-autoproxy proxy-target-class="true"/> <!-- 可选,默认false(JDK代理),true(CGLIB代理) -->
3. 测试代码与结果
- 测试类代码
import com.niit.aop3.StudentService;
@Test
public void test3() {ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");StudentService studentService = (StudentService) ac.getBean("ssi3");System.out.println("-----调用add()方法(触发所有注解通知)-----");studentService.add();ac.close();
}
- 测试结果
三种实现方式对比
为了更清晰地选择合适的方式,我们通过表格总结核心差异:
实现方式 | 依赖接口 | 配置方式 | 灵活性 | 推荐度 | 适用场景 |
---|---|---|---|---|---|
基于Spring Advice接口 | 需实现Advice系列接口 | XML配置 | 低 | ★★☆☆☆ | 简单通知场景(仅需单一通知) |
自定义切面实现 | 无需接口 | XML配置 | 中 | ★★★☆☆ | 需多个通知,但不想用注解 |
注解驱动实现(@Aspect) | 无需接口 | 注解+少量XML | 高 | ★★★★★ | 企业级开发(推荐首选) |
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的Java-Spring入门指南知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_13040333.html?spm=1001.2014.3001.5482
非常感谢您的阅读,喜欢的话记得三连哦 |