1 AOP 概念
1 AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。
2 OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。
3 OOP缺陷:
OOP允许开发者定义纵向的关系,但并不适合定义横向的关系, 例如日志功能。
日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,
如安全性、异常处理和透明的持续性也都是如此
这种散布在各处的无关的代码被称为横切(cross cutting)
在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
4 AOP的作用:
AOP技利用一种称为"横切"的技术,剖解开封装的对象内部,
将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。
所谓"切面",就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来
便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用AOP,可以在不修改原来代码的基础上添加新功能。
1 AOP:Aspect Oriented Programming
2 AOP是一种设计思想,面向切面编程的思想,是面向对象编程的补充和完善
3 AOP面向切面编程通过 预编译方式 和 运行期动态代理的方式
实现了在不修改源代码的情况下给程序动态的、统一的添加额外功能。
4 OOP是纵向继承机制,能抽取的一定是连续的代码
AOP是横向抽取机制,专门解决非连续性代码的抽取问题
5 AOP的底层就是动态代理
1.2 AOP相关术语
横切关注点
从每个方法中抽取出来的同一类 非核心业务
每个项目中可以使用 多个横切关注点 对相关方法进行多个不同方面的增强
例如:计算器程序中的日志功能就是 横切关注点
通知
封装横切关注点的类就是切面
注意:
1 切面中的每一个横切关注点都是一个 通知
2 附加功能代码,在目标对象中叫横切关注点,抽取出来之后,封装到了切面中,在切面中叫通知方法
1 前置通知
目标方法执行前的附加代码
2 返回通知
目标方法执行完毕后写的附加代码
3 异常通知
目标代码出现异常的时候执行的附加代码
4 后置通知
finally代码块中,即无论异常发生与否都要执行的附加代码
5 环绕通知
相当于前面的四种
使用try catch 环绕整个目标方法,包含前四种通知的所有位置
切面
作用:切面就是用来封装横切关注点的
封装横切关注点的类就是切面
封装通知方法的类叫切面
横切关注点是相对于目标方法的叫法
通知方法是相对于切面的叫法,通知方法就是横切关注点被抽取到了切面中的叫法
目标对象
代理对象
连接点
抽取横切关注点的位置
例如:
连接点1:计算器中,目标对象方法执行前位置抽取的
连接点2:计算器中,目标对象方法执行后位置抽取的
连接点3:计算器中,目标对象方法执行出现异常位置抽取的
连接点4:计算器中,目标对象方法执行finally代码块位置中抽取的
切入点
定位连接点的方式
总结
1 抽横切关注点封装到切面中,横切关注点就成了通知,通过切入点定位到连接点
2 然后就可以在不改变目标对象代码的前提下,把切面中的通知
通过切入点表达式套到连接点上,实现目标代码的功能增强
1 简化代码
抽取方法中固定位置的重复代码,让被抽取的方法更加专注于自己的功能,提高内聚性
2 代码增强
把特定的功能封装到切面中,看哪里有需要就哪里套,被套用了切面中的逻辑的方法就得到了功能增强
2 AOP的应用场景
AOP 是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,
使得代码更加清晰、简洁、易于维护。
AOP 可以应用于各种场景,以下是一些常见的AOP应用场景:
1.
在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,
可以在方法执行前、执行后或异常抛出时记录日志。
2.
在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,
可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。
3.
在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,
可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,
如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
4.
在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。
可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,
在方法执行完毕后计算方法执行时间并输出到日志中。
5.
系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,
可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,
则进行异常处理(如记录日志、发送邮件等)。
6.
可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
7.
AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。
综上所述,AOP可以应用于各种场景,它的作用
是将通用的横切关注点与业务逻辑分离,使得代码更加清晰、简洁、易于维护。
3 AOP的实现
3.1 基于注解的AOP
- AspectJ是AOP思想的一种具体实现方式
- 有接口使用JDK动态代理或者是cglib动态代理,没接口只能使用cglib动态代理
JDK动态代理:
JDK 的API中,java.lang.reflect包中的Proxy类提供了创建动态代理的方法
JDK 动态代理使用前提是:被代理的目标类必须实现接r
原因是因为JDK动态代理要求目标对象和代理对象实现相同的接口。
JDK 动态代理生成的代理类在com.sun.proxy包下,类名为$proxy+数字
cglib动态代理:
目标类通过被继承而实现的被代理,所以也不需要目标类实现接口,顾没有前提条件
Spring配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
...
xsi:schemaLocation="http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.zhiyou100.aop.annotation"></context:component-scan>
<aop:aspectj-autoproxy />
</beans>
切面类
@Component
@Aspect
public class LoggerAspect {
@Before("execution(public int com.zhiyou100.aop.annotation.Target.add(int,int))")
public void adviced01(){
System.out.println("前置通知:=======>");
}
}
测试切面
前置通知:=======>
目标代码:(lnum + rnum)=101
简化切入点
@Before("execution(public int com.zhiyou100.aop.annotation.Target.add(int,int))")
public void adviced01(){
System.out.println("前置通知:=======>");
}
@Before("execution(* com.zhiyou100.aop.annotation.Target.*(..))")
public void adviced01(){
System.out.println("前置通知:=======>");
}
前置通知:=======>
目标代码:(lnum + rnum)=101
前置通知:=======>
目标代码:(lnum - rnum)=-99
前置通知:=======>
目标代码:(lnum * rnum)=100
前置通知:=======>
目标代码:(lnum / rnum)=0
测试连接点
前置通知,操作名add,参数[1, 100]=======>
目标代码:(lnum + rnum)=101
前置通知,操作名sub,参数[1, 100]=======>
目标代码:(lnum - rnum)=-99
前置通知,操作名mul,参数[1, 100]=======>
目标代码:(lnum * rnum)=100
前置通知,操作名div,参数[1, 100]=======>
目标代码:(lnum / rnum)=0
@Before("execution(* com.zhiyou100.aop.annotation.Target.*(..))")
public void adviced01(JoinPoint joinPoint){
String methodName=joinPoint.getSignature().getName();
System.out.println("前置通知,操作名"+methodName+",参数"+
Arrays.toString(joinPoint.getArgs())+"=======>");
}
测试切入点表达式的复用
前置通知,操作名add,参数[1, 100]=======>
目标代码:(lnum + rnum)=101
后置通知,操作名add,参数[1, 100]=======>
前置通知,操作名sub,参数[1, 100]=======>
目标代码:(lnum - rnum)=-99
后置通知,操作名sub,参数[1, 100]=======>
前置通知,操作名mul,参数[1, 100]=======>
目标代码:(lnum * rnum)=100
后置通知,操作名mul,参数[1, 100]=======>
前置通知,操作名div,参数[1, 100]=======>
目标代码:(lnum / rnum)=0
后置通知,操作名div,参数[1, 100]=======>
@Pointcut("execution(* *.*(..)))")
public void pointCut() {}
@After("pointCut()")
public void advice02(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println(" 后置通知,操作名" + methodName +
",参数" + Arrays.toString(joinPoint.getArgs()) + "=======>\n");
}
@Before("pointCut()")
public void adviced01(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println(" 前置通知,操作名" + methodName +
",参数" + Arrays.toString(joinPoint.getArgs()) + "=======>");
}
4 五种通知
Before("切入点")
执行时机:目标方法开始前
AfterReturning(value="切入点",returning="result")
执行时机:目标方法正常结束后
AfterThrowing(value="切入点",throwing="result")
执行时机:目标方法出现异常后
After("切入点")
执行时机:目标方法最终结束后,finally 代码块中执行
使用 try...catch...finally 结构围绕整个被代理的目标方法
包括上面四种通知对应的所有位置
注意:环绕通知返回值必须和目标操作返回值一致
理由:
环绕通知更像动态代理,动态代理中,代理对象返回的一定要和目标对象一致
因为代理对象只是对目标进行增强,代理对象不能改变目标原有的特性
4.1 测试返回通知
@Aspect
@Component
public class LoggerAspect02 {
@Pointcut("execution(* *.*(..)))")
public void pointCut01(){}
@AfterReturning(value="pointCut01()",returning = "res")
public void returnAdvice(JoinPoint joinPoint,Object res){
String mname=joinPoint.getSignature().getName();
System.out.println("=======>切入点:全部,计算结果:"+res+
",当前方法:"+mname+",参数:"+
Arrays.toString(joinPoint.getArgs()));
}
}
目标代码:(lnum + rnum)=101
=======>切入点:全部,计算结果:101,当前方法:add,参数:[1, 100]
目标代码:(lnum - rnum)=-99
=======>切入点:全部,计算结果:-99,当前方法:sub,参数:[1, 100]
目标代码:(lnum * rnum)=100
=======>切入点:全部,计算结果:100,当前方法:mul,参数:[1, 100]
目标代码:(lnum / rnum)=0
=======>切入点:全部,计算结果:0,当前方法:div,参数:[1, 100]
4.2 测试异常通知
- 时机:目标方法出现异常时执行,catch代码块中执行
public void testAdvice01(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("aopdemo.xml");
Calculator calculator = (Calculator)ioc.getBean(Calculator.class);
calculator.add(1,100);
calculator.sub(1,100);
calculator.mul(1,100);
calculator.div(10,0);
}
@Aspect
@Component
public class LoggerAspect03 {
@Pointcut("execution(* *.div(..))")
public void pointCut02(){}
@AfterThrowing(value="pointCut02()",throwing = "ex")
public void advice01(JoinPoint joinPoint,Exception ex){
String mname = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("*****异常日志:"+mname+",参数:"+args+",异常原因:"+ex);
}
}
目标代码:(lnum + rnum)=101
目标代码:(lnum - rnum)=-99
目标代码:(lnum * rnum)=100
*****异常日志:div,参数:[10, 0],异常原因:java.lang.ArithmeticException: / by zero
4.3 测试环绕通知
- 使用 try catch finally 围绕整个被代理的目标方法
@Aspect
@Component
public class LoggerAspect04 {
@Pointcut("execution(* *.*(..))")
public void pointCut01(){}
@Around(value="pointCut01()")
public Object roundAdvice(ProceedingJoinPoint joinPoint){
Object res=0;
try {
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("------>环绕---前置:"+name+"::::"+args);
res = joinPoint.proceed();
System.out.println("------>环绕---返回:"+res);
} catch (Throwable e) {
System.out.println("------>环绕---异常:"+e.getMessage());
throw new RuntimeException(e);
} finally {
System.out.println("------>环绕---后置执行完毕\n");
}
return res;
}
}
------>环绕---前置:add::::[1, 100]
目标方法:(lnum + rnum)=101
------>环绕---返回:101
------>环绕---后置执行完毕
------>环绕---前置:sub::::[1, 100]
目标方法:(lnum - rnum)=-99
------>环绕---返回:-99
------>环绕---后置执行完毕
------>环绕---前置:mul::::[1, 100]
目标方法:(lnum * rnum)=100
------>环绕---返回:100
------>环绕---后置执行完毕
------>环绕---前置:div::::[10, 0]
------>环绕---异常:/ by zero
------>环绕---后置执行完毕
4.4 四大非环绕
@Before("pointCut01()")
public void advice02(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("前置---------->通知:方法名:"+name+",参数:"+args);
}
@AfterReturning(value = "pointCut01()",returning = "res")
public void advice03(JoinPoint joinPoint,Object res){
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("返回---------->通知:结果:"+res+"\n");
}
@AfterThrowing(value = "pointCut01()",throwing = "ex")
public void advice04(JoinPoint joinPoint,Exception ex){
System.out.println("异常---------->通知:异常:"+ex);
}
@After(value = "pointCut01()")
public void advice05(JoinPoint joinPoint){
System.out.println("后置---------->通知执行完毕");
}
5 名词解释
从每个方法中抽取出来的同一类 非核心业务
在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点
AOP把软件系统分为两个部分:
核心关注点 和 横切关注点
业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。
横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,
比如权限认证、日志、事务、异常等。
AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
前置通知:目标方法前执行
返回通知:目标方法成功结束后执行
异常通知:目标方法异常结束后执行
后置通知:目标方法最终结束后(可能正常结束,可能异常结束,定义在finally中)
环绕通知:使用try catch包围整个目标方法,包含以上四种通知的四个位置
纯逻辑概念,非语法定义
指被拦截的那些点
定义了哪些连接点会被拦截并应用通知
是一个表达式
例如:
execution(\* com.spring.service.impl.*.*(..))
符合条件的每个方法都是一个具体的连接点。
切面=切入点+通知
切面是一个封装了切入点和通知的类
被代理的目标对象
向目标对象应用通知之后创建的代理对象
指把通知应用到目标上,生成代理对象的过程
可以在编译期织入,也可以在运行期织入,Spring采用运行期进行织入
切入点 PointCut
-- 连接点是程序执行过程中的某个特定点,例如方法调用、异常抛出等
-- 切点定义了哪些连接点会被拦截并应用通知
-- 简单来说,切点就是用来定义切面的位置,即捕获哪些连接点的调用然后执行通知的操作
6 Spring AOP框架介绍和关系梳理
1.
2.
3.
基于AOP编程思维,封装动态代理技术
简化了动态代理技术实现的框架!
SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可
即可完成面向切面思维编程的实现!
7 动态代理
-
动态代理(InvocationHandler):
-
JDK原生的实现方式,需要被代理的目标类必须实现接口
-
要求代理对象和目标对象实现同样的接口
-
cglib:
-
通过继承被代理的目标类实现代理,所以不需要目标类实现接口
-
AspectJ:
-
早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解
8 通知的执行顺序
9 通知的优先级
- 切面加一个@Order注解即可实现切面的优先级
- 默认214748…. ,为int 的最大值
- value值越小切面优先级越高
@Aspect
@Component
@Order(2)
public class LoggerAspect04 {
@Pointcut("execution(* *.*(..))")
public void pointCut01(){}
@Around(value="pointCut01()")
public Object roundAdvice(ProceedingJoinPoint joinPoint){
Object res=0;
try {
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("------>环绕---前置:"+name+"::::"+args);
res = joinPoint.proceed();
System.out.println("------>环绕---返回:"+res);
} catch (Throwable e) {
System.out.println("------>环绕---异常:"+e.getMessage());
throw new RuntimeException(e);
} finally {
System.out.println("------>环绕---后置执行完毕\n");
}
return res;
}
}
@Aspect
@Component
@Order(1)
public class ValidateAspect01 {
@Pointcut("execution(* *.div(..))")
public void pointCut(){}
@Before("pointCut()")
public void checkDivisorIsZeroAdvice(JoinPoint joinPoint){
Object[] arr = joinPoint.getArgs();
if(arr[1].equals(0)){
System.out.println("校验通知:!!!!!!!!除数为0");
}else{
System.out.println("校验通知:==========>除数不为0");
}
}
}
校验通知:!!!!!!!!除数为0
------>环绕---前置:div::::[10, 0]
------>环绕---异常:/ by zero
------>环绕---后置执行完毕