目录
概要
和面向对象编程的区别
优点
AOP的底层原理
JDK动态代理技术
AOP七大术语
切点表达式
AOP实现方式
Spring对AOP的实现包括以下3种方式:
在本篇文章中,我们主要讲解前两种方式。
基于AspectJ的AOP注解式开发
定义目标类以及目标方法
定义切面类
目标类和切面类都纳入Spring bean管理
在切面类中添加通知
再通知上添加切点表达式
在Spring配置文件中启用自动代理
通知类型
接下来,编写程序来测试这几个通知的执行顺序
切面的先后顺序
优先使用切点表达式
基于XML配置方式的AOP
编写目标类
编写切面类,并编写通知
编写Spring配置文件
概要
在软件开发的旅程中,我们常常会遇到一些横切关注点(cross - cutting concerns),这些关注点如同贯穿整个应用程序的丝线,涉及多个不同的模块和功能。例如,日志记录、事务管理、权限验证等功能,它们并非属于某个特定的业务模块,却又在许多业务方法中都需要被执行。
在传统的编程模式下,为了实现这些横切关注点,开发者往往不得不将相关的代码片段分散地嵌入到各个业务方法中。以日志记录为例,可能在每个需要记录日志的业务方法开头和结尾都要添加打印日志的代码,这不仅导致代码的大量重复,还使得业务逻辑与这些辅助功能的代码紧密纠缠在一起,严重影响了代码的可读性、可维护性和可扩展性。
Spring AOP 应运而生,它提供了一种优雅而强大的解决方案,能够将这些横切关注点从业务逻辑中分离出来,以一种模块化的方式进行集中管理和维护。通过定义切面(Aspect),Spring AOP 可以在不修改业务逻辑代码的前提下,在特定的连接点(Join Point,如方法调用、异常抛出等)插入相应的通知(Advice,如前置通知、后置通知、环绕通知等),从而实现对横切关注点的统一处理。
和面向对象编程的区别
维度 | 面向对象编程 | 面向切面编程 |
---|---|---|
核心思想 | 以 “对象” 为中心,将现实世界抽象为类、对象及对象间的交互。 | 以 “切面” 为中心,将横切关注点从业务逻辑中分离,实现模块化管理。 |
基本单元 | 类(Class)和对象(Object),通过封装、继承、多态构建程序结构。 | 切面(Aspect),通过定义切点(Pointcut)和通知(Advice)织入横切逻辑。 |
关注点类型 | 聚焦于业务逻辑的纵向模块化(如用户管理、订单处理等独立业务模块)。 | 聚焦于横切关注点的横向抽取(如日志、权限、事务等贯穿多个模块的功能)。 |
优点
- 代码复用性强
- 代码易维护
- 使开发者更专注于业务逻辑
AOP的底层原理
JDK动态代理技术
为接口创建代理类的字节码文件,使用ClassLoader将字节码文件加载到JVM,创建代理类实例对象,执行对象的目标方法。
AOP七大术语
- 连接点Joinpoint:指那些被拦截到的点(位置)。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
- 切点Pointcut:指我们要对哪些Joinpoint进行拦截的定义,在程序执行流程中,真正织入切面的方法(一个切点对应多个连接点)
- 切面Aspect:切点+通知就是一个切面,需要自己编写和配置
- 通知Advice:通知又叫增强,就是具体你要织入的代码
- 织入Weaving:把通知应用到目标对象上的过程(指把增强应用到目标对象来创建新的代理对象的过程)
- 代理对象 Proxy:一个目标对象被织入通知后产生的新对象
- 目标对象 Target:被织入通知的对象
其中的通知(Aspect)包括:
- 前置通知:befer
- 后置通知:after-returning
- 最终通知:after
- 异常通知:throwing
- 环绕通知:around
public class Cat {// Cat类的run方法public void run() {System.out.println("Cat is running");}
}
class Test {public static void main(String[] args) {Cat cat = new Cat();cat.run();}
}
切点表达式
切点表达式用来定义通知(Advice)往哪些方法上切入,语法格式如下:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
- 访问权限控制符:(可选项)没写就是4种权限都可以
- 返回值类型:(必填项)若为“*”,表示返回值类型任意
- 全限定类名:(可选项)两个点“..”表示当前包以及子包下的所有类,若省略,就表示所有的类
- 方法名:(必填项)“*”表示所有方法,set * 表示所有的set方法
- 形式参数列表:(必填项)
- ():表示没有参数的列表
- (..) :参数类型和个数随意的方法
- (*): 只有一个参数的方法
- (*, String): 第一个参数类型随意,第二个参数是String的
- 异常:(可选项)省略是表示任意异常
如:
execution(public * com.powernode.mall.service.*.delete*(..))
表示返回值类型任意,处于com.powernode.mall.service包下的所有类的所有参数任意的deleteXxx方法execution(* com.powernode.mall..*(..))
任意修饰符、返回值类型的,处于com.powernode.mall报下的所有方法execution(* *(..))
表示该项目的所有方法
AOP实现方式
Spring对AOP的实现包括以下3种方式:
- Spring框架结合AspectJ框架实现的AOP,基于注解方式。
- Spring框架结合AspectJ框架实现的AOP,基于XML方式。
- Spring框架自己实现的AOP,基于XML配置方式。
在本篇文章中,我们主要讲解前两种方式。
基于AspectJ的AOP注解式开发
定义目标类以及目标方法
// 目标类
public class OrderService {// 目标方法public void generate(){System.out.println("订单已生成!");}
}
定义切面类
// 切面类
@Aspect
public class MyAspect {
}
注解@Aspect会告诉Spring该类是一个注解类
目标类和切面类都纳入Spring bean管理
在目标类OrderService上添加@Component注解
在切面类MyAspect类上添加**@Component**注解
在切面类中添加通知
// 切面类
@Aspect
@Component
public class MyAspect {// 这就是需要增强的代码(通知)public void advice(){System.out.println("我是一个通知");}
}
再通知上添加切点表达式
// 切面类
@Aspect
@Component
public class MyAspect {// 切点表达式@Before("execution(* com.xxx.spring6.service.OrderService.*(..))")// 这就是需要增强的代码(通知)public void advice(){System.out.println("我是一个通知");}
}
其中,注解@Before表示前置通知(具体的通知类型在上面有讲过,下面也会有方法中的通知注解)
在Spring配置文件中启用自动代理
<!--开启组件扫描--><context:component-scan base-package="com.xxx.spring6.service"/><!--开启自动代理--><aop:aspectj-autoproxy proxy-target-class="true"/>
<aop:aspectj-autoproxy proxy-target-class="true"/> 开启自动代理之后,凡是带有@Aspect注解的bean都会生成代理对象。
proxy-target-class="true" 表示采用cglib动态代理。
proxy-target-class="false" 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类(AOP的底层就是动态代理,关于动态代理的内容可查看代理机制)
通知类型
- 前置通知:@Before 目标方法执行之前的通知
- 后置通知:@AfterReturning 目标方法执行之后的通知
- 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
- 异常通知:@AfterThrowing 发生异常之后执行的通知
- 最终通知:@After 放在finally语句块中的通知
接下来,编写程序来测试这几个通知的执行顺序
// 切面类
@Component
@Aspect
public class MyAspect {@Around("execution(* com.xxx.spring6.service.OrderService.*(..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("环绕通知开始");// 执行目标方法。proceedingJoinPoint.proceed();System.out.println("环绕通知结束");}@Before("execution(* com.xxx.spring6.service.OrderService.*(..))")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("execution(* com.xxx.spring6.service.OrderService.*(..))")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("execution(* com.xxx.spring6.service.OrderService.*(..))")public void afterThrowingAdvice(){System.out.println("异常通知");}@After("execution(* com.xxx.spring6.service.OrderService.*(..))")public void afterAdvice(){System.out.println("最终通知");}}
读者可自行测试,其中异常通知需要产生异常才能触发,当发生异常之后,最终通知也会执行,因为最终通知@After会出现在finally语句块中。出现异常之后,“后置通知”和“环绕通知”的结束部分不会执行
切面的先后顺序
我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高
优先使用切点表达式
- 上面的切点表达式重复写了多次,没有得到复用,同时如果要修改切点表达式,需要修改多处,难维护
- 我们可以将切点表达式单独的定义出来,在需要的位置引入即可
// 切面类
@Component
@Aspect
@Order(2)
public class MyAspect {@Pointcut("execution(* com.xxx.spring6.service.OrderService.*(..))")public void pointcut(){}@Around("pointcut()")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("环绕通知开始");// 执行目标方法。proceedingJoinPoint.proceed();System.out.println("环绕通知结束");}@Before("pointcut()")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("pointcut()")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("pointcut()")public void afterThrowingAdvice(){System.out.println("异常通知");}@After("pointcut()")public void afterAdvice(){System.out.println("最终通知");}}
使用@Pointcut注解来定义独立的切点表达式。
注意这个@Pointcut注解标注的方法随意,只是起到一个能够让@Pointcut注解编写的位置
基于XML配置方式的AOP
编写目标类
// 目标类
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配置文件
<!--纳入spring bean管理--><bean id="vipService" class="com.xxx.spring6.service.VipService"/><bean id="timerAspect" class="com.xxx.spring6.service.TimerAspect"/><!--aop配置--><aop:config><!--切点表达式--><aop:pointcut id="p" expression="execution(* com.xxx.spring6.service.VipService.*(..))"/><!--切面--><aop:aspect ref="timerAspect"><!--切面=通知 + 切点--><aop:around method="time" pointcut-ref="p"/></aop:aspect></aop:config>
</beans>