Spring——AOP

Spring AOP

0. 思想

运用动态代理的方式对核心业务类的方法进行增强,对核心逻辑和非核心逻辑进行隔离,把非核心逻辑织入到核心业务逻辑之中。开发人员在开发时可以专注于核心业务逻辑,使各个业务逻辑之间的耦合度降低,提高程序的可复用性。

1. AOP的术语和细节

  1. Jointpoint(连接点)

    被拦截的方法,通常是业务层的所有方法。

  2. Pointcut(切入点)

    并不是所有业务层的方法都需要被拦截增强。那些需要被增强的方法是切入点。

  3. Advice(通知/增强)

    指拦截到的Jointpoint之后需要做的事情。通知包括前置通知、后置通知、异常通知、最终通知、环绕通知。

  4. Taeget(目标对象)

    被代理的对象。

  5. Weaving(织入)

    是指把增强应用到目标对象来创建代理对象的过程。

  6. Proxy(代理)

    一个类被AOP织入增强后,产生的一个结果代理类。

  7. Aspect(切面)

    是切入点和通知的结合。(需要手动配置)

2. XML配置AOP步骤

  1. 把通知bean交给spring管理

  2. 使用app:config标签表明开始AOP配置

  3. 使用aop:aspect标签表明开始配置切面

    id:切面的唯一标识
    ref:指定通知类bean的id

  4. aop:aspect标签内部使用对应标签来配置通知的类型,比如aop:before表示配置前置通知。
    method属性用于指定通知类中哪个方法是前置通知
    pointcut属性用于指定切入点表达式,表示对业务层哪些方法增强

  5. 切入点表达式写法:

    • 关键字:execution(表达式)

    • 表达式:访问修饰符 返回值 全限定类名.方法名(参数列表)

    • 标准写法:public void com.billychen.service.impl.AccountServiceImpl.saveAccount()

    • 访问修饰符可以省略 void com.billychen.service.impl.AccountServiceImpl.saveAccount()

    • 返回值可以使用通配符,表示任意返回值 * com.billychen.service.impl.AccountServiceImpl.saveAccount()

    • 包名可以使用通配符表示任意包,但是有几个包就要写几个*. 比如* *.*.*.*.AccountServiceImpl.saveAccount()

    • 包名可以使用..表示当前包及其子包 * *..AccountServiceImpl.saveAccount()

    • 类名和方法名均可以使用*实现通配

    • 参数列表写数据类型或全限定类名

    • 可以使用通配符表示任意参数类型,但必须有参数。也可以使用..表示有无参数均可以
      全通配写法:* *..*.*(..)

    • 实际开发的通常写法是切到业务层实现类的所有方法* com.billychen.service.impl.*.*(..)

以下是一个实例:

在该实例中,Logger是一个通知类,AccountServiceImpl是一个核心业务的类。

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
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!--配置service对象-->
<bean id="accountService" class="com.billychen.service.impl.AccountServiceImpl">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>

<!--配置Logger对象-->
<bean id="logger" class="com.billychen.utils.Logger"></bean>

<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<aop:before method="beforePrintLog" pointcut-ref="ptl"></aop:before>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="ptl"></aop:after-returning>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="ptl"></aop:after-throwing>
<aop:after method="afterPrintLog" pointcut-ref="ptl"></aop:after>

<!--仅限当前切面使用
如果写在切面之外,则可以供所有切面使用(要写在切面定义之前)-->
<aop:pointcut id="ptl" expression="execution(* com.billychen.service.impl.*.*(..))"></aop:pointcut>
</aop:aspect>
</aop:config>

</beans>

3.使用注解

配置类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.billychen.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
* 配置类
* 开启AOP注解
* 指定创建IoC容器时需要扫描的包
*/
@EnableAspectJAutoProxy
@Configuration
@ComponentScan(basePackages = "com.billychen")
public class SpringConfig {
}

Logger类如下(即通知类)

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
package com.billychen.utils;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
* 模拟记录日志的工具类,提供了共用代码(通知)
*/
@Component
@Aspect
public class Logger {

//指定切入点方法
@Pointcut("execution(* com.billychen.service.impl.*.*(..))")
private void ptl() {

}

/**
* 前置
*/
//可以获取切点方法的参数
@Before("ptl() && args(i)")
public void beforePrintLog(JoinPoint joinPoint, int i) {
Object[] args = joinPoint.getArgs();
System.out.println("参数为" + args[0]);
System.out.println("前置:Logger类中的BeforePrintLog方法开始记录日志!");
}

/**
* 后置
*/
@AfterReturning("ptl()")
public void afterReturningPrintLog() {
System.out.println("后置:Logger类中的AfterReturningPrintLog方法开始记录日志!");
}

/**
* 异常
*/
@AfterThrowing("ptl()")
public void afterThrowingPrintLog() {
System.out.println("异常:Logger类中的AfterThrowingPrintLog方法开始记录日志!");
}

/**
* 最终
*/
@After("ptl()")
public void afterPrintLog() {
System.out.println("最终:Logger类中的AfterPrintLog方法开始记录日志!");
}

/**
* 环绕
* ProceedingJoinPoint的方法proceed()可以明确调用切入点的方法
* 环绕通知 是spring提供的一种可以在代码中手动控制增强方法何时执行的方式
*/
//@Around("ptl()")
public Object aroundPrintLog(ProceedingJoinPoint joinPoint) {
//明确调用业务层方法
Object returnValue = null;
try {
//得到方法执行的参数
Object[] args = joinPoint.getArgs();
System.out.println("环绕通知——前置");
returnValue = joinPoint.proceed(args);
System.out.println("环绕通知——后置");
return returnValue;
} catch (Throwable throwable) {
System.out.println("环绕通知——异常");
throw new RuntimeException(throwable);
} finally {
System.out.println("环绕通知——最终");
}
}
}