Spring(三)-AOP
1、名词理解
- 切面(Aspect):
- 含有前置通知,后置通知,返回通知,异常抛出通知,环绕通知等方法的类;
- 通知(Advice):
- 对原方法进行添加处理(如日志等)的方法;
- 切入点(PointCute):
- 通知需要在哪些方法上执行的表达式;(可以唯一匹配或模糊匹配);
- 连接点(JoinPoint):
- 与切入点匹配的具体执行的方法;
- 目标(Target):
- 原业务类(主要 是核心代码);
- 代理(Proxy):
- 生成的代理类(包含原业务类的 核心代码 和 通知里面的代码);
2、前置通知
2.1 jar
<properties>
<spring.version>4.3.18.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring-beans begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-beans end -->
<!-- spring-core begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-core end -->
<!-- spring-context begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-context end -->
<!-- spring-expression begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-expression end -->
<!-- spring-aspects begin -->
<!-- maven项目中,使用aop的AspectJ框架,只需要增加此依赖,自动添加依赖aspectjweaver(包含了aspectjrt)-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-aspects end -->
</dependencies>
2.2 切入点
通知需要在哪些方法上执行的表达式;(可以唯一匹配或模糊匹配);
2.2.1 唯一匹配
execution(public int com.kgc.spring.aspectj.ArithmeticCalculator.add(int ,int ))
execution(修饰符 返回值类型 方法全类名)
2.2.2 模糊匹配
execution(* com.kgc.spring.aspectj.*.*(..)
通用切入点表达式含义:
-
第一个*:代表任意的修饰符,任意的返回值类型;
-
第二个*:代表任意的类;
-
第三个*:代表任意的方法;
-
. . :代表任意的类型和个数的形参;
2.2.3 可重用切入点表达式
其他地方直接应用此方法即可;
//重用切入点表达式
@Pointcut( "execution(* com.kgc.spring.aspectj.*.*(..))")
public void joinPointcut(){}
//同一个类中引用
@Before("joinPointcut()")
@After("joinPointcut()")
//其他类中引用(方法全类名)
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
2.3 JoinPoint 和 ProceedingJoinPoint
2.3.1 JoinPoint 对象
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。
常用api:
方法名 | 功能 |
---|---|
Signature getSignature(); | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |
2.3.2 ProceedingJoinPoint对象
ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中 添加了 两个方法.
方法名 | 功能 |
---|---|
Object proceed() throws Throwable | 执行目标方法 |
Object proceed(Object[] var1) throws Throwable | 传入的新的参数去执行目标方法 |
2.4 @Before
2.4.1 接口
ArithmeticCalculator
public interface ArithmeticCalculator {
//加
int add(int m,int n);
//减
int sub(int m,int n);
//乘
int nul(int m,int n);
//除
int div(int m,int n);
}
2.4.2 实现类
ArithmeticCalculatorImpl
@Service("arithmeticCalculator")
//起别名,方便单元测试,根据别名,从容器中获取
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int m, int n) {
return m + n;
}
@Override
public int sub(int m, int n) {
return m - n;
}
@Override
public int nul(int m, int n) {
return m*n;
}
@Override
public int div(int m, int n) {
System.out.println("====== 执行 div 方法 ======");
return m/n;
}
}
2.4.3 @Before 前置通知
在目标方法执行前,自动执行此方法(通过代理实现);
@Component //声明为一个普通的组件,放入spring的容器中,才可以生效
@Aspect //声明当前类是 一个切面
public class LogAspect {
//重用切入点表达式
@Pointcut( "execution(* com.kgc.spring.aspectj.*.*(..))")
public void joinPointcut(){}
//前置通知 @Before
@Before("joinPointcut()")
public void logBeforeMethod(JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspect "+methodName+" 方法,入参:"+ Arrays.toString(methodParams) +" ------");
}
}
2.5 配置文件
spring-aop.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/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">
<!-- 组件 -->
<context:component-scan base-package="com.kgc.spring.aspectj"></context:component-scan>
<!-- 基于注解方式实现Aspect切面 -->
<!-- 作用:当spring的容器检测到此配置项,会自动将Aspect切面匹配的目标对象,放入容器,默认使用的是jdk的动态代理 -->
<aop:aspectj-autoproxy ></aop:aspectj-autoproxy>
</beans>
2.6测试
public void testSpringAopAspectj(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
class com.sun.proxy.$Proxy15
------ LogAspect div 方法,入参:[20, 10] ------
====== 执行 div 方法 ======
****** 通过单元测试,计算结果:2 ******
3、后置通知
3.1 @After
目标方法发执行之后,自动执行;
特点:
- 后置通知无法获取目标方法的返回值;
- 它的执行跟目标方法是否抛出异常无关,不影响此方法的执行;
@After("joinPointcut()")
public void logAfterMethod(JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspect "+methodName+" 方法执行结束 ------");
}
3.2 测试
3.2.1 无异常
@Test
public void testSpringAopAspectj(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
====== 执行 div 方法 ======
------ LogAspect div 方法执行结束 ------
****** 通过单元测试,计算结果:2 ******
3.2.2 有异常
@Test
public void testSpringAopAspectj(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 0);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
====== 执行 div 方法 ======
------ LogAspect div 方法执行结束 ------ //有异常也会执行后置通知
java.lang.ArithmeticException: / by zero
4、返回通知
4.1 @AfterReturning
- 目标方法返回结果后自动执行,可以获取目标方法的返回值;
- 但是要求@AfterReturning必须增加属性returning,指定一个参数名;
- 且此参数名必须跟通知方法的一个形参名一致,用于接收返回值;
@AfterReturning(value = "joinPointcut()",returning = "result")
public void afterReturningMethod(JoinPoint joinPoint,Object result){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("------ LogAspect "+methodName+" 方法,执行结果:"+ result +" ------");
}
4.2 测试
测试结果
====== 执行 div 方法 ======
------ LogAspect div 方法,返回结果:2 ------
****** 通过单元测试,计算结果:2 ******
5、异常抛出通知
5.1 @AfterThrowing
- 异常抛出通知 @AfterThrowing ,在目标方法抛出异常后,可以获取目标方法发生异常后抛出的异常信息;
- 但是要求 @AfterThrowing 必须增加属性 throwing,指定一个参数名;
- 且此参数名必须跟通知方法的一个形参名一致,用于接收异常;
@AfterThrowing(value = "joinPointcut()",throwing = "ex")
public void logAfterThrowingMethod(JoinPoint joinPoint,Exception ex){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("------ LogAspect "+methodName+" 方法,执行异常信息:"+ ex.getMessage() +" ------");
}
5.2 测试
@Test
public void testSpringAopAspectj(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 0);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
====== 执行 div 方法 ======
------ LogAspect div 方法,执行异常信息:/ by zero ------
java.lang.ArithmeticException: / by zero
6、环绕通知
6.1 @Around
- 环绕通知 @Around,可以看作是上面四种通知的结合体,一般不建议跟单个的通知共用(防止冲突失效);
- 作用:可以让开发人员在环绕通知的处理方法中根据不同也业务逻辑,决定是否发起对目标方法的调用;
@Around(value = "joinPointcut()")
public Object logAroundMethod(ProceedingJoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//定义获取目标方法的返回值变量
Object result = null;
try{
//实现前置通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,入参:"+ Arrays.toString(joinPoint.getArgs()) +" ------");
//手动调用原目标方法(业务中决定,是否对核心方法方法发起调用)
result = joinPoint.proceed();
}catch (Throwable tx){
//实现异常抛出通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,执行异常信息:"+ tx.getMessage() +" ------");
}finally {
//实现后置通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,执行结束 ------");
}
//实现返回通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,执行结果:"+ result +" ------");
return result;
}
6.2 测试
6.2.1 测试结果,无异常
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 10);
class com.sun.proxy.$Proxy13
------ LogAspect div 方法 Around通知,入参:[20, 10] ------
====== 执行 div 方法 ======
------ LogAspect div 方法 Around通知,执行结束 ------
------ LogAspect div 方法 Around通知,返回结果:2 ------
****** 通过单元测试,计算结果:2 ******
6.2.2 测试结果,有异常
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 0);
class com.sun.proxy.$Proxy13
------ LogAspect div 方法 Around通知,入参:[20, 0] ------
====== 执行 div 方法 ======
------ LogAspect div 方法 Around通知,执行异常信息:/ by zero ------
------ LogAspect div 方法 Around通知,执行结束 ------
------ LogAspect div 方法 Around通知,返回结果:null ------
6.2.3 测试结果 不调用 原方法
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 0);
//(业务中决定,是否对核心方法发起调用)
//不调用核心方法
//result = joinPoint.proceed();
------ LogAspect div 方法 Around通知,入参:[20, 10] ------
------ LogAspect div 方法 Around通知,执行结束 ------
------ LogAspect div 方法 Around通知,返回结果:null ------
7、切入点优先级
当有多个前置通知时,我们想自定义前置通知顺序:使用@Order(int)
指定切面优先级,一般都是int型整数,值越小,优先级越高**(默认值 2^31 – 1 最低优先级);
7.1 多个前置通知
logBeforeMethod
@Before("joinPointcut()")
public void logBeforeMethod(JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspectBeforeMethod "+methodName+" 方法,入参:"+ Arrays.toString(methodParams) +" ------");
}
verifyBeforeMethod
@Component
@Aspect
public class VerifyParamAspect {
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
public void verifyBeforeMethod( JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ verifyBeforeMethod "+methodName+" 方法,入参:"+ Arrays.toString(methodParams) +" ------");
}
}
7.2 测试(默认)
@Test
public void testVerifyParamAspect(){
//从容器中获取计算器的实例对象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//调用切面作用的目标方法,执行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通过单元测试,计算结果:"+result +" ******");
}
测试结果
------ LogAspectBeforeMethod div 方法,入参:[20, 10] ------ //LogAspectBeforeMethod 先执行
------ verifyBeforeMethod div 方法,入参:[20, 10] ------
====== 执行 div 方法 ======
****** 通过单元测试,计算结果:2 ******
7.3 测试(自定义优先级)
@Component
@Aspect
@Order(1) //指定切面优先级,一般都是int型整数,值越小,优先级越高(默认值 2^31 - 1)
public class VerifyParamAspect {
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
public void verifyBeforeMethod( JoinPoint joinPoint){
//获取通知作用的目标方法名
String methodName = joinPoint.getSignature().getName();
//获取通知作用的目标方法入参,返回的是参数值数组
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ verifyBeforeMethod "+methodName+" 方法,入参:"+ Arrays.toString(methodParams) +" ------");
}
}
测试结果
------ verifyBeforeMethod div 方法,入参:[20, 10] ------ //优先级高的切面中的verifyBeforeMethod,先执行
------ LogAspectBeforeMethod div 方法,入参:[20, 10] ------
====== 执行 div 方法 ======
****** 通过单元测试,计算结果:2 ******