Spring-面向切面编程AOP
# Spring-面向切面编程AOP
摘要
本文介绍了 AOP 的概念、作用和实现原理,从 OOP 的弊端引入 AOP,突出 AOP 的作用;
基于 Spring 通过 xml 文件配置 AOP、基于 Springboot 通过注解配置 AOP 并了解 AOP 常用注解以及通知顺序。
# AOP概念
概念:通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件开发中的一个热点,也是Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
本质:Spring 的动态代理开发,通过代理类为原始类增加额外功能。能够将那些与业务无关,却为业务模块服务的功能(事务管理、日志管理、权限管理)封装隔离起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的拓展和维护。
作用:在程序运行期间,不修改代码就可对已有方法进行增强。
优势:减少重复代码、提高开发效率、维护方便
原理:基于动态代理,使用 JDK Proxy 或 Cglib 代理。JDK Proxy 实现接口创建代理对象,Cglib 生成一个被代理对象的子类作为代理。
步骤:原始功能、额外功能、切入点、组装切面(额外功能 + 切入点)
注意:AOP 编程不可能取代 OOP,AOP 是 OOP 编程的补充。
对比
⭐ AOP (Aspect Oriented Programing)
- 面向切面编程 = Spring动态代理开发
- 以切面为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建。
- 切面 = 切入点 + 额外功能
⭐ POP (Procedure Oriented Programing)
- 面向过程(方法、函数)编程 —— C
- 以过程为基本单位的程序开发,通过过程间的彼此协同,相互调用,完成程序的构建。
⭐ OOP (Object Oritened Programing)
- 面向对象编程 —— Java、C++、python
- 以对象为基本单位的程序开发,通过对象间的彼此协同,相互调用,完成程序的构建。
# AOP理解
参考知乎回答:什么是面向切面编程AOP?
Java在面向对象的世界里无限风光,OOP 成就了无数经典的软件,它让我们的软件更健壮、更易于扩展和维护,但程序猿对软件质量追求是永无止境的,OOP 虽然对业务的抽象和封装无懈可击。
但是对于系统日志,性能统计、异常处理等分布在软件各个角落的系统层面的需求,维护起来十分不便,这类问题 OOP 难以解决,于是AOP 横空出世。
OOP 使软件更加健壮,易于扩展和维护;AOP 不修改代码就可对已有方法进行增强。
# OOP案例
先来看一个简单的小例子,体会一下 OOP 所遇到的难题。如果我们要在 UserService 的每个方法里都加入额外功能 doMethod()
。
public class UserService {
public void register(User user) {
doMethod(); // 添加额外功能
System.out.println("UserServiceImpl.register 业务运算 + DAO");
}
public boolean login(String name, String password) {
doMethod(); // 添加额外功能
System.out.println("UserServiceImpl.login 业务运算 + DAO");
return true;
}
...
// 下面还有9999个方法等待你添加doMethod()方法...
// 改完后,突然有一天业务不再需要这个额外功能了...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们可以直接在每个方法中直接调用额外功能doMethod()
,但这样做不是很好。因为额外功能doMethod()
并不是我们的核心业务;随着系统越来越完善,类似这样的非核心业务也会越来越多,比如权限,异常处理,性能监控等。这样的功能出现在很多类的很多方法中干扰了我们的核心业务代码,怎么解决呢?AOP就是为此而生,通过AOP就可以统一的添加额外功能。
# AOP相关术语
Spring 框架通过使用基于XML配置文件的方法或 @AspectJ
注释形式提供了编写自定义切面的简单而强大的方法。
连接点(Join point)
:表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点,在AOP中表示为在哪里干;切入点(Pointcut)
:选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为在哪里干的集合;增强/通知(Advice)
:在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知;在AOP中表示为干什么;切面(Aspect)
:切入点和通知(引介)的结合。在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为在哪干和干什么集合;引介(Introduction)
:这是一种特殊的通知,可以在不修改类代码的前提下在运行期动态地对类添加一些方法或成员变量;目标对象(Target object)
:是指代理的目标对象;AOP代理(AOP proxy)
:指一个类被AOP织入增强后就产生一个结果代理类;织入(Weaving)
:指把增强应用到目标对象来创建新的代理对象的过程;
# 基于xml的AOP
注意
基于xml配置很啰嗦,可以跳过,直接看 Springboot 基于注解的 AOP。
我可以将日志记录,性能监控,异常处理这样的非核心功能单独被抽取出来,与业务代码分离,横切在核心业务代码之上。这就是我们通常所说的面向切面编程(AOP),通过一个例子看看他是如何实现的:
⭐ 配置依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
⭐ 业务类:主要功能
public class UserService {
public void register(User user) {
System.out.println("UserServiceImpl.register 业务运算 + DAO");
}
public boolean login(String name, String password) {
System.out.println("UserServiceImpl.login 业务运算 + DAO");
return true;
}
}
2
3
4
5
6
7
8
9
10
⭐ 额外功能:实现MethodBeforeAdvice
接口
public class Before implements MethodBeforeAdvice {
/**
* 作用: 把需要运行在原始方法执行之前运行的额外功能, 书写在 before 方法中
*/
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("---method before advice log---");
}
}
2
3
4
5
6
7
8
9
⭐ 定义切入点
由程序员根据自己的需要,决定额外功能加入给哪个原始方法(register、login)
<!-- 简单的测试:所有方法都做为切入点,都加入额外的功能-->
<aop:config>
<aop:pointcut id="pc" expression="execution(* * (..))"/>
</aop:config>
2
3
4
⭐ applicationContext.xml完整配置:额外功能 + 定义切入点
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="com.zqc.aop.UserService"/>
<!-- 额外功能 -->
<bean id="before" class="com.zqc.aop.Before"/>
<!--切入点:额外功能的加入-->
<!--目的:由程序员根据自己的需要,决定额外功能加入给哪个原始方法(register、login)-->
<!-- 简单的测试:所有方法都做为切入点,都加入额外的功能-->
<aop:config>
<aop:pointcut id="pc" expression="execution(* * (..))"/>
<!--表达的含义: 所有的方法 都加入before的额外功能-->
<aop:advisor advice-ref="before" pointcut-ref="pc"/>
</aop:config>
</beans>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
⭐ 调用
/**
* 用于测试动态代理
*/
@Test
public void test1() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = (UserService) ctx.getBean("userService");
userService.login("admin", "1234");
userService.register(new User());
}
2
3
4
5
6
7
8
9
10
11
我们成功将额外方法加入到了 UserService的
login()和
register()` 方法中。
这就是 Spring AOP 的强大之处,在运行时通过动态代理技术对 UserService的 login()
和register()
方法进行了增强,添加了打印日志的功能。
# 基于注解的AOP
引入依赖:这里使用的使 Springboot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2
3
4
5
6
7
8
创建目标对象:主体功能,被代理的对象
@Service
public class CalServiceImpl implements CalService {
@Override
public int cal(int x, int y) {
int result = x /y;
System.out.println("CalServiceImpl方法被调用,计算结果为:" + result);
return result;
}
}
2
3
4
5
6
7
8
9
配置切面类:额外功能,配置了 5 种通知方式,后面将介绍这五种方式。
@Aspect
@Component
public class MyAspect {
@Before("execution(public int com.zqc.service.CalServiceImpl.*(..))")
public void beforeNotify() {
System.out.println("@Before 我是前置通知MyAspect!");
}
@After("execution(public int com.zqc.service.CalServiceImpl.*(..))")
public void afterNotify() {
System.out.println("@After 我是后置通知MyAspect!");
}
@AfterReturning("execution(public int com.zqc.service.CalServiceImpl.*(..))")
public void afterReturningNotify() {
System.out.println("@AfterReturning 我是返回后通知MyAspect!");
}
@AfterThrowing("execution(public int com.zqc.service.CalServiceImpl.*(..))")
public void afterThrowingNotify() {
System.out.println("@AfterThrowing我是异常通知MyAspect!");
}
@Around("execution(public int com.zqc.service.CalServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object retValue = null;
System.out.println("@Around 我是环绕通知之前!");
retValue = proceedingJoinPoint.proceed();
System.out.println("@Around 我是环绕通知之后!");
return retValue;
}
}
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
测试
@SpringBootTest
class SpringbootApplicationTests {
@Resource
private CalService calService;
@Test
void contextLoads() {
calService.cal(1, 2); // 正常输出
calService.cal(1, 0); // 导致异常输出
}
}
2
3
4
5
6
7
8
9
10
11
正常输出:
@Around 我是环绕通知之前!
@Before 我是前置通知MyAspect!
CalServiceImpl方法被调用,计算结果为:0
@Around 我是环绕通知之后!
@After 我是后置通知MyAspect!
@AfterReturning 我是返回后通知MyAspect!
2
3
4
5
6
异常输出:
@Around 我是环绕通知之前!
@Before 我是前置通知MyAspect!
@After 我是后置通知MyAspect!
@AfterThrowing我是异常通知MyAspect!
2
3
4
# AOP常用注解
@Before:前置通知,目标方法之前执行
@After:后置通知,目标方法之后执行
@AfterReturning:返回通知:执行方法结束前执行(异常不执行)
@AfterThrowing:异常通知:出现异常时候执行
@Around:环绕通知,环绕目标方法执行,可以用于分析程序执行的时间
# AOP的全部通知顺序
正常执行顺序:@Around前、@Before、业务、@Around后、@After、@AfterReturning
异常执行顺序:@Around前、@Before、@After、@AfterThrowing