AOP Aspect-OrientedProgramming 面向切面编程
可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程
主要目的:
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
本质是在一系列纵向的控制流程中,把那些相同的子流程提取成一个横向的面
术语
Aspect(切面)
aspect 由 pointcount 和 advice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中.
AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:
- 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
- 如何在 advice 中编写切面代码.
可以简单地认为, 使用 @Aspect 注解的类就是切面.
advice(通知/增强)
由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码.
许多 AOP框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截.
例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了.
连接点(join point)
程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.
在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.
切点(point cut)
匹配 join point 的谓词(a predicate that matches join points).
Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行.
在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.
关于join point 和 point cut 的区别
在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join point 和 point cut 本质上就是两个不同纬度上的东西.
advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice
织入(Weaving)
将 aspect 和其他对象连接起来, 并创建 adviced object 的过程.
把切面应用到目标对象来创建新的代理对象的过程。
根据不同的实现技术, AOP织入有三种方式:
- 编译器织入, 这要求有特殊的Java编译器.
- 类装载期织入, 这需要有特殊的类装载器.
- 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.
引入(introduction)
允许我们向现有的类添加新方法属性。就是把切面(也就是新方法属性:通知定义的)用到目标类中吗。
spring实现AOP两种方式
- 基于XML Schema的配置文件定义
- 通过@Aspect系列标注定义
AOP步骤
- 定义切面类-aspect
- 定义连接点-pointcut
- 定义通知:五种通知
- 环绕通知:必须添加ProceedingJoinPoint
在目标方法执行之前,执行之后都要执行(环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行) - 前置通知Before advice:
在目标方法执行之前执行 - 后置通知After returning advice:
在目标方法执行之后执行 - 异常通知After throwing advice:
目标方法执行后抛出异常才执行 - 最终通知After (finally) advice:
在目标方法执行之后 都会执行的通知
- 环绕通知:必须添加ProceedingJoinPoint
- 配置切面
通知选取规则
- 5大通知类型中,环绕通知功能最为强大,因为环绕通知,可以控制目标方法是否执行。
- 如果需要记录异常信息,使用异常通知。
- 其他通知,只能做记录工作,不能做处理
切入点表达式
- within表达式 粗粒度的只能控制类,用于匹配指定类型内的方法执行
- Execution(返回值类型 包名.类名.方法名(参数列表)),用于匹配方法执行的连接点
- this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
- target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
- args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;
示例
Add(int a)
- 例子1:
<aop:pointcut expression=
"execution(int service.UserServiceImpl.add())" id="txPointcut"/>
改切点表达式表示
返回值为int 包名类名serviceUserServiceImpl 方法为add()的匹配规则
- 例子2:
<aop:pointcut expression="execution(* service.*.add())" id="txPointcut"/>
规则:返回值值任意, 包名service下子类的add(),只能包含一层,子孙类不行。
- 例子3:
<aop:pointcut
expression="execution(* service..*.add())" id="txPointcut"/>
规则:方法返回值任意, service包下的所有子孙类的add()
- 例子4:
<aop:pointcut expression=
"execution(* service..*.add(int,String))" id="txPointcut"/>
规则:返回值的类型任意 service子孙包下的add方法参数类型为int,String
5. 例子5
<aop:pointcut expression=
"execution(* service..*.add(..))"
id="txPointcut"/>
规则:返回值类型任意 service下的所有子孙类.add方法() (参数任意)
6. 例子6:
要求返回值为任意的,service包下的全部类的全部方法的任意参数
Execution(* service..*.*(..))
简化:
execution(* service..*(..))
为什么会有AOP
- 首先原始代码加一个事务控制,需要在每个方法中添加事务控制的代码,这样会有大量重复代码
- 使用静态代理的话(实现同一个接口,内部维护一个代理对象)方法多时,代码成本增加
- 动态代理(JDK动态生成了一个类去实现接口,隐藏了这个过程)前提目标类必须有实现的接口
- cglib(动态生成的子类继承目标的方式实现,在运行期动态的在内存中构建一个子类)前提cglib目标类不能为final
- AOP以此灵活选择JDK和cglib
源码
AOP的调用原理
- 当spring容器解析到AOP标签时,开启启动AOP的相关配置
- 当解析到切入点表达式时,该表达式会进入spring内存中保留
- 当解析到切面类时,首先会为切面创建对象。并且根据切入点表达式,和通知的匹配关系进行绑定。
- 如果从容器中获取对象时,如果该对象与切入点表达式中的规则匹配。
则会为其创建代理对象,如果该类实现了接口,则会为其创建JDK的代理,如果该类没有实现接口,则会采用cglib进行代理。代理对象创建完成后,交给用户使用。 - 当代理对象执行方法时,则会执行与切入点表达式绑定的通知方法。
使用注解配置切入点
- 将切入点写入通知内部
@Before(value=“execution(* service….(…))”)
缺点:如果该切入点需要重复使用,则必须重复写多次 - 自定义方法编辑切入点
@Pointcut(value=“execution(* service….(…))”)
public void pointcut(){}
优点:- 可以实现切入点表达式的复用
- 方便表达式管理
spring AOP用处
- 声明式事务管理
- Controller层的参数校验
- 实现数据库读写分离
- 执行方法前判断是否具有权限
- 对部分函数的调用进行日志记录。监控部分重要函数,若抛出指定的异常,可以以短信或邮件方式通知相关人员
- 信息过滤,页面转发
- 检测执行性能
示例
如初Action对象在执行actionSomeThing时会输出什么
package com.example.demo;
public class AOPTest {
public void actionSomeThing(){
System.out.println(1);
}
}
@Aspect
class AspectJTest{
@Pointcut("execution(* com.example.demo.AOPTest*.*(..))")
public void performance(){
}
@Before("performance()")
public void before(){System.out.println(2);}
@AfterReturning("performance()")
public void afterReturn(){System.out.println(3);}
@After("performance()")
public void after(){System.out.println(4);}
@Around("performance()")
public Object around(ProceedingJoinPoint joinPoint){
Object obj = null;
try{
System.out.println(5);
joinPoint.proceed();
System.out.println(6);
}catch (Throwable e){
e.printStackTrace();
}
return obj;
}
}
521643
@Pointcut("execution(* com.example.demo.AOPTest*.*(..))")
定了一个切入点,正则表达式的意思是在执行com.bwf.web.Action类的所有方法时,将会发送performance切入点通知。
@Before是在所拦截方法执行之前执行一段逻辑。
@After 是在所拦截方法执行之后执行一段逻辑。
@Around是可以同时在所拦截方法的前后执行一段逻辑。
@AfterReturning在所拦截方法return后执行该注解的函数
本例中,
- 首先要执行的是@Around注解的函数,所以,先打印5。
- obj = joinpoint.proceed();调用的就是actionSomeThing函数本身。那么在执行actionSomeThing函数前,首先要执行@Before注解函数,因此,再打印2
- 之后是performance函数执行,打印1
- 接下来执行around的最后一步输出,打印6
- 然后拦截函数执行完毕,调用@After注解函数,打印4
- 最后拦截函数return后,执行@AfterReturn注解函数,打印3
执行顺序
[Aspect1] around advise 1
[Aspect1] before advise
method
[Aspect1] around advise2
[Aspect1] after advise
[Aspect1] afterReturning advise
注解配置切面参数说明
- 除了@Around外,每个方法里都可以加或者不加参数JoinPoint,如果有用JoinPoint的地方就加,不加也可以,JoinPoint里包含了类名、被切面的方法名,参数等属性,可供读取使用。
- @Around参数必须为ProceedingJoinPoint,pjp.proceed相应于执行被切面的方法。
- @AfterReturning方法里,可以加returning = “XXX”,XXX即为在controller里方法的返回值,本例中的返回值是“first controller”。
- @AfterThrowing方法里,可以加throwing = “XXX”,供读取异常信息
异常通知示例
/**
* 能够接受异常信息,获取当前方法
* 如果添加JoinPoint,必须位于第一位
*/
@AfterThrowing(value = "performance()",throwing = "throwable")
public void throwss(JoinPoint joinPoint,Throwable throwable){
System.out.println("获取异常信息"+throwable.getMessage());
System.out.println("获取异常类型"+throwable.getClass());
System.out.println("当前执行的方法名为"+joinPoint.getSignature().getName());
System.out.println("方法异常时执行.....");
}
执行
@RequestMapping("/aoptest")
public String tets() {
aopTest.actionSomThing();//生效
return "AAA";
}
输出
获取异常信息/ by zero
获取异常类型class java.lang.ArithmeticException
当前执行的方法名为asdada
方法异常时执行.....
异常情况的执行顺序
[Aspect1] around advise 1
[Aspect1] before advise
throw an exception
[Aspect1] after advise
[Aspect1] afterThrowing advise
遇到的问题service切入点不生效
package com.example.demo.AOP;
public interface AOPTest {
public void actionSomThing();
}
@Service
public class AOPTestImpl implements AOPTest {
public void actionSomThing(){
System.out.println(1);
}
}
@Component
@Aspect
public class AspectJTest{
@Pointcut("execution(public * com.example.demo.AOP.*.*(..))")
public void performance(){}
@Before("performance()")
public void before(){System.out.println(2);}
@AfterReturning("performance()")
public void afterReturn(){System.out.println(3);}
@After("performance()")
public void after(){System.out.println(4);}
@Around("performance()")
public Object around(ProceedingJoinPoint joinPoint){
Object obj = null;
try{
System.out.println(5);
obj = joinPoint.proceed();
System.out.println(6);
}catch (Throwable e){
e.printStackTrace();
}
return obj;
}
}
@Autowired
AOPTest aopTest;
@RequestMapping("/")
public String test() {
new AOPTestImpl().actionSomThing();//不生效
return "BBB";
}
@RequestMapping("/aoptest")
public String tets() {
aopTest.actionSomThing();//生效
return "AAA";
}
当时琢磨了好久,后来经人点拨,AOP是是靠动态代理实现的,直接new的话相当于自力更生调用了原来的方法,而使用spring生成的对象就经过了动态代理这一步骤。
多个环绕通知的执行规则
如果有多个环绕通知,则或先执行下一个通知,如果没有下一个通知,则会执行目标方法
其结构是一种嵌套关系
示例:还是原来那个叭,改一下execution
package com.example.demo.AOP;
interface AOPTest {
public void actionSomThing();
}
@Service
public class AOPTestImpl implements AOPTest {
public void actionSomThing(){
System.out.println(1);
}
}
@Component
@Aspect
public class AspectJTest{
@Pointcut("execution(public * com.example.demo.AOP.*.*(..))")
public void performance(){}
@Before("performance()")
public void before(){System.out.println(2);}
@AfterReturning("performance()")
public void afterReturn(){System.out.println(3);}
@After("performance()")
public void after(){System.out.println(4);}
@Around("performance()")
public Object around(ProceedingJoinPoint joinPoint){
Object obj = null;
try{
System.out.println(5);
obj = joinPoint.proceed();
System.out.println(6);
}catch (Throwable e){
e.printStackTrace();
}
return obj;
}
}
@Controller
public class main {
@RequestMapping("/aoptest2")
public String tets(){//这个方法也会被配置的AOP切到
aopTest.actionSomThing();
return "BBB";
}
}
- 首先执行@Around注解的函数,进入第一个AOP事件,所以先打印5
- 执行方法内容前,执行@Before注解函数,打印2
- obj = joinpoint.proceed(),然后发现方法内容也是定义的切面,再次进入@Around注解的函数,进入第二个AOP事件的环绕通知,执行5
- joinpoint.proceed();,执行actionSomething函数前执行@Before注解函数,打印2
- actionSomething函数打印1
- 然后第二个环绕通知最后一步打印6
- 然后拦截函数执行完毕,调用@After注解函数,打印4
- 然后拦截函数return后,执行@AfterReturn注解函数,打印3
- actionSomeThing方法执行完毕,第二个AOP事件结束,在第一个环绕通知最后一步打印6
- 然后拦截函数执行完毕,调用@After注解函数,打印4
- 最后拦截函数return后,执行@AfterReturn注解函数,打印3
5
2
5
2
1
6
4
3
6
4
3
但是这种情况是同一个方法多层被切面,AOP切面是同一个
还有种情况是多个AOP切面
把上个例子的数字打印换为具体的通知类型看着清楚些
@Component
@Aspect
public class AspectJTest{
@Pointcut("execution(public * com.example.demo.AOP.*.actionSomThing(..))")
public void performance(){}
@Before("performance()")
public void before(){System.out.println("AspectJTest1-Before");}
@AfterReturning("performance()")
public void afterReturn(){System.out.println("AspectJTest1-AfterReturning");}
@After("performance()")
public void after(){System.out.println("AspectJTest1-After");}
@Around("performance()")
public Object around(ProceedingJoinPoint joinPoint){
Object obj = null;
try{
System.out.println("AspectJTest1-AroundFirst");
obj = joinPoint.proceed();
System.out.println("AspectJTest1-AroundSecond");
}catch (Throwable e){
e.printStackTrace();
}
return obj;
}
}
@Component
@Aspect
public class AspectJTest2 {
@Pointcut("execution(public * com.example.demo.AOP.*.actionSomThing(..))")
public void performance(){}
@Before("performance()")
public void before(){System.out.println("AspectJTest2-Before");}
@AfterReturning("performance()")
public void afterReturn(){System.out.println("AspectJTest2-AfterReturning");}
@After("performance()")
public void after(){System.out.println("AspectJTest2-After");}
@Around("performance()")
public Object around(ProceedingJoinPoint joinPoint){
Object obj = null;
try{
System.out.println("AspectJTest2-AroundFirst");
obj = joinPoint.proceed();
System.out.println("AspectJTest2-AroundSecond");
}catch (Throwable e){
e.printStackTrace();
}
return obj;
}
}
输出
AspectJTest1-AroundFirst
AspectJTest1-Before
AspectJTest2-AroundFirst
AspectJTest2-Before
1
AspectJTest2-AroundSecond
AspectJTest2-After
AspectJTest2-AfterReturning
AspectJTest1-AroundSecond
AspectJTest1-After
AspectJTest1-AfterReturning
但是这种情况项目重启之后也有可能是这种情况
AspectJTest2-AroundFirst
AspectJTest2-Before
AspectJTest1-AroundFirst
AspectJTest1-Before
1
AspectJTest1-AroundSecond
AspectJTest1-After
AspectJTest1-AfterReturning
AspectJTest2-AroundSecond
AspectJTest2-After
AspectJTest2-AfterReturning
也就是说aspect1和aspect2的执行顺序是未知的
那么怎么制定aspect的执行顺序呢
指定aspect的执行顺序
- 实现org.springframework.core.Ordered接口,实现它的getOrder()方法
- getaspect添加@Order注解,该注解全称为org.springframework.core.annotation.Order
值越小越先执行
@Component
@Aspect
@Order(5)
public class AspectJTest{
//
}
@Component
@Aspect
@Order(1)
public class AspectJTest2 {
//
}
AspectJTest2-AroundFirst
AspectJTest2-Before
AspectJTest1-AroundFirst
AspectJTest1-Before
1
AspectJTest1-AroundSecond
AspectJTest1-After
AspectJTest1-AfterReturning
AspectJTest2-AroundSecond
AspectJTest2-After
AspectJTest2-AfterReturning
注意点
- 如果在同一个aspect类中,针对同一个pointcut,定义了两个相同的advice(比如两个@Before),那么这两个advice的执行顺序是无法确定的,哪怕给两个advice添加了@Order注解也不行
- 对于@Around环绕通知,不管它有没有返回值,但是必须要方法内部,调用一下 pjp.proceed();否则,Controller 中的接口将没有机会被执行,从而也导致了 @Before这个advice不会被触发。比如,我们假设正常情况下,执行顺序为”aspect2 -> apsect1 -> controller”
比如我将AspectTest改为这样
@Component
@Aspect
public class AspectJTest{
@Pointcut("execution(public * com.example.demo.AOP.*.actionSomThing(..))")
public void performance(){}
@Before("performance()")
public void before(){System.out.println("AspectJTest1-Before");}
@AfterReturning("performance()")
public void afterReturn(){System.out.println("AspectJTest1-AfterReturning");}
@After("performance()")
public void after(){System.out.println("AspectJTest1-After");}
@Around("performance()")
public Object around(ProceedingJoinPoint joinPoint){
Object obj = null;
try{
System.out.println("AspectJTest1-AroundFirst");
// obj = joinPoint.proceed();
System.out.println("AspectJTest1-AroundSecond");
}catch (Throwable e){
e.printStackTrace();
}
return obj;
}
}
输出
AspectJTest1-AroundFirst
AspectJTest1-AroundSecond
AspectJTest1-After
AspectJTest1-AfterReturning
可以看到@Before注解的函数没有执行,原方法也未执行
spring aop就是一个同心圆,要执行的方法为圆心,最外层的order最小。从最外层按照AOP1、AOP2的顺序依次执行doAround方法,doBefore方法。然后执行method方法,最后按照AOP2、AOP1的顺序依次执行doAfter、doAfterReturn方法。也就是说对多个AOP来说,先before的,一定后after。
那不同的切面,顺序怎么决定呢,尤其是同格式的切面处理,譬如两个execution的情况,那spring就是随机决定哪个在外哪个在内了。
所以大部分情况下,我们需要指定顺序,最简单的方式就是在Aspect切面类上加上@Order(1)注解即可,order越小最先执行,也就是位于最外层。像一些全局处理的就可以把order设小一点,具体到某个细节的就设大一点。
参考:
Spring AOP是什么?你都拿它做什么?
Spring AOP 切入点表达式
Spring boot中使用aop详解
Spring AOP @Before @Around @After 等 advice 的执行顺序
彻底征服 Spring AOP 之 理论篇