AOP概述:
AOP是一种设计思想,是软件设计领域中的面向切面编程。它是面向对象编程的一种补充和完善,它以通过预编译方式和运行其动态代理方式实现—>在不修改源代码的情况下给程序动态统一添加额外功能的一种技术
横切关注点:
从每个方法中抽取出来的同一类非核心业务
[比如:日志功能],在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点
横切关注点是对于目标对象来说,抽取出来的非核心业务代码,将横切关注点封装到切面中,而在这个切面中,每个横切关注点都被表示为一个通知方法
通知:
每一个横切关注点上要做的事情都需要写一个方法来实现
,这样的方法就叫通知方法,也就是说横切关注点是实现额外功能的,该功能是通过通知方法实现的
前置通知:在被代理的目标方法前执行 返回通知:在被代理的目标方法成功结束后执行 异常通知:在被代理的目标方法异常结束后执行 后置通知:在被代理的目标方法最终结束后执行 环绕通知:使用try-catch-finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
切面:封装通知方法的类
目标:被代理的目标对象
代理:向目标对象应用通知后创建的代理对象
那么AOP到底应该怎么做呢?
要想实现AOP,那么目标对象必须在此之前就已经存在的,我们通过分析之后,产生了目标对象,才需要通过AOP对该目标对象进行功能增强的,代理对象也是不需要我们去创建
那么AOP是做什么呢?
即为从目标对象中将非核心业务代码抽取出来,所抽取出的非核心业务代码叫做横切关注点,而将其代码放入的类叫做切面,切面中去封装横切关注点,每一个横切关注点都是一个方法,而这个方法被叫做通知,但非核心代码不仅仅是被抽取出来就OK了,我们将其抽取出来之后,是需要在目标对象中实现该功能以达到功能增强的目的,我们不仅需要将其抽取出来,还需要将其套到当前的目标对象上,代码是从哪里抽取的,就需要套哪里
连接点:
并不是语法定义的,而是一个纯逻辑概念,把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴与y轴的交叉点就是连接点
切入点:定位连接点的方式
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)
如果把连接点看作数据库中的记录,那么切入点就是查询记录的SQL语句
Spring的AOP技术可以通过切入点定位到特定的连接点
切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件
AOP的作用:
简化代码:把方法中固定位置的重复的代码抽取出来
,让被抽取的方法更专注于自身的核心功能,提高内聚性
代码增强:把特定的功能封装到切面类中
,哪里有需要,就往哪里套,被套用了切面逻辑的方法就被切面给增强了
基于注解的AOP:
上面我们说到AOP是一种思想,而该思想是通过Aspect注解层实现的
动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口,因为这个技术要求代理对象和目标对象实现相同的接口
cglib:通过继承被代理的目标类实现代理,不需要目标类实现接口
Aspectj:本质上是静态代理,将代理逻辑"织入"被代理的目标类编译得到的字节码文件,所以最终效果是动态的,weaver就是织入器,Spring只是借用了Aspectj中的注解
实现基于注解的AOP:
准备工作:
新建模块或者项目:
添加依赖:是在IOC所需依赖基础上再加入下面的依赖:也就是说aop的实现也是需要以IOC作为基础的
<!-- spring-aspects会帮我们传递过来aspectjweaver--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency>
加入上述依赖后,点击左边的刷新按钮即可!
打开spring_aop的dependency:
创建接口:
package spring_aop_annotation; public interface Calculator { int add(int i,int j); int sub(int i,int j); int mul(int i,int j); int div(int i,int j); }
创建其实现类:
package spring_aop_annotation; @Component public class CalculatorImpl implements Calculator{ @Override public int add(int i, int j) { int result=i+j; System.out.println("方法内部:result,"+result); return result; } @Override public int sub(int i, int j) { int result=i-j; System.out.println("方法内部:result,"+result); return result; } @Override public int mul(int i, int j) { int result=i*j; System.out.println("方法内部:result,"+result); return result; } @Override public int div(int i, int j) { int result=i/j; System.out.println("方法内部:result,"+result); return result; } }
创建切面类并配置:
package spring_aop_annotation; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Component @Aspect //将当前组件标识为切面 public class LoggerAspects {//切面类 }
创建spring-aop-annotation.xml文件:
<?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/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 切面类和目标类都需要交给IOC容器管理 切面类必须通过@Aspects注解标识为一个切面 --> <context:component-scan base-package="spring-aop_annotation"></context:component-scan> <!-- 启用 AspectJ 自动代理--> <aop:aspectj-autoproxy/> </beans>
基于注解的AOP之前置通知:在目标对象方法执行之前执行
修改LoggerAspects中的代码如下所示:
package spring_aop_annotation; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Component @Aspect //切面类--->需要通过指定的注解将方法标识为通知方法 public class LoggerAspects { //需要在切入点设置切入点表达式,Before用来标识前置通知的注解,该通知在目标方法执行之前执行 @Before("execution(public int spring_aop_annotation.CalculatorImpl.add(int,int))")//假设将前置通知作用于add方法上 public void beforeAdviceMethod(){ System.out.println("LoggerAspects,前置通知"); } }
通过测试类测试:
import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import spring_aop_annotation.CalculatorImpl; public class AopTest { @Test public void testAopAnnotation(){ ApplicationContext ioc=new ClassPathXmlApplicationContext("spring-aop-annotation.xml"); CalculatorImpl calculator=ioc.getBean(CalculatorImpl.class); calculator.add(4,10); } }
输出如下所示:
报错的信息为没有找到该bean对象,但我们已经对该类加了注解并且也在XML文件中配置了扫描该类所在的包,那么为什么 IOC容器无法获取该对象呢?原因是:只要我们为目标对象创建了代理类,那么我们无法通过该目标对象直接去访问,而是要通过代理对象去访问,那么代理类是什么呢?我们好像似乎不知道,虽然我们不知道其代理类,但是我们知道它所实现的接口啊,早在之前我们就说过通过IOC容器获取,我们并不需要知道它的类型,只需要知道它所实现的接口或者继承的父类即可
修改测试类代码如下所示:
import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import spring_aop_annotation.Calculator; public class AopTest { @Test public void testAopAnnotation(){ ApplicationContext ioc=new ClassPathXmlApplicationContext("spring-aop-annotation.xml"); Calculator calculator=ioc.getBean(Calculator.class); calculator.add(4,10); } }
测试结果如下所示:
LoggerAspects,前置通知 方法内部:result,14
基于注解的AOP之切入点表达式的语法和重用以及获取连接点的信息:
这是我们上述所写的切入点表达式,其实很多地方不仅可以简化且还可以提高重用度:
@Before("execution(public int spring_aop_annotation.CalculatorImpl.add(int ,int ))")
首先来说简化的过程,其public int是可以直接使用 *
代替的,表示任意的访问修饰符和返回值类型
,且方法中的参数类型也是可以使用..
代替的,表示任意的参数列表
,当然类/包我们也可以使用*
代替,表示该包下的所有子包或者所有类
@Before("execution(* spring_aop_annotation.CalculatorImpl.add(..))")
提高重用率体现在:
这里我们是指定了CalculatorImpl下的add方法,那么如果当我们在测试类中直接调用目标对象的其他方法,会调用成功吗?
一试便知!
calculator.sub(4,10);
测试如下:
方法内部:result,-6
与上述加法不同的是,这里这输出了关于计算的信息,而并没有输出前置通知中的信息,原因是,在切面类的Before注解中我们指定了是add方法,因此,当我们调用其他方法时,前置方法的信息并不会被输出,这也就说明上述写法的重用率并不高
如下所示,我们不去指定目标对象的类中的方法,而是使用*
代替,表示类中任意的方法,如下所示:
@Before("execution(* spring_aop_annotation.CalculatorImpl.*(..))")
此时输出结果如下所示:
LoggerAspects,前置通知 方法内部:result,-6
在之前的动态代理中,目标对象方法执行之前,我们所做的工作是输出方法的方法名和参数列表,但我们上述所使用的这种方式该怎么获取其方法名和参数列表呢?
如下所示:
修改LoggerAspects类中的方法如下所示:
package spring_aop_annotation; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import java.util.Arrays; @Component @Aspect public class LoggerAspects { @Before("execution(* spring_aop_annotation.CalculatorImpl.*(..))") //获取连接点信息---在通知方法的参数列表处,设置JoinPoint类型的参数,就可以获取连接点所对应方法的信息 public void beforeAdviceMethod(JoinPoint joinPoint){ //获取连接点所对应的方法的签名信息[方法的声明部分] Signature signature=joinPoint.getSignature(); //获取连接点所对应方法的参数 Object[] args=joinPoint.getArgs(); System.out.println("LoggerAspects,方法:"+signature.getName()+",参数:"+ Arrays.toString(args)); } }
输出如下所示:
此时在输出结果中,我们成功获取到了参数名和参数列表
LoggerAspects,方法:sub,参数:[4, 10] 方法内部:result,-6
上述我们已经对其切入点表达式进行了简化,但这似乎还不是最简洁的,如果此时我们有了后置通知,那么在该后置通知的注解上依然要将该表达式再写一遍啊,对此我们可以进行切入点表达式的重用
切入点表达式的重用:
在LoggerAspects类中设置公用的切入点:
@Pointcut("execution(* spring_aop_annotation.CalculatorImpl.*(..))") public void pointCut(){}
在其他方法的注解中直接引用即可
基于注解的AOP之各种通知的使用:
后置通知:在目标对象方法的finally子句中执行
在介绍通知方法时,我们提到,后置通知是在被代理的目标方法最终结束后执行,但方法结束完对应的有两个位置,如下所示:
那么后置通知到底是执行那个呢?
一试便知!
第一步:在LoggerAspects;类中的后置通知中输出信息
System.out.println("LoggerAspects,方法:后置通知");
第二步:在测试类中人为创建异常
//调用除法运算-->被除数不能为0 calculator.div(4,0);
输出结果如下所示:
即使产生了异常,但后置通知中的信息依然被输出,由此可说明后置通知是在执行方法的finally子句中执行的
通过之前的学习我们知道finally是做善后处理,常用来关闭资源
等,那么我们在其中只需要获取执行方法的方法名,再输出执行完毕的信息即可,如下所示:
@After("pointCut()") public void afterAdviceMethod(JoinPoint joinPoint){ Signature signature=joinPoint.getSignature(); System.out.println("LoggerAspects,方法:"+signature.getName()+",执行完毕"); }
输出如下所示:
返回通知:在目标对象方法返回值之后执行
在LoggerAspects类中创建返回通知,如下所示:
@AfterReturning("pointCut()") public void adviceReturnMethod(){//创建返回通知 System.out.println("LoggerAspects,返回通知"); }
测试类测试,输出结果如下:
测试结果并没有输出返回通知中的有关信息,原因是返回通知是执行方法成功被调用并且直接结束之后才会执行的
,但这里有异常抛出啊,因此返回通知并没有被执行
修改测试类中的代码,如下所示:
calculator.div(4,1);
此时输出结果 如下所示:
LoggerAspects,方法:div,参数:[4, 1] 方法内部:result,4 LoggerAspects,返回通知 LoggerAspects,方法:div,执行完毕
返回通知就是在上述我们介绍后置通知时,方法结束完对应的有两个位置的第一个位置
既然返回通知是在执行方法成功被调用并且直接结束之后才会执行的,那么我们也可通过该通知获取返回值等,如下所示:
//在返回通知中若要获取目标对象方法的返回值,只需要通过@AfterReturning的returning属性,就可以将通知方法的某个参数指定为接受目标对象方法的返回值的参数 @AfterReturning(value = "pointCut()",returning = "result") public void adviceReturnMethod(JoinPoint joinPoint,Object result){ //创建返回通知 Signature signature=joinPoint.getSignature(); System.out.println("LoggerAspects,返回通知"); System.out.println("LoggerAspects,方法:"+signature.getName()+",执行结果:"+result); }
测试结果如下所示:
LoggerAspects,方法:div,参数:[4, 1] 方法内部:result,4 LoggerAspects,返回通知 LoggerAspects,方法:div,执行结果:4 LoggerAspects,方法:div,执行完毕
异常通知:在目标对象方法的catch子句中执行
在LoggerAspects类中创建异常通知,如下所示:
@AfterThrowing("pointCut()") public void afterThrowingAdviceMethod(JoinPoint joinPoint){ Signature signature=joinPoint.getSignature(); System.out.println("LoggerAspects,方法:"+signature.getName()+",异常通知"); }
输出如下所示,注意注意:若程序不发生异常,那么异常通知中的信息是不会被输出的
既然是通知,那么仅仅是输出通知是不够的,该通知应该输出具体的异常信息,如下所示:
//在返回通知中若要获取目标对象方法的异常信息,只需要通过@AfterThrowing的throwing属性, // 就可以将通知方法的某个参数指定为接受目标对象方法的异常的参数 @AfterThrowing(value = "pointCut()",throwing = "ex") public void afterThrowingAdviceMethod(JoinPoint joinPoint,Throwable ex){ Signature signature=joinPoint.getSignature(); System.out.println("LoggerAspects,方法:"+signature.getName()+",异常:"+ex); }
输出如下所示:此时异常信息被获取到并且成功输出
关于四个通知的符号说明:
各种通知的执行顺序:
Spring版本5.3.x以前:
前置通知 目标操作 后置通知 返回通知或异常通知
Spring版本5.3.以后:
前置通知 目标操作 返回通知或异常通知 后置通知
基于注解的AOP之环绕通知:
使用@Around注解标识,使用try-catch-finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
在LoggerAspects类中创建环绕通知,如下所示:
@Around("pointCut()") //环绕方法的返回值必须和目标对象方法的返回值一致 public Object aroundAdviceMethod(ProceedingJoinPoint proceedingJoinPoint) {//ProceedingJoinPoint可执行的连接点的对象 Object result=null; try { System.out.println("环绕通知-->前置通知"); result= proceedingJoinPoint.proceed();// 表示目标对象方法的执行,类似于动态代理中的invoke方法 System.out.println("环绕通知-->返回通知"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->异常通知"); }finally{ System.out.println("环绕通知-->后置通知"); } return result; }
测试结果如下所示:
环绕通知-->前置通知 LoggerAspects,方法:div,参数:[3, 5] 方法内部:result,0 LoggerAspects,返回通知 LoggerAspects,方法:div,执行结果:0 LoggerAspects,方法:div,执行完毕 环绕通知-->返回通知 环绕通知-->后置通知
切面的优先级:
上述我们创建了LoggerAspects切面,它是关于日志功能的,此后我们还会有其他的切面,例如关于事务功能的,关于验证的切面
等等
假设此时我们再创建一个关于针对计算器计算的切面,
package spring_aop_annotation; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Component @Aspect @Order public class validateAspects { @Before("spring_aop_annotation.LoggerAspects.pointCut()")//访问公共的切入点表达式 public void beforeMethod(){ System.out.println("validateAspects,前置通知"); } }
输出如下所示:
不同切面之间是存在优先级的,而优先级我们可以通过@Order注解
去设置
首先点开@Order注解
源码如下所示:
@Order注解
的属性值越小,优先级越高,而Integer.MAX_VALUE为切面的默认优先级
package org.springframework.core.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Documented public @interface Order { int value() default Integer.MAX_VALUE; }
设置validateAspects切面的优先级为1:
此时输出结果如下所示: