SpringAOP的使用

1、AOP概述

  • AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
  • AOP编程操作的主要对象是切面(aspect),而切面模块化横切关注点
  • 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
  • AOP术语:
    • 横切关注点:从每个方法中抽取出来的同一类非核心业务。
    • 切面(Aspect):封装横切关注点信息的类,每个关注点体现为一个通知方法。
    • 通知(Advice):切面必须要完成的各个具体工作。
    • 目标(Target):被通知的对象。
    • 代理(Proxy):向目标对象应用通知之后创建的代理对象。
    • 连接点(Joinpoint):横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。
    • 切入点(pointcut):定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

2、实现AOP

  • ①新建maven工程,导入aop模块Spring AOP(spring-aspects):
1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
  • ②定义一个业务逻辑类(MathCalculator)并加入到容器中,在业务逻辑运行的时候将日志进行打印(方法之前、方法运行结束、方法出现异常,xxx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 接口一般不加在容器中,
* 即使加了也不创建对象,
* 只要这个组件是一个接口,则相当于告诉spring容器中可能有这种类型的组件
*/
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);

}

@Service
public class MathCalculator implements Calculator{
public int add(int i, int j) {
System.out.println("add目标方法执行");
return i + j;
}

public int sub(int i, int j) {
System.out.println("sub目标方法执行");
return i - j;
}

public int mul(int i, int j) {
System.out.println("mul目标方法执行");
return i*j;
}

public int div(int i, int j) {
System.out.println("div目标方法执行");
return i/j;
}
}
  • ③定义一个日志切面类(logUtil)并加入到容器中,同时标注@Aspect注解(告知spring这是一个切面类),里面包含多个通知方法:
    • 前置通知(@Before):logStart(在目标方法运行之前运行)
    • 后置通知(@After):logEnd(在目标方法运行结束之后运行(无论方法正常结束还是异常结束))
    • 返回通知(@AfterReturning):logReturn(在目标方法(div)正常返回之后运行)
    • 异常通知(@AfterThrowing):logException(在目标方法(div)出现异常以后运行)
  • spring对通知方法的要求不严格,即是private也行,但是参数列表一定要准确。
  • AspectJ中的exection表达式:
    • execution(访问权限符 返回值类型 方法签名(参数表))
    • 通配符“*”的使用:
      • 匹配一个字符或多个字符:(“execution(public int com.example.aop.MathCal*or.*(int,int))”)
      • 匹配任意一个参数:(“execution(public int com.example.aop.MathCal*or.*(int,*))”)
      • 只能匹配一层路径:(“execution(public int com.example.*.MathCal*or.*(int,*))”)
      • 权限位置不能用*。
    • 通配符“..”的使用:
      • 匹配任意多个参数,任意类型参数:(“execution(public int com.example.aop.MathCal*or.*(..))”)
      • 匹配任意多层路径:(“execution(public int com.aop..MathCal*or.*(int,*))”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Component
@Aspect
public class logUtil {
//在目标方法开始之前执行
@Before("execution(* com.example.aop.MathCalculator.*(int,int))")
public static void logStart(JoinPoint joinPoint){
//获取目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取到方法签名
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name +"方法开始执行,参数列表为:" + Arrays.asList(args));
}

//在目标方法正常返回后执行
@AfterReturning(value = "execution(* com.example.aop.MathCalculator.*(int,int))",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name +"方法执行完成,执行结果为:" + result);
}

//在目标方法出现异常后执行
//如果只对某种特殊的异常类型感兴趣,可以将参数声明为其异常的参数类型,然后通知就只在抛出这个类型及其子类的异常时才被执行
@AfterThrowing(value = "execution(* com.example.aop.MathCalculator.*(int,int))",throwing = "exception")
public static void logException(JoinPoint joinPoint,Exception exception){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法出现异常,异常信息为:" + exception);
}

//在目标方法结束完后执行
@After("execution(* com.example.aop.MathCalculator.*(int,int))")
public static void logEnd(JoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法执行结束");
}
}

还可以使用抽取可重用的切入点表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Component
@Aspect
public class logUtil2 {
//1.声明一个void方法
//2.给方法上标上@Pointcut注解
@Pointcut("execution(* com.example.aop.MathCalculator.*(int,int))")
public void myPoint(){
}

//在目标方法开始之前执行
@Before("myPoint()")
public static void logStart(JoinPoint joinPoint){
//获取目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取到方法签名
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("logUtil前置:" + name +"方法开始执行,参数列表为:" + Arrays.asList(args));
}

//在目标方法正常返回后执行
@AfterReturning(value = "myPoint()",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("logUtil返回:" + name +"方法执行完成,执行结果为:" + result);
}

//在目标方法出现异常后执行
/*如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型
。然后通知就只在抛出这个类型及其子类的异常时才被执行*/
@AfterThrowing(value = "myPoint()",throwing = "exception")
public static void logException(JoinPoint joinPoint, Exception exception){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("logUtil异常:" + name + "方法出现异常,异常信息为:" + exception);
}

//在目标方法结束完后执行
@After("myPoint()")
public static void logEnd(JoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("logUtil后置:" + name + "方法执行结束");
}
}
  • ④给配置类中加 @EnableAspectJAutoProxy,并用@ComponentScan注解扫描aop包下的组件,即开启基于注解的aop模式:
1
2
3
4
5
6
7
@Configuration
//开启基于注解的aop模式
@EnableAspectJAutoProxy
@ComponentScan("com.example.aop")
public class MainConfig {

}
  • ⑤测试aop,如果业务逻辑类有实现接口,则默认使用的是jdk代理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainTest {
public static void main(String[] args) throws SQLException {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
//从ioc容器中获取目标对象,如果想要用类型找,就必须要用它的接口类型,不要用本类
//Calculator calculator = applicationContext.getBean(MathCalculator.class)报错No qualifying bean of type 'com.example.aop.MathCalculator' available
//aop的底层是动态代理,容器中保存的主键是他的代理对象$Proxy18,不是本类类型
Calculator calculator = applicationContext.getBean(Calculator.class);
System.out.println(calculator);//com.example.aop.MathCalculator@7bedc48a
System.out.println(calculator.getClass());//class com.sun.proxy.$Proxy26

//根据id获取,要用接口类型接收
//MathCalculator mathCalculator = (MathCalculator) applicationContext.getBean("mathCalculator")报错java.lang.ClassCastException: com.sun.proxy.$Proxy26 cannot be cast to com.example.aop.MathCalculator
Calculator calculator2 = (Calculator) applicationContext.getBean("mathCalculator");
System.out.println(calculator2);//com.example.aop.MathCalculator@7bedc48a
System.out.println(calculator2.getClass());//class com.sun.proxy.$Proxy26
}
}
  • 如果业务逻辑类没有实现接口,则是使用cglib代理,测试如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainTest {
public static void main(String[] args) throws SQLException {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
//没有实现接口就是本类类型
//CGLIB帮我们创建好代理对象
MathCalculator calculator = applicationContext.getBean(MathCalculator.class);
System.out.println(calculator);//com.example.aop.MathCalculator@33c911a1
System.out.println(calculator.getClass());//class com.example.aop.MathCalculator$$EnhancerBySpringCGLIB$$3034706b

MathCalculator calculator1 = (MathCalculator) applicationContext.getBean("mathCalculator");
System.out.println(calculator1);//com.example.aop.MathCalculator@33c911a1
System.out.println(calculator1.getClass());//class com.example.aop.MathCalculator$$EnhancerBySpringCGLIB$$3034706b
}
}
  • 测试通知方法的执行顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MainTest {
/*
* try{
* @Before
* method.invoke(obj,args)
* @AfterReturning
* }catch(e){
* @AfterThrowing
* }finally{
* @After
* }
* 正常执行:@Before(前置通知)-->@After(后置通知)-->@AfterReturning
* 出现异常:@Before(前置通知)-->@After(后置通知)-->@AfterThrowing
* */
public static void main(String[] args) throws SQLException {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
MathCalculator bean = applicationContext.getBean(MathCalculator.class);
bean.add(1,1);
System.out.println("****************");
bean.div(1,0);
}
}

执行结果:

1
2
3
4
5
6
7
8
9
add方法开始执行,参数列表为:[1, 1]
add目标方法执行
add方法执行结束
add方法执行完成,执行结果为:2
****************
div方法开始执行,参数列表为:[1, 0]
div目标方法执行
div方法执行结束
div方法出现异常,异常信息为:java.lang.ArithmeticException: / by zero
  • 如果使用环绕通知,可以获取目标方法的完全控制权(方法是否执行、控制参数、控制返回值),目标方法的一切信息,都可以通过invocation(invoke方法传进去的参数名称)参数获取到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Component
@Aspect
public class logUtil {
@Pointcut("execution(* com.example.aop.MathCalculator.*(int,int))")
public void myPoint(){
}

@Around("myPoint()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) {
Object[] args = proceedingJoinPoint.getArgs();
String name = proceedingJoinPoint.getSignature().getName();
Object proceed = null;
try {
//@Before
System.out.println("【环绕前置通知】:" + name +"方法开始执行,参数列表为:" + Arrays.asList(args));
//利用反射调用目标方法即可,就是method.invoke(obj,args)
proceed = proceedingJoinPoint.proceed(args);
//@AfterReturning
System.out.println("【环绕返回通知】:" + name +"方法执行完成,执行结果为:" + proceed);
} catch (Throwable throwable) {
//@AfterThrowing
System.out.println("【环绕异常通知】:" + name + "方法出现异常,异常信息为:" + throwable);
//为了让外界知道这个异常,得抛出去
throw new RuntimeException(throwable);
} finally {
//@After
System.out.println("【环绕后置通知】:" + name + "方法执行结束");
}
//反射调用后的返回值也一定返回
return proceed;
}
}

public class MainTest {
/*
* 正常执行:@Before【环绕前置通知】-->@AfterReturning【环绕返回通知】-->@After(后置通知)
* 出现异常:@Before【环绕前置通知】-->@AfterThrowing【环绕异常通知】-->@After(后置通知)
* */
public static void main(String[] args) throws SQLException {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
MathCalculator bean = applicationContext.getBean(MathCalculator.class);
bean.add(1,1);
System.out.println("****************");
bean.div(1,0);
}
}

测试结果:

1
2
3
4
5
6
7
8
9
【环绕前置通知】:add方法开始执行,参数列表为:[1, 1]
add目标方法执行
【环绕返回通知】:add方法执行完成,执行结果为:2
【环绕后置通知】:add方法执行结束
****************
【环绕前置通知】:div方法开始执行,参数列表为:[1, 0]
div目标方法执行
【环绕异常通知】:div方法出现异常,异常信息为:java.lang.ArithmeticException: / by zero
【环绕后置通知】:div方法执行结束
  • 如果普通通知和环绕通知同时开启:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Component
@Aspect
public class logUtil {
@Pointcut("execution(* com.example.aop.MathCalculator.*(int,int))")
public void myPoint(){
}

//在目标方法开始之前执行
@Before("execution(* com.example.aop.MathCalculator.*(int,int))")
public static void logStart(JoinPoint joinPoint){
//获取目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取到方法签名
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name +"方法开始执行,参数列表为:" + Arrays.asList(args));
}

//在目标方法正常返回后执行
@AfterReturning(value = "execution(* com.example.aop.MathCalculator.*(int,int))",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name +"方法执行完成,执行结果为:" + result);
}

//在目标方法出现异常后执行
//如果只对某种特殊的异常类型感兴趣,可以将参数声明为其异常的参数类型,然后通知就只在抛出这个类型及其子类的异常时才被执行
@AfterThrowing(value = "execution(* com.example.aop.MathCalculator.*(int,int))",throwing = "exception")
public static void logException(JoinPoint joinPoint, Exception exception){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法出现异常,异常信息为:" + exception);
}

//在目标方法结束完后执行
@After("execution(* com.example.aop.MathCalculator.*(int,int))")
public static void logEnd(JoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法执行结束");
}

@Around("myPoint()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) {
Object[] args = proceedingJoinPoint.getArgs();
String name = proceedingJoinPoint.getSignature().getName();
Object proceed = null;
try {
//@Before
System.out.println("【环绕前置通知】:" + name +"方法开始执行,参数列表为:" + Arrays.asList(args));
//利用反射调用目标方法即可,就是method.invoke(obj,args)
proceed = proceedingJoinPoint.proceed(args);
//@AfterReturning
System.out.println("【环绕返回通知】:" + name +"方法执行完成,执行结果为:" + proceed);
} catch (Throwable throwable) {
//@AfterThrowing
System.out.println("【环绕异常通知】:" + name + "方法出现异常,异常信息为:" + throwable);
//为了让外界知道这个异常,得抛出去
throw new RuntimeException(throwable);
} finally {
//@After
System.out.println("【环绕后置通知】:" + name + "方法执行结束");
}
//反射调用后的返回值也一定返回
return proceed;
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
【环绕前置通知】:add方法开始执行,参数列表为:[1, 1]
add方法开始执行,参数列表为:[1, 1]
add目标方法执行
【环绕返回通知】:add方法执行完成,执行结果为:2
【环绕后置通知】:add方法执行结束
add方法执行结束
add方法执行完成,执行结果为:2
****************
【环绕前置通知】:div方法开始执行,参数列表为:[1, 0]
div方法开始执行,参数列表为:[1, 0]
div目标方法执行
【环绕异常通知】:div方法出现异常,异常信息为:java.lang.ArithmeticException: / by zero
【环绕后置通知】:div方法执行结束
div方法执行结束
div方法出现异常,异常信息为:java.lang.RuntimeException: java.lang.ArithmeticException: / by zero

总结执行顺序为:环绕前置–>普通前置–>目标方法执行–>环绕返回/异常–>环绕后置–>普通后置–>普通返回/异常。

  • 如果有多个普通切面同时执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@Component
@Aspect
public class LogUtil {

@Pointcut("execution(* com.example.aop.MathCalculator.*(int,int))")
public void myPoint(){
}

//在目标方法开始之前执行
@Before("myPoint()")
public static void logStart(JoinPoint joinPoint){
//获取目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取到方法签名
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("LogUtil前置:" + name +"方法开始执行,参数列表为:" + Arrays.asList(args));
}

//在目标方法正常返回后执行
@AfterReturning(value = "myPoint()",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("LogUtil返回:" + name +"方法执行完成,执行结果为:" + result);
}

//在目标方法出现异常后执行
@AfterThrowing(value = "myPoint()",throwing = "exception")
public static void logException(JoinPoint joinPoint, Exception exception){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("LogUtil异常:" + name + "方法出现异常,异常信息为:" + exception);
}

//在目标方法结束完后执行
@After("myPoint()")
public static void logEnd(JoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("LogUtil后置:" + name + "方法执行结束");
}
}

@Component
@Aspect
public class AnotherLogUtil {
@Pointcut("execution(* com.example.aop.MathCalculator.*(int,int))")
public void myPoint(){
}

//在目标方法开始之前执行
@Before("myPoint()")
public static void logStart(JoinPoint joinPoint){
//获取目标方法运行时使用的参数
Object[] args = joinPoint.getArgs();
//获取到方法签名
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("AnotherLogUtil前置:" + name +"方法开始执行,参数列表为:" + Arrays.asList(args));
}

//在目标方法正常返回后执行
@AfterReturning(value = "myPoint()",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("AnotherLogUtil返回:" + name +"方法执行完成,执行结果为:" + result);
}

//在目标方法出现异常后执行
@AfterThrowing(value = "myPoint()",throwing = "exception")
public static void logException(JoinPoint joinPoint, Exception exception){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("AnotherLogUtil异常:" + name + "方法出现异常,异常信息为:" + exception);
}

//在目标方法结束完后执行
@After("myPoint()")
public static void logEnd(JoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("AnotherLogUtil后置:" + name + "方法执行结束");
}
}

执行结果(由于这两个切面类没有用@Order注解设置优先级,所以默认先后顺序由两个类的字母排序先后决定):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AnotherLogUtil前置:add方法开始执行,参数列表为:[1, 1]
LogUtil前置:add方法开始执行,参数列表为:[1, 1]
add目标方法执行
LogUtil后置:add方法执行结束
LogUtil返回:add方法执行完成,执行结果为:2
AnotherLogUtil后置:add方法执行结束
AnotherLogUtil返回:add方法执行完成,执行结果为:2
****************
AnotherLogUtil前置:div方法开始执行,参数列表为:[1, 0]
LogUtil前置:div方法开始执行,参数列表为:[1, 0]
div目标方法执行
LogUtil后置:div方法执行结束
LogUtil异常:div方法出现异常,异常信息为:java.lang.ArithmeticException: / by zero
AnotherLogUtil后置:div方法执行结束
AnotherLogUtil异常:div方法出现异常,异常信息为:java.lang.ArithmeticException: / by zero

图解:

如果在上面AnotherLogUtil类里加个环绕通知,则执行结果如下,证明环绕通知只影响当前所在切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
【环绕前置通知】:add方法开始执行,参数列表为:[1, 1]
AnotherLogUtil前置:add方法开始执行,参数列表为:[1, 1]
LogUtil前置:add方法开始执行,参数列表为:[1, 1]
add目标方法执行
LogUtil后置:add方法执行结束
LogUtil返回:add方法执行完成,执行结果为:2
【环绕返回通知】:add方法执行完成,执行结果为:2
【环绕后置通知】:add方法执行结束
AnotherLogUtil返回:add方法执行完成,执行结果为:2
****************
【环绕前置通知】:div方法开始执行,参数列表为:[1, 0]
AnotherLogUtil前置:div方法开始执行,参数列表为:[1, 0]
LogUtil前置:div方法开始执行,参数列表为:[1, 0]
div目标方法执行
LogUtil后置:div方法执行结束
LogUtil异常:div方法出现异常,异常信息为:java.lang.ArithmeticException: / by zero
【环绕异常通知】:div方法出现异常,异常信息为:java.lang.ArithmeticException: / by zero
【环绕后置通知】:div方法执行结束
AnotherLogUtil异常:div方法出现异常,异常信息为:java.lang.RuntimeException: java.lang.ArithmeticException: / by zero