AOP 的概念和入门
# 80.AOP 的概念和入门
AOP 是 Spring 中的另一个核心的概念,have fun !
# 什么是 AOP
AOP,全称 Aspect Oriented Programming, 面向切面编程。
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。
专业一点的说法:在软件业,AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
- AOP 的作用:在程序运行期间,不修改源码对已有方法进行增强。
- AOP 的优势:减少重复代码、提高开发效率、维护方便
- AOP 的实现方式:使用动态代理技术
在 Spring 中,我们通过配置(XML 和注解),来完成上一篇博客的案例,Spring 还会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
# AOP 相关术语
在继续学习之前,我们介绍和 AOP 相关的术语,这不仅仅对我们以后学习 Spring 有帮助,对于学习其他知识也有用。
Joinpoint(连接点): 所谓连接点是指那些被拦截到的点。在 Spring 中,这些点指的是方法,因为 Spring 只支持方法类型的连接点。
例如,接口是标准,则接口中的方法都是连接点。实现了接口的类,都有连接点(在本次案例中,service 类的所有方法就是连接点)
Pointcut(切入点): 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。即,不是一个类中的所有方法,都会被增强。即被增强的方法就是切入点。例如在动态代理中加个判断,如果是 test 方法就直接返回,即不增强 test 方法。service 类的所有方法就是连接点,但不一定都是切入点。
Advice(通知/增强): 所谓通知是指拦截到 Joinpoint 之后所要做的事情,也就是要增强的代码就是通知。根据代码的先后顺序,可以给通知分类:前置通知,后置通知,异常通知,最终通知,环绕通知。
前置通知就是在调用切入点之前要做的事情,例如开启事务。示例:
@Override//整个的invoke方法在执行就是环通知
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
txManager.beginTransaction(); //前置通知
rtVa = method.invoke(accountService, args);//在环绕通知中有明确的切入点方法调用。
txManager.commit(); //后置通知
return rtValue;
} catch (Exception e) {
txManager.rollback();
throw new RuntimeException(e); //异常通知
} finally {
txManager.release(); //最终通知
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Introduction(引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field(了解即可)
Target(目标对象): 代理的目标对象,被代理对象。
Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程。本例中,我们将事务控制加入到代理对象的过程,就叫织入。Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
Proxy(代理): 一个类被 AOP 织入增强后,就产生一个结果代理类。
Aspect(切面): 是切入点和通知(引介)的结合。就是在切入点的方法执行前,执行后、如果有异常,最终通知要做什么,这个配置过程就叫切面。
# 基于 XML 的 AOP
现在我们开始来使用 AOP,我们首先是通过 XML 来配置。
环境准备:我们可以将所有代码都删除,从头来搭建环境。
# 导入依赖
我们在 pom.xml 中引入一个新的依赖,该依赖用于我们后续写切入点表达式使用
<!-- 切入点表达式 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
2
3
4
5
6
# 新建 service 接口
package com.peterjxl.service;
public interface IAccountService {
void saveAccount();
void updateAccount(int i);
int deleteAccount();
}
2
3
4
5
6
7
我们现在仅仅是学习 AOP,因此并不会真的实现 CRUD
我们的方法签名也是不同的,一个是无返回值无参,一个是无返回值有参,一个是有返回值无参,后续如果有其他类型的方法签名,组合下就可以
# 新增 service 实现类
package com.peterjxl.service.impl;
import com.peterjxl.service.IAccountService;
public class AccountServiceImpl implements IAccountService {
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新" + i);
}
@Override
public int deleteAccount() {
return 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 新增日志类
我们新增一个日志类,该类是一个公用的类,service 的方法在执行之前,都可以调用日志类的方法,来记录日志(先不实现事务,而是简单地加个输出语句)
package com.peterjxl.utils;
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志,计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
*/
public void printLog() {
System.out.println("Logger类中的printLog方法开始记录日志了。。。");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置 AOP
新建 bean.xml,添加 AOP 的约束:之前我们已经演示过如何查找约束了,现在我们在文档中搜索 xmlns:aop
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
2
3
4
5
6
7
8
9
10
11
然后我们将 service 层交由 Spring 管理:
<bean id="accountService" class="com.peterjxl.service.impl.AccountServiceImpl"/>
接下来就是配置 AOP 了,我们首先将公用的通知 bean,也加入到 Spring 管理:
<!-- 配置Logger类 -->
<bean id="logger" class="com.peterjxl.utils.Logger"/>
2
然后使用 <aop:config>
标签表明开始 AOP 的配置
<aop:config>
</aop:config>
2
3
使用 <aop:aspect>
标签表明配置切面,该标签有两个属性:id 属性用于给切面提供一个唯一标识,ref 属性指定通知类
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
</aop:aspect>
</aop:config>
2
3
4
5
6
然后就是配置通知了,我们使用对应的标签来配置通知的类型,例如前置通知使用 <aop:before>
标签
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型 并且建立通知方法和切入点方法的对应关系 -->
<aop:before method="printLog" pointcut="execution(public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount())"/>
</aop:aspect>
</aop:config>
2
3
4
5
6
7
在通知类型的标签中,有如下属性:
- method 属性:用于指定 Logger 类中哪个方法是前置通知
- pointcut 属性:用于指定切入点表达式,该表达式的含义指定在哪个方法上切入
这里我们配置的是,在 service 实现类的 saveAccount 方法中,加一个前置通知,也就是调用打印日志的方法。
切入点表达式的写法说明:
- 关键字:
execution(表达式)
- 表达式的内容:访问修饰符 返回值 包名.类名.方法名(参数列表)。
- 本例中,
public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount()
就表示我们要增强的方法
# 新建测试类
package com.peterjxl.test;
import com.peterjxl.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* AOP的测试类
*/
public class AOPTest {
public static void main(String[] args) {
// 1. 获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2. 根据id获取Bean对象
IAccountService as = (IAccountService) ac.getBean("accountService");
// 3. 执行方法
as.saveAccount();
as.updateAccount(1);
as.deleteAccount();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行结果:
Logger类中的printLog方法开始记录日志了。。。
执行了保存
执行了更新1
2
3
可以看到能正常使用 AOP
# 切入点表达式的写法
我们可以看到,刚刚我们写的切入点表达式有点复杂,其实我们可以用通配符来简化。
标准的写法:
public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略:
void com.peterjxl.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,星号 * 表示一个或多个字符串,表示任意返回值
* com.peterjxl.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包,但是有几级包,就要写几个星号
* *.*.*.*.AccountServiceImpl.saveAccount())
也可以使用两个小数点 ..
,来表示当前包及其子包
* *..AccountServiceImpl.saveAccount())
类名和方法名都可以使用*来实现通配
* *..*.*())
参数列表:
- 可以直接写数据类型:基本类型直接写名称 int,引用类型写包名.类名的方式 java.lang.String
- 可以使用通配符表示任意类型,但是必须有参数
- 可以使用..表示有无参数均可,有参数可以是任意类型
因此有一个全通配写法:该表达式表示全部类的全部方法
* *..*.*(..)
因此如果我们这样配置 AOP:
<aop:before method="printLog" pointcut="execution( * *..*.*(..))"/>
运行结果:service 中的每个方法调用前,都执行了前置通知
Logger类中的printLog方法开始记录日志了。。。
执行了保存
Logger类中的printLog方法开始记录日志了。。。
执行了更新1
Logger类中的printLog方法开始记录日志了。。。
2
3
4
5
当然,我们一般都不会写全通配。实际开发中切入点表达式的通常写法:切到业务层实现类下的所有方法
* com.peterjxl.service.impl.*.*(..)
# 其他通知类型
现在我们来演示下后置通知,异常通知,最终通知,环绕通知等类型的增强
在日志类中新增方法:
package com.peterjxl.utils;
public class Logger {
public void printLog() {
System.out.println("Logger类中的printLog方法开始记录日志了。。。");
}
public void beforePrintLog() {
System.out.println("前置通知 Logger类中的beforePrintLog方法开始记录日志了。。。");
}
public void afterReturningPrintLog() {
System.out.println("后置通知 Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
public void afterThrowingPrintLog() {
System.out.println("异常通知 Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
public void afterPrintLog() {
System.out.println("最终通知 Logger类中的afterPrintLog方法开始记录日志了。。。");
}
}
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
在 bean.xml 也配置相关的通知:
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知:在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut="execution(public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount())"/>
<!--配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount())"/>
<!--配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount())"/>
<!--配置最终通知:无论切入点方法是否正常执行它都会在其后面执行-->
<aop:after method="afterPrintLog" pointcut="execution(public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount())"/>
</aop:aspect>
</aop:config>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
此时我们可以运行测试类,运行结果:
前置通知 Logger类中的beforePrintLog方法开始记录日志了。。。
执行了保存
后置通知 Logger类中的afterReturningPrintLog方法开始记录日志了。。。
最终通知 Logger类中的afterPrintLog方法开始记录日志了。。。
2
3
4
# 简化切入点表达式
可以看到,每一个通知中的 expression 都是重复的,我们可以抽取出来:
<!-- 配置切入点表达式 id属性:给切入点表达式起一个唯一标识,expression属性:指定切入点表达式 -->
<aop:pointcut id="pt1" expression="execution(public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount())"/>
2
此标签写在 <aop:aspect>
标签内部时,只能当前切面使用。还可以写在 <aop:aspect>
外面,此时就变成了所有切面可用。注意,如果要写在 <aop:aspect>
外面,需要在 aop:aspect
标签之前写好,而不能写在 <aop:aspect>
标签的下面,这是约束的顺序要求
然后其他通知类型,就可以引用这个表达式了:
<aop:before method="beforePrintLog" pointcut-ref="pt1"/>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"/>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"/>
<aop:after method="afterPrintLog" pointcut-ref="pt1"/>
2
3
4
完整代码:
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<aop:pointcut id="pt1" expression="execution(public void com.peterjxl.service.impl.AccountServiceImpl.saveAccount())"/>
<aop:before method="beforePrintLog" pointcut-ref="pt1"/>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"/>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"/>
<aop:after method="afterPrintLog" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
2
3
4
5
6
7
8
9
# 环绕通知
在演示环绕通知之前,我们先将其他的通知类型给注释掉,方便演示
在 logger 类中新建方法:
public void aroundPrintLog() {
System.out.println("环绕通知 Logger类中的aroundPrintLog方法开始记录日志了。。。");
}
2
3
在 bean.xml 中配置:
<aop:around method="aroundPrintLog" pointcut-ref="pt1"/>
此时我们运行,会发现切入点方法没有执行,而通知方法执行了。这是因为环绕通知,它的含义是让我们自己决定怎么增强方法,也就是自己决定怎么去“环绕”这个切入点。
环绕通知,和我们之前的动态代理有点类似,我们得明确的调用切入点方法!而在调用之前或之后,我们可以自己决定要做什么!在调用之前写的代码就是前置通知,在调用之后写的就是异常通知
Spring 框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法 proceed()
,此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,在程序执行时,Spring 框架会为我们提供该接口的实现类供我们使用。
我们来看一个实际的案例吧:
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到,这和我们自己实现动态代理的代码类似,环绕通知是 Spring 框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
注意:异常类型必须写 Throwable,而不能写 Exception,因为是会抛出一个更广泛的异常
# 源码
本项目已将源码上传到 GitHub (opens new window) 和 Gitee (opens new window) 上。并且创建了分支 demo11,读者可以通过切换分支来查看本文的示例代码