Spring AOP
通过动态代理,我们可以很方便地给一个目标方法进行增强,每次增强就像在目标方法上包一圈,最终包成一棵”洋葱“。每层”洋葱皮“在许多的代理中都可以复用,这就构成了切面 Aspects。
AOP 概述
AOP(Aspect Oriented Programming):⾯向切⾯编程,⾯向⽅⾯编程。
AOP是⼀种编程技术, AOP是对OOP的补充延伸,底层使用动态代理实现。
AOP 用来解决交叉代码(如事务、安全、异常处理、日志等)的复用问题。
交叉代码:通用的、与业务不强关联的、辅助性的代码。交叉代码如果直接写入业务代码中,会导致:
- 交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复⽤。并且修改这些交叉业务代码的话,需要修改多处。
- 程序员⽆法专注核⼼业务代码的编写,在编写核⼼业务代码的同时还需要处理这些交叉业 务。

可不可以业务方法只关心业务,以增强的方式完成交叉代码的注入(织入)?
可以,使用动态代理技术,面向切面编程 AOP。
AOP:将与核⼼业务⽆关的代码独⽴的抽取出来,形成⼀个独⽴的组件,然后以横向交叉的⽅式应⽤到业务流程当中的过程被称为AOP。
AOP的优点:
- 代码复⽤性增强。
- 代码易维护。
- 使开发者更关注业务逻辑。
AOP 七大术语
连接点 Joinpoint
在程序的整个执⾏流程中,可以织入切面的位置。
⽅法的执⾏前后,异常抛出之后等位置。
切点 Pointcut
在程序执⾏流程中,真正织入切面的方法。
切点即目标方法,⼀个切点对应多个连接点(方法前后、异常处理等)。
通知 Advice
通知⼜叫增强,就是具体你要织⼊的代码。
通知包括: 前置通知、后置通知、环绕通知、异常通知、最终通知
切⾯ Aspect
切面 = 切点 + 通知。
织⼊ Weaving
把通知应用到目标对象上的过程。 (代理对象的构建过程)
代理对象 Proxy
⼀个⽬标对象被织⼊通知后产⽣的新对象。
⽬标对象 Target
被织⼊通知的对象。

切点表达式
切面 = 通知 + 切点。AOP 中,切面是织入多个目标对象的,即一个通知,可以应用于多个切点上,如何表达一类切点,即需要切点表达式。
使用切点表达式,可以很方便地表示一个通知应用的地方(即切点),也表示这一个切面的覆盖范围。
语法:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]⽅法名(形式参数列表) [异常])
// 完整表达
execution(访问控制权限修饰符 返回值类型 全限定类名.⽅法名(形式参数列表 异常)访问控制权限修饰符:可选。
没写就是四个权限都包括,使用
分隔多个修饰符。返回值类型:必需。
*表示任意返回值类型。全限定类名:可选。(包名.类名)
xxx.*表示 xxx 包下的所有类。xxx..表示 xxx 包及其子包下的所有类。(全限定类名以 xxx 开头的类)方法名:必需。和全限定类名用
.衔接。*表示类中的所有方法,支持模糊匹配xxx*表示所有的 xxx 开头的方法。形式参数列表:必需。
(..)表示任意形参(个数、类型不限)。(*)表示只有一个参数(类型不限)的方法。()表示没有参数(*, String)表示第一个参数类型随意,第二个参数String类型。异常:可选。
省略时表示任意异常类型。
- 参数表前即为方法名,方法名前即类名。
匹配规则:
- 在切点表达式覆盖的类中
- 在这些类中,匹配修饰符、返回值、方法名、参数表、抛出异常的方法
// mall包及子包下所有的类的所有的⽅法
execution(* com.powernode.mall..*(..))
// service包下所有的类中以delete开始的所有⽅法
execution(public * com.powernode.mall.service.*.delete*(..))
// 所有类的所有⽅法
execution(* *(..))Spring AOP
Spring对AOP的实现包括以下3种⽅式:
- Spring框架结合 AspectJ 框架实现的AOP,基于注解⽅式。
- Spring框架结合 AspectJ 框架实现的AOP,基于XML⽅式。
- Spring框架⾃⼰实现的AOP,基于XML配置⽅式。
- 实际开发中,都是Spring+AspectJ来实现AOP。而且使用注解方法居多。
AspectJ:Eclipse组织的⼀个⽀持AOP的框架。AspectJ项⽬起源于帕洛阿尔托(Palo Alto)研究中⼼(缩写为PARC)。该中⼼由Xerox集团资助, Gregor Kiczales领导,从1997年开始致⼒于AspectJ的开发,1998年第⼀次发布给外部⽤户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3⽉正式将AspectJ项⽬移交给了Eclipse组织,因为AspectJ的发展和受关注程度⼤⼤超出了PARC的预期,他们已经⽆⼒继续维持它的发展。
AspectJ框架是独⽴于Spring框架之外的⼀个框架,Spring框架⽤了AspectJ 来实现了 AOP。
引入依赖:
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring aop依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring aspects依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.0-M2</version>
</dependency>启用 context、aop 命名空间。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>AspectJ+注解
定义目标类和目标方法,并纳入 Spring 管理
// ⽬标类 @Component("orderService") public class OrderService { // ⽬标⽅法 public void generate(){ System.out.println("订单已⽣成!"); } }使用 @Aspect 定义切⾯类,并纳入 Spring 管理
// 切⾯类 @Component("myAspect") @Aspect public class MyAspect { }开启组件扫描、自动代理
<!--开启组件扫描--> <context:component-scan base-package="com.powernode.spring6.service"/> <!--开启⾃动代理--> <aop:aspectj-autoproxy proxy-target-class="true"/> <!-- 开启⾃动代理之后,@Aspect标注的Bean会解析其中的切点表达式,容器内的Bean都会进行代理,并织入匹配的通知--> <!-- proxy-target-class="true" 表示采⽤cglib动态代理。 proxy-target-class="false" 表示采⽤jdk动态代理。默认值是false。即使写成false,当没有接⼝的时候,也会⾃动选择cglib⽣成代理类。 -->在切⾯类中编写通知(即在方法中编写织入的代码)、声明切面。
// 切⾯类 @Aspect @Component public class MyAspect { // 通知 + 切点 = 切面 @声明通知的注解("切点表达式") // 织入的代码 public void advice(){ System.out.println("我是⼀个通知"); } }
声明通知的注解
@Before:前置通知,⽬标⽅法执行之前的通知代理方法伪码:
... void|... 方法名(参数表) throws ...{ @Before; rev = 执行目标方法; // 可能抛异常 if(返回值类型是 void){ return; } else{ return rev; } }@AfterReturning:后置通知,⽬标⽅法执行之后的通知。代理方法伪码:
... void|... 方法名(参数表) throws ...{ rev = 执行目标方法; // 可能抛异常 @AfterReturning; if(返回值类型是 void){ return; } else{ return rev; } }@Around:环绕通知,⽬标⽅法之前添加通知,同时⽬标⽅法执⾏之后添加通知。代理方法伪码:
... void|... 方法名(参数表) throws ...{ @Around-前; rev = 执行目标方法; // 可能抛异常 @Around-后; if(返回值类型是 void){ return; } else{ return rev; } }@AfterThrowing:异常通知,发⽣异常之后执⾏的通知代理方法伪码:
... void|... 方法名(参数表) throws ...{ try{ rev = 执行目标方法; // 可能抛异常 if(返回值类型是 void){ return; } else{ return rev; } } catch(...) { @AfterThrowing; // 异常通知 } }@After:最终通知,放在finally语句块中的通知... void|... 方法名(参数表) throws ...{ try{ rev = 执行目标方法; // 可能抛异常 if(返回值类型是 void){ return; } else{ return rev; } } finally { @After; // 最终通知 } }
?底层采用装饰模式完成各个通知的织入:
@AfterThrowing + @After > @Around > @AfterReturning + @Before > 目标对象。
- 优先级越小的通知,越后包装。如:@Around(1) > @Around(2)
扁平化代理方法伪码:
... void|... 方法名(参数表) throws ...{
try{
@Around-前;
@Before;
rev = 执行目标方法; // 可能抛异常
@AfterReturning; // 后置通知
@Around-后;
// try-catch 语法:finally 语句块一定会在返回前执行
if(返回值类型是 void){
return;
} else{
return rev;
}
} catch(...) {
@AfterThrowing; // 异常通知
} finally {
@After; // 最终通知
}
}- 在不发生异常的情况下,环绕通知会环绕前置通知和后置通知。
- 出现异常之后,后置通知和环绕通知的后半部分不会执⾏。
- 最终通知一定会执行。
通知允许的方法参数
无参的方法
@Before("pointcut001()") public void beforeAdvice(){ System.out.println("前置通知"); }代理伪码:如上。
接收 ProceedingJoinPoint对象的方法:@Around 要实现前后增强的整体逻辑必须用这个。
@Around("pointcut001()") public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("环绕通知开始"); // 前增强 proceedingJoinPoint.proceed(); // 执⾏⽬标⽅法 System.out.println("环绕通知结束"); // 后增强 }代理伪码:执行这个方法。
joinpoint.getSignature().getName()可以获取目标方法的方法名。接收
@AfterThrowing处理的异常对象@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex") // throwing 属性指定通知中接收异常对象的形参名,形参类型必需能接收切点表达式所涵盖的异常类型 public void handleException(Exception ex) { // 在这里处理捕获到的异常 System.out.println("Exception caught: " + ex.getMessage()); }
切面的先后顺序
当需要织入多个同一类的通知时,可以使用 @Order 注解来标注这些通知的先后顺序。
@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {
// ...
}- 在切面类上使用 @Order 注解标注这个切面类所有通知的优先级。数字越小,优先级越高。
切点的引用
可以使用 @Pointcut 注解单独标注一个切点,从而使切点表达式得到复用,在需要的地方引用即可。
@Component
@Aspect
public class MyAspect {
// @Pointcut 标注切点表达式
@Pointcut("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void pointcut001(){} // 方法无关紧要
// 使用被标注的方法名引用切点表达式
// 可以使用 || && 对切点表达式进行复合
@Before("pointcut001()")
public void beforeAdvice(){
System.out.println("前置通知");
}
// ...
}全注解式AOP
全注解式开发,即提供一个类,在这个类中使用注解标注配置信息,代替配置文件。
// 标注解析这个类获取配置信息
@Configuration
// 开启包扫描
@ComponentScan("com.powernode.spring6.service")
// 启用自动代理,使用CGLIB,即 <aop:aspectj-autoproxy proxy-target-class="true"/>
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Configuration {
}
// 使用 AnnotationConfigApplicationContext 类解析类获取配置信息
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(Spring6Configuration.class);AspectJ + xml
编写目标类、目标方法
// ⽬标类 public class VipService { // 目标方法 public void add(){ System.out.println("保存vip信息。"); } }编写切⾯类,并且编写通知
public class TimerAspect { // 通知(增强的代码) public void time(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long begin = System.currentTimeMillis(); proceedingJoinPoint.proceed(); //执⾏⽬标 long end = System.currentTimeMillis(); System.out.println("耗时"+(end - begin)+"毫秒"); } }编写spring配置⽂件
<!--开启 context、aop 命名空间--> <!--将目标类、切面类纳⼊spring bean管理--> <bean id="vipService" class="com.powernode.spring6.service.VipService"/> <bean id="timerAspect" class="com.powernode.spring6.service.TimerAspect"/> <!--aop配置--> <aop:config> <!--切点表达式:id是这个切点表达式的唯一标识--> <aop:pointcut id="p" expression="execution(* com.powernode.spring6.service.VipService.*(..))"/> <!--切⾯类--> <aop:aspect ref="timerAspect"> <!--切⾯ = 通知(切面类中的某个方法) + 切点--> <aop:around method="time" pointcut-ref="p"/> </aop:aspect> </aop:config>
Spring AOP案例
事务处理
控制事务的代码是固定的格式,无非:
try{
// 开启事务(前增强)
startTransaction();
// 执⾏核⼼业务逻辑(目标方法)
//......
// 提交事务(后增强)
commitTransaction();
}catch(Exception e){
// 回滚事务(异常处理)
rollbackTransaction();
}很显然,控制事务的代码就是和业务逻辑没有关系的交叉业务,可以使用AOP完成对的事务控制,通过织入环绕通知(处理回滚的异常)即可实现。
业务类:
@Component
// 业务类
public class AccountService {
// 转账业务⽅法
public void transfer(){
System.out.println("正在进⾏银⾏账户转账");
}
// 取款业务⽅法
public void withdraw(){
System.out.println("正在进⾏取款操作");
}
}
@Component
// 业务类
public class OrderService {
// ⽣成订单业务⽅法
public void generate(){
System.out.println("正在⽣成订单");
}
// 取消订单业务⽅法
public void cancel(){
System.out.println("正在取消订单");
}
}
// 织入切面
@Aspect
@Component
// 事务切⾯类
public class TransactionAspect {
@Around("execution(* com.powernode.spring6.biz..*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
try {
System.out.println("开启事务");
// 执⾏⽬标
proceedingJoinPoint.proceed();
System.out.println("提交事务");
} catch (Throwable e) {
System.out.println("回滚事务");
}
}
}- 编写代码来控制事务,是编程式事务。
安全日志
需求:记录sql语句的执行。
//⽤户业务
@Component
public class UserService {
public void getUser(){
System.out.println("获取⽤户信息");
}
public void saveUser(){
System.out.println("保存⽤户");
}
public void deleteUser(){
System.out.println("删除⽤户");
}
public void modifyUser(){
System.out.println("修改⽤户");
}
}
// 商品业务类
@Component
public class ProductService {
public void getProduct(){
System.out.println("获取商品信息");
}
public void saveProduct(){
System.out.println("保存商品");
}
public void deleteProduct(){
System.out.println("删除商品");
}
public void modifyProduct(){
System.out.println("修改商品");
}
}
@Component
@Aspect
public class SecurityAspect {
@Pointcut("execution(* com.powernode.spring6.biz..save*(..))")
public void savePointcut(){}
@Pointcut("execution(* com.powernode.spring6.biz..delete*(..))")
public void deletePointcut(){}
@Pointcut("execution(* com.powernode.spring6.biz..modify*(..))")
public void modifyPointcut(){}
@Before("savePointcut() || deletePointcut() || modifyPointcut()")
public void beforeAdivce(JoinPoint joinpoint){
System.out.println("XXX操作员正在执行"+joinpoint.getSignature().getName()+"⽅法");
}
}