Spring AOP

1. AOP简介

1.1. 什么是AOP

AOP:全称是Aspect Oriented Programming 即:面向切面编程

简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强

1.2. AOP的作用及优势

作用:在程序运行期间,不修改源码对已有方法进行增强

优势:减少重复代码、提高开发效率、维护方便

1.3. AOP的实现原理

动态代理。Spring会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

1.4. AOP相关术语

Joinpoint(连接点):被拦截到的点。Spring只支持方法类型的连接点,所以连接点就是要拦截的方法。简单地讲,ServiceImpl中所有的方法,都是连接点

Pointcut(切入点):被切面增强的连接点

Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知的类型:前置通知(before),后置通知(after),异常通知(afterThrowing),返回通知(afterReturning),环绕通知(around)

Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或Field

Target(目标对象):代理的目标对象,即被代理对象,比如ServiceImpl

Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类

Aspect(切面):是切入点和通知(引介)的结合

连接点和切入点的区别:

  • 连接点不一定是切入点,切入点一定是连接点
  • 连接点是被拦截的方法,该方法可能会被增强,也可能不会,如果被切面增强,就是切入点

1.5. 学习Spring中的AOP要明确的事

开发阶段(我们做的)

  • 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
  • 把公用代码抽取出来,制作成通知。(开发阶段最后再做)
  • 在配置文件中,声明切入点与通知间的关系,即切面。

运行阶段(Spring框架完成的)

  • Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

1.6. 实际开发中切入点表达式的写法

切到业务层实现类ServiceImpl下所有的方法

2. 切入点表达式

切入点表达式指定要对哪些方法进行增强,即指定切入点

表达式形式:返回值 返回类型 包名.类名.方法名(形参类型列表)

2.1. public修饰符可省略

切入点表达式的访问权限默认是public,所以以下两个表达式是等价的

1
2
public void demo.spring.service.UserServiceImpl.addUser()
void demo.spring.service.UserServiceImpl.addUser() // 省略public

2.2. 形参类型列表

参数是基本类型,直接写名称。如果是引用类型,则写全限定名

1
2
* *..*.*(int)
* *..*.*(int, long, java.lang.String)

2.3. 使用通配符

返回值类型可以使用通配符。

1
2
void demo.spring.service.UserServiceImpl.addUser()  // 返回类型是void
* demo.spring.service.UserServiceImpl.addUser() // 返回类型可以是任意类型

方法名可以使用通配符

1
2
* demo.spring.service.UserServiceImpl.addUser()     // 匹配addUser()方法
* demo.spring.service.UserServiceImpl.*() // 匹配所有空参方法

参数类型列表可以使用通配符,注意通配符是两个点.

1
2
* demo.spring.service.UserServiceImpl.*()           // 匹配所有空参方法
* demo.spring.service.UserServiceImpl.*(..) // 匹配所有方法(参数列表任意)

类名可以使用通配符

1
2
3
* demo.spring.service.UserServiceImpl.*(..)      // 匹配UserServiceImpl类
* demo.spring.service.*ServiceImpl.*(..) // 匹配以ServiceImpl名称结尾的类
* demo.spring.service.*.*(..) // 匹配demo.spring.service包下的所有类

包名分隔符可以写作两个点.,表示当前包及其子包的通配

1
2
* demo.spring.service.*.*(..)         // 匹配demo.spring.service包下的所有类
* demo.spring.service..*.*(..) // 匹配demo.spring.service包及其子包下的所有类(递归匹配)

全通配写法 * *..*.*(..)

3. AOP各通知执行顺序

AOP各通知执行逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
try {
try {
// @Before 前置通知
method.invoke(..);
} finally {
// @After 后置通知
}
// @AfterReturning 返回通知
} catch () {
// @AfterThrowing 异常通知
}

执行分两种情况:

  • 执行正常:before、目标方法、after、afterReturning
  • 执行异常:before、目标方法、after、afterThrowing

也就是说,before、目标方法、after这3个步骤是一定会执行的,最后一步是异常通知还是返回通知,取决于是否发生异常

4. AOP案例环境搭建

4.1. 依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Spring核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>

4.2. 目标对象Target

1
2
3
4
public interface UserService {
void addUser();
void deleteUser();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* UserServiceImpl是被增强的对象,即目标对象Target
*/
public class UserServiceImpl implements UserService {
/* 连接点: 目标对象中,所有可以被增强的方法
* addUser()和deleteUser()就是连接点
* */

/* 切入点: 被切面增强的连接点。
* addUser()和deleteUser()都被增强了,所以都是切入点
* */

@Override
public void addUser() {
System.out.println("目标方法: Adduser");
}

@Override
public void deleteUser() {
System.out.println("目标方法: deleteuser");
int a = 1 / 0; // 制造异常
}
}

4.3. 自定义切面类

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
package demo.spring.springlearning.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class MyAspect {
public void before(JoinPoint joinPoint) {
System.out.println("before");
}

public void after() {
System.out.println("after");
}

public Object around(ProceedingJoinPoint pjp) {
Object result = null;
try {
try {
Object[] args = pjp.getArgs(); // 获取方法参数
System.out.println("前置通知");
result = pjp.proceed(args); // 调用目标方法
} finally {
System.out.println("后置通知");
}
System.out.println("返回通知");

} catch (Throwable t) {
System.out.println("异常通知");
}
return result;
}

public void afterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("afterThrowing " + e.getMessage());
}

public void afterReturning(JoinPoint joinPoint) {
System.out.println("afterReturning");
}
}

4.4. 案例:前/后/异常/返回 通知

编辑applicationContext.xml

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
<!-- 定义目标Bean -->
<bean class="demo.spring.springlearning.service.impl.UserServiceImpl"/>

<!-- 定义切面Bean -->
<bean id="myAspect" class="demo.spring.springlearning.aspect.MyAspect"/>

<!-- aop:config 声明AOP配置 -->
<aop:config>
<!-- aop:pointcut 配置切入点 -->
<aop:pointcut id="pc" expression="execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))"/>

<!-- aop:aspect配置切面
id: 给切面起一个标识,可以省略
ref: 指定切面Bean
-->
<aop:aspect id="userAdvice" ref="myAspect">
<!-- 前置通知
method: 指定切面的哪个方法作为前置通知
pointcut-ref: 指定<aop:pointcut>的id
-->
<aop:before method="before" pointcut-ref="pc"/>
<!-- 后置通知 -->
<aop:after method="after" pointcut-ref="pc"/>
<!-- 返回通知 -->
<aop:after-returning method="afterReturning" pointcut-ref="pc"/>
<!-- 异常通知 -->
<aop:after-throwing method="afterThrowing" pointcut-ref="pc" throwing="e"/>
</aop:aspect>
</aop:config>

测试

1
2
3
4
5
6
7
8
@Test
public void test() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
UserService userService = ioc.getBean(UserService.class);
userService.addUser();
System.out.println("============================");
userService.deleteUser();
}

4.5. 案例:环绕通知

环绕通知与其它通知,一般单独使用。环绕通知具体要做什么,由程序员决定,而不是由Spring来决定,所以灵活性最好。

实现方式是:在环绕通知切入点方法中,传入ProceedingJoinPoint参数。ProceedingJoinPoint是一个接口。你可以通过pjp.getArgs()获取目标方法的执行参数,通过pjp.proceed(args)执行目标方法。最后你要返回执行目标方法的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object around(ProceedingJoinPoint pjp) {
Object result = null;
try {
try {
Object[] args = pjp.getArgs(); // 获取方法参数
System.out.println("前置通知");
result = pjp.proceed(args); // 调用目标方法
} finally {
System.out.println("后置通知");
}
System.out.println("返回通知");

} catch (Throwable t) {
System.out.println("异常通知");
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 定义目标Bean -->
<bean class="demo.spring.springlearning.service.impl.UserServiceImpl"/>

<!-- 定义切面Bean -->
<bean id="myAspect" class="demo.spring.springlearning.aspect.MyAspect"/>

<!-- aop:config 声明AOP配置 -->
<aop:config>
<!-- aop:pointcut 配置切入点 -->
<aop:pointcut id="pc" expression="execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))"/>
<!-- aop:aspect配置切面
id: 给切面起一个标识,可以省略
ref: 指定切面Bean
-->
<aop:aspect id="userAdvice" ref="myAspect">
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="pc"/>
</aop:aspect>
</aop:config>

5. AOP配置详解

5.1. 切入点的几种配置方式

5.1.1. 通过pointcut属性设置切入点表达式

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 定义目标Bean -->
<bean class="demo.spring.springlearning.service.impl.UserServiceImpl"/>
<!-- 定义切面Bean -->
<bean id="myAspect" class="demo.spring.springlearning.aspect.MyAspect"/>
<!-- AOP配置 -->
<aop:config>
<aop:aspect id="userAdvice" ref="myAspect">
<!-- pointcut="切入点表达式" -->
<aop:before method="before" pointcut="execution(* demo.spring.springlearning.service.impl.*.*(..))"/>
<aop:after method="after" pointcut="execution(* demo.spring.springlearning.service.impl.*.*(..))"/>
</aop:aspect>
</aop:config>

5.1.2. 通过pointcut-ref引用已配置的切入点

多个通知的切入点表达式可能会重复,显得冗余。这时候可以用<aop:pointcut>单独定义一个切入点,在配置通知时,使用pointcut-ref来引用对应的切入点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 定义目标Bean -->
<bean class="demo.spring.springlearning.service.impl.UserServiceImpl"/>
<!-- 定义切面Bean -->
<bean id="myAspect" class="demo.spring.springlearning.aspect.MyAspect"/>
<!-- AOP配置 -->
<aop:config>
<aop:aspect id="userAdvice" ref="myAspect">
<!-- aop:pointcut 配置切入点 -->
<aop:pointcut id="pc" expression="execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))"/>
<!-- pointcut-ref 引用切入点 -->
<aop:before method="before" pointcut-ref="pc" />
<aop:after method="after" pointcut-ref="pc" />
</aop:aspect>
</aop:config>

AOP可以配置多个<aop:aspect>切面,以上示例的<aop:pointcut>是配置在<aop:aspect>内部的,只有该切面内部的通知才能引用该切入点。如果想定义好的切入点可以被所有的通知引用,就把<aop:pointcut>放到外面。注意DTD约束要求<aop:pointcut>必须写在<aop:aspect>之后,否则会出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 定义目标Bean -->
<bean class="demo.spring.springlearning.service.impl.UserServiceImpl"/>
<!-- 定义切面Bean -->
<bean id="myAspect" class="demo.spring.springlearning.aspect.MyAspect"/>
<!-- AOP配置 -->
<aop:config>
<!-- 将切入点放到外面。此时<aop:pointcut>必须在<aop:aspect>之前定义 -->
<aop:pointcut id="pc" expression="execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))"/>
<aop:aspect id="userAdvice" ref="myAspect">
<!-- pointcut-ref 引用切入点 -->
<aop:before method="before" pointcut-ref="pc" />
<aop:after method="after" pointcut-ref="pc" />
</aop:aspect>
</aop:config>

6. AOP基于注解配置

6.1. 注解定义Bean

开启包扫描

1
<context:component-scan base-package="demo.spring.springlearning"/>

使用@Service标识Target

1
2
@Service
public class UserServiceImpl implements UserService {

使用@Component标识自定义切面

1
2
@Component
public class MyAspect {

6.2. AOP注解配置

开启AOP注解支持

1
<aop:aspectj-autoproxy/>

使用@Aspect声明切面类。使用@Before等等定义对应的通知,value是切入点表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Aspect // 声明当前类是一个切面类,相当于<aop:config>
public class MyAspect {
@Before("execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))")
public void before(JoinPoint joinPoint) {
System.out.println("before");
}

@After("execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))")
public void after() {
System.out.println("after");
}

@AfterThrowing(value = "execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("afterThrowing " + e.getMessage());
}

@AfterReturning("execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("afterReturning");
}
}

觉得切入点表达式重复冗余,可以使用@Pointcut定义一个单独的切入点

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
@Component
@Aspect // 声明当前类是一个切面类,相当于<aop:config>
public class MyAspect {
/**
* 定义一个切入点表达式,相当于<aop:pointcut>
*/
@Pointcut("execution(* demo.spring.springlearning.service.impl.UserServiceImpl.*(..))")
public void pt() {
}

/** 引用切入点 */
@Before("pt()")
public void before(JoinPoint joinPoint) {
System.out.println("before");
}

@After("pt()")
public void after() {
System.out.println("after");
}

// @Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object result = null;
try {
try {
Object[] args = pjp.getArgs(); // 获取方法参数
System.out.println("前置通知");
result = pjp.proceed(args); // 调用目标方法
} finally {
System.out.println("后置通知");
}
System.out.println("返回通知");

} catch (Throwable t) {
System.out.println("异常通知");
}
return result;
}

@AfterThrowing(value = "pt()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("afterThrowing " + e.getMessage());
}

@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("afterReturning");
}
}

6.3. AOP完全注解配置(不使用XML)

定义Spring配置类

1
2
3
4
5
6
7
@Configuration
// 相当于<context:component-scan base-package="demo.spring.springlearning"/>
@ComponentScan(basePackages = "demo.spring.springlearning")
// 相当于<aop:aspectj-autoproxy/>
@EnableAspectJAutoProxy
public class SpringConfig {
}

测试

1
2
3
4
5
6
7
8
@Test
public void test() {
ApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class);
UserService userService = ioc.getBean(UserService.class);
userService.addUser();
System.out.println("============================");
userService.deleteUser();
}
panchaoxin wechat
关注我的公众号
支持一下