Spring Aop 面向切面编程

Aop概述

OOP 原则之一是每个类都应该有一个单一的职责。因此,我们创建了多个类来共同处理一项任务,其中每个类负责处理特定的功能。然而,一些关注点打破了这个原则,而 AOP 帮助我们解决了这些问题。
什么是AOP (Aspect Oriented Programming)呢?AOP是面向切面编程,相比较OOP面向对象编程来说,Aop关注的不再是程序中的某个类、某些方法,Aop考虑的是一种面到面的切入,即层与层之间的切入,所以称之为切面。AOP的底层实现原理是 动态代理 (JDK + CGLIB)。

Aop可以做什么呢?主要应用于日志记录、性能统计、安全控制、事务处理等公共功能性(非业务性)的重复使用。

比如美元支付和人民币支付都实现了支付这个接口,现在需要在支付业务中增加日志功能,这个需要可以通过 装饰器模式或代理模式 实现。但如果需要添加日志功能的业务类数量很多,此时手动为每个业务类增加装饰器或代理,工程量比较大且耦合度也比较大。

上面的需求可以通过AOP模式实现,实现代码复用且达到松耦合的要求。

Aop的特点:

  • 降低模块与模块之间的耦合度,提高业务代码的聚合度(高内聚低耦合)
  • 提高了代码的复用性
  • 提高系统的扩展性(高版本兼容低版本)
  • 可以在不影响原有功能的基础上添加新的功能。

Aop 术语

  1. Joinpoint(连接点)
  2. Pointcut(切入点)
  3. Advice(通知)
  4. Aspect(切面)
  5. Target(目标对象)
  6. Weave(织入)
  7. Introduction(引入)

Joinpoint(连接点)

被拦截的每个点,spring中指被拦截的每一个方法,Spring Aop 一个连接点代表一个方法的执行,即被拦截的方法或被代理的方法

Pointcut(切入点)

对连接点进行拦截的定义(匹配规则定义,规定拦截哪些方法,对哪些方法进行处理),Spring有专门的表达式语言定义。即定义拦截条件,满足条件的方法才被拦截(代理)被切入。

Advice(通知)

拦截到每一个 Joinpoint(连接点)后所要作的操作。即代理方法,扩展委托(被代理)的方法(扩展被拦截的方法)

通知类型 标签 说明
前置通知 before 连接点执行前的行为
返回通知 afterReturn 连接点执行正常结束后的行为
异常抛出通知 afterThrow 连接点执行发生异常后的行为
最终通知 after 连接点执行结束后的行为,无论是否发生异常,类似finally
环绕通知 around 包围一个连接点(Joinpoint)的通知。最强大的一种通知类型(包含了上面4种通知),环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行(类似nodejs的中间件概念)

Aspect(切面)

切入点与通知的结合,决定了切面的定义,切入点定义了要拦截哪些类的哪些方法,通知则定义了拦截到方法后要做什么,切面则是横切关注点的抽象,与类相似,类是对物体特征的抽象,切面是横切关注点的抽象(即切面类,在这个切面类中定要切入点通知)。

Target(目标对象)

被代理的对象

Weave(织入)

将切面应用到目标对象并生成代理对象的这个过程叫做织入。

Introduction(引入)

在不修改原有程序代码的情况下,在程序运行期间为类动态添加方法或者字段的过程叫做引入。

概念关系图

Aop实现

环境搭建

pom.xml 导入依赖包

  • srping-context
  • aspectjweaver
pom.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
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>shsxt-spring-aop</artifactId>
<version>1.0-SNAPSHOT</version>

<name>shsxt-spring-aop</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>

<build>
</build>
</project>

配置 Spring 配置

  • 开启自动扫描包
  • 开启Aop自动代理
spring.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 设置自动扫描包 -->
<context:component-scan base-package="org.example"/>

<!-- 开启Aop,注解实现:配置Aop自动代理 -->
<aop:aspectj-autoproxy />
</beans>

使用注解实现Aop

定义业务类

UserService
1
2
3
4
5
6
7
8
9
10
package org.example.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
public void test(String userName){
System.out.println("User service test..." + userName);
}
}

定义切面

LogCut
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
87
88
89
90
package org.example.aspect;

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

/**
* 定义切面
* 切面是 切入点 和 通知的结合
* 切入点:定义规则, 定义切面要拦截哪些类的哪些方法
* 通知:方法拦截后要做的事情
*/
@Component // 将类交给 IOC 容器管理
@Aspect // 声明当前类是一个切面
public class LogCut {
/**
* 定义切入点
* 通过 @Pointcut 定义规则 SpEL表达式
* @Pointcut("execution(* *(..))") 所有方法
* @Pointcut("execution(* set*(..))") 所有set方法
* @Pointcut("execution(* org.example.service..*.*(..))") org.example.service及其子包下面的所有类的所有方法
* @Pointcut("execution(* org.example.service.*.*(..))") org.example.service下面的所有类的所有方法
*/
@Pointcut("execution(* org.example.service..*.*(..))")
public void cut() {}

/**
* 前置通知,在目标方法执行前 执行
*/
@Before(value="cut()")
public void before() {
System.out.println("cut切入点的前置通知...");
}

/**
* 返回通知,目标方法正常返回后执行
*/
@AfterReturning(value="cut()")
public void afterReturn(){
System.out.println("cut切入点的返回通知...");
}

/**
* 最终通知 在目标方法执行后(无论是否出现异常)执行
*/
@After(value="cut()")
public void after(){
System.out.println("cut切入点的最终通知...");
}

/**
* 异常通知 当目标方法发生错误时执行
* @param e
*/
@AfterThrowing(value="cut()", throwing = "e")
public void afterThrow(Exception e){
System.out.println("cut切入点的异常通知..." + e.getMessage());
}

/**
* 环绕通知
* 环绕通知有返回值
* 环绕通知需要手动调用目标方法,否则目标方法不会执行 pjp.proceed();
* @param pjp 用于调入连接点(目标方法)
* @return
*/
@Around(value="cut()")
public Object around(ProceedingJoinPoint pjp){
System.out.println("cut切入点 环绕通知的 前置消息");
Object result = null;
try{
// 调用目标方法
result = pjp.proceed();
// 获取目标对象
System.out.println(pjp.getTarget() + "===" + pjp.getSignature());
System.out.println("cut切入点 环绕通知的 返回消息");
}
catch(Exception e){
e.printStackTrace();
System.out.println("cut切入点 环绕通知的 异常消息");
}
catch(Throwable throwable){
throwable.printStackTrace();
}finally{
System.out.println("cut切入点 环绕通知的 最终消息");
}

return result;
}
}

测试

UserServiceTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;

import static org.junit.Assert.assertTrue;

import org.example.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class UserServiceTest
{
@Test
public void test(){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = ac.getBean("userService", UserService.class);
userService.test("隔壁老王");
}
}

运行结果如图

使用配置文件实现Aop

与使用注解实现Aop的区别是把注解改成配置文件。

定义切面

LogCut2
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
package org.example.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

@Component
public class LogCut2 {
public void cut() {}

public void before() {
System.out.println("cut切入点的前置通知...");
}

public void afterReturn(){
System.out.println("cut切入点的返回通知...");
}

public void after(){
System.out.println("cut切入点的最终通知...");
}

public void afterThrow(Exception e){
System.out.println("cut切入点的异常通知..." + e.getMessage());
}


public Object around(ProceedingJoinPoint pjp){
System.out.println("cut切入点 环绕通知的 前置消息");
Object result = null;
try{
// 调用目标方法
result = pjp.proceed();
// 获取目标对象
System.out.println(pjp.getTarget() + "===" + pjp.getSignature());
System.out.println("cut切入点 环绕通知的 返回消息");
}
catch(Exception e){
e.printStackTrace();
System.out.println("cut切入点 环绕通知的 异常消息");
}
catch(Throwable throwable){
throwable.printStackTrace();
}finally{
System.out.println("cut切入点 环绕通知的 最终消息");
}

return result;
}
}

配置文件配置Aop

spring.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
30
31
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 设置自动扫描包 -->
<context:component-scan base-package="org.example"/>

<!-- aop 相关配置 -->
<aop:config>
<!-- 定义切面 -->
<aop:aspect ref="logCut2">
<!-- 定义切入点 -->
<aop:pointcut id="cut" expression="execution(* org.example.service..*.*(..))"/>

<!-- 定义通知 -->
<aop:before method="before" pointcut-ref="cut" />
<aop:after-returning method="afterReturn" pointcut-ref="cut"/>
<aop:after method="after" pointcut-ref="cut"/>
<aop:after-throwing method="afterThrow" throwing="e" pointcut-ref="cut"/>
<aop:around method="around" pointcut-ref="cut"/>
</aop:aspect>
</aop:config>
</beans>