文章目录
Spring 中的 AOP
1. Spring 中 AOP 的细节
1.1 AOP 相关术语
-
Joinpoint (连接点):
- 所谓连接点是指那些被拦截到的点。在 Spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
- 比如之前写的小例子中的业务层的 dao 接口中的所有方法,就是jointpoint,因为这些方法是连接业务和增强方法之间的桥梁
- 所谓连接点是指那些被拦截到的点。在 Spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
-
Pointcut(切入点):
- 所谓切入点是指我们要对哪些 Jointpoint 进行拦截的定义。
- 还是之前写的小例子,如果业务层中存在一种方法,并不需要被代理类增强方法,那么这个方法便不是切入点。那么这个方法是不是连接点?一定是。
- 所以,所有的切入点都是连接点,而连接点,不一定都是切入点。
- 所谓切入点是指我们要对哪些 Jointpoint 进行拦截的定义。
-
Advice(通知/增强):
- 所谓通知是指拦截到 Jointpoint 之后所要做的事情就是通知。
- 动态代理类中的 invoke 方法具有拦截方法的功能,在拦截方法之后,需要对方法进行增强,那么对方法的增强,就称为通知。
- 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
- 在动态代理中,整个 invoke 方法就是环绕通知,在环绕通知中,有明确的切入点方法调用
- 在环绕通知中,找到对切入点方法调用(增强):
- 在该方法前,是前置通知
- 在该方法后,是后置通知
- 在对异常的处理中,是异常通知
- 在 finally 语句块中,是最终通知
- 通知方法,就是我们用来增强目标对象的类中的方法。
- 所谓通知是指拦截到 Jointpoint 之后所要做的事情就是通知。
-
Introduction(引介):
- 引介是一种特殊的通知,在不修改类代码的前提下,Introduction 可以在运行期为类动态地添加一些方法或 Field。
-
Target(目标对象):
- 代理的目标对象
-
Weaving(织入):
- 是指把增强应用到目标对象来创建新的代理类对象的过程。
- 我们在新增业务层的事务的支持的时候,采用动态代理的方式。在代理过程中,对业务新增了事务的支持,这个过程,就叫做织入。
- spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入
- 是指把增强应用到目标对象来创建新的代理类对象的过程。
-
Proxy(切面):
- 一个类被 AOP 织入增强后,就产生一个结果代理类。
-
Aspect
- 是切入点和通知(引介)的结合。
- 通知方法和切入点之间的关系,通知方法何时执行,怎样执行,如何去配置他们之间的关系,而配置好的他们之间的关系,就叫做切面。
- 是切入点和通知(引介)的结合。
1.2 学习 spring 中的 AOP 要明确的事
- 开发阶段(我们做的)
- 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
- 把公共代码抽取出来,制成通知(开发阶段最后再做):团队中拥有技术比较牛的人员来做。
- 在配置文件中,声明切入点与通知间的关系,即切面:团队中拥有技术比较牛的人员来做。
- 运行阶段(Spring 框架完成的)
- Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对象的功能织入,完成完整的代码逻辑运行。
2. spring 基于 XML 的 AOP
2.1 前提代码:
-
创建一个新的 maven 工程。
-
在maven工程下,创建以下几个源包:
-
创建以下几个类:
-
IAccountService 类,用来模拟业务层:
package com.self.learning.service; /** * 账户的业务层接口 */ public interface IAccountService { /** * 模拟保存账户, 无返回值无参 */ void saveAccount(); /** * 模拟更新账户, 无返回值有参 * @param i */ void updateAccount(int i); /** * 模拟删除账户, 有返回值无参 * @return */ int deleteAccount(); }
-
AccountServiceImpl 类,用来模拟业务层的实现类:
package com.self.learning.service.impl; import com.self.learning.service.IAccountService; /** * 账户的业务层实现类 */ public class AccountServiceImpl implements IAccountService { @Override public void saveAccount() { System.out.println("执行了保存"); } @Override public void updateAccount(int i) { System.out.println("执行了更新"); } @Override public int deleteAccount() { System.out.println("执行了删除"); return 0; } }
-
模拟一个业务层的日志文件:
package com.self.learning.utils; /** * 由于记录日志的工具类,它里面提供了公共的代码 */ public class Logger { /** * 用于打印日志:计划让其在切入点方法在执行前执行(切入点方法就是业务层方法) */ public void printLog(){ System.out.println("Logger 类中的 pringLog 方法开始执行日志了。"); } }
-
测试类,使用 Spring 整合了 junit4,我使用的Spring的版本是Spring5,因此要求junit的版本大于 4.12:
package com.self.learning.spring.test; import com.self.learning.service.IAccountService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * 测试 AOP 的配置 */ @RunWith(value = SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:bean.xml") public class AOPTest { @Autowired IAccountService accountService; @Test public void test() { accountService.saveAccount(); accountService.updateAccount(4); accountService.deleteAccount(); } }
2.2 Spring 中基于 XML 的 AOP 配置步骤
-
把通知的 bean 也交给 Spring 来管理
-
使用 aop:config 标签表名开始 AOP 配置
-
使用 aop:aspect 标签名配置切面
- id属性:是给切面提供一个唯一标识
- ref属性:是指定通知类 bean 的 Id。
-
在 aop:aspect 标签的内部使用对应的标签来配置通知的类型。
-
aop:before 标签:
-
表示配置前置通知
-
method 属性:用于指定 Logger 类中,哪个方法是前置通知
-
pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层中哪些方法增强。
-
切入点表达式的写法:
-
关键字:execution(表达式)
-
表达式:
-
访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
-
标准的表达式写法:
public void com.self.learning.service.impl.AccountServiceImpl.saveAccount()
-
访问修饰符可以省略:
void com.self.learning.service.impl.AccountServiceImpl.saveAccount()
-
返回值可以使用通配符表示任意返回值:
* com.self.learning.service.impl.AccountServiceImpl.saveAccount()
-
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个 *:
* *.*.*.*.*.AccountServiceImpl.saveAccount()
-
包名可以使用 … 表示当前包和子包:
* *..AccountServiceImpl.saveAccount()
-
类名和方法名都可以使用 * 来实现通配:
* *..*.*()
-
参数列表可以直接写数据类型:
-
基本类型直接写名称,
-
引用类型写包名.类名的方式:java.lang.String
* *..*.*(int)
-
类型可以使用通配符表示任意类型,但是必须要有参数
-
可以使用 … 表示有无参数均可,有参数可以是任意类型
-
-
全通配写法 —— 实际开发不适用,因为类都符合这种通配类型,在运行时,代码都会被增强一次:
* *..*.*(..)
-
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有的方法:
* com.self.learning.service.impl.*.*(..)
-
-
-
-
2.3 Spring 配置文件的配置工作:
我这里的 Spring 的配置文件名字是 bean.xml。
首先,以下是 bean 的约束:
<?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">
</beans>
-
首先是配置 Spring 的 IOC,把 Service 对象配置进来:
<bean id="accountService" class="com.self.learning.service.impl.AccountServiceImpl"></bean>
-
然后是,配置 Logger 类:
<bean id="logger" class="com.self.learning.utils.Logger"></bean>
-
接下来是配置 AOP:
<!-- 配置 AOP --> <aop:config> <!-- 配置切面 --> <aop:aspect id="logAdvice" ref="logger"> <!-- 配置通知类型,并且建立通知方法和切入点方法的关联 --> <aop:before method="printLog" pointcut="execution(public void com.self.learning.service.impl.AccountServiceImpl.saveAccount())"></aop:before> </aop:aspect> </aop:config>
-
最后,在测试类中运行,以下是结果
Logger 类中的 pringLog 方法开始执行日志了。 执行了保存 Logger 类中的 pringLog 方法开始执行日志了。 执行了更新 Logger 类中的 pringLog 方法开始执行日志了。 执行了删除
2.4 四种常见的通知类型和通用化表达式
四种常见的通知类型明别为:
前置通知,后置通知,异常通知,最终通知:
package com.self.learning.utils;
/**
* 由于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 前置通知:在切入点方法执行之前执行
*/
public void beforePrintLog(){
System.out.println("前置通知 Logger 类中的 beforePrintLog 方法开始执行日志了。");
}
/**
* 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个。
*/
public void afterReturningPrintLog(){
System.out.println("后置通知 Logger 类中的 afterReturningPrintLog 方法开始执行日志了。");
}
/**
* 异常通知:在切入点方法执行产生异常之后,执行。它和后置通知永远只能执行一个。
*/
public void afterThrowingLog(){
System.out.println("异常通知 Logger 类中的 afterThrowingLog 方法开始执行日志了。");
}
/**
* 最终通知:无论切入点方法是否正常执行,都会在其后执行
*/
public void afterPringLog(){
System.out.println("最终通知 Logger 类中的 afterPringLog 方法开始执行日志了。");
}
}
其中:
- 前置通知:在切入点方法执行之前执行
- 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个。
- 异常通知:在切入点方法执行产生异常之后,执行。它和后置通知永远只能执行一个。
- 最终通知:无论切入点方法是否正常执行,都会在其后执行
在 Spring 配置文件中的配置为:
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切入点表达式 id 属性用于指定表达式的唯一标志, expression 用于指定表达式内容
-->
<aop:pointcut id="pt1" expression="execution(* com.self.learning.service.impl.*.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知 -->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<!-- 配置后置通知 -->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<!-- 配置异常通知 -->
<aop:after-throwing method="afterThrowingLog" pointcut-ref="pt1"></aop:after-throwing>
<!-- 配置最终通知 -->
<aop:after method="afterPringLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
其中,asp:pointcut 为通用化表达式,用来替换 execute 表达式。
以下是运行结果(同样只是运行 accountService 方法):
前置通知 Logger 类中的 beforePrintLog 方法开始执行日志了。
执行了保存
后置通知 Logger 类中的 afterReturningPrintLog 方法开始执行日志了。
最终通知 Logger 类中的 afterPringLog 方法开始执行日志了。
这里要注意:
aop:pointcut 可以写在 aop:aspect 内部,也可以写在 aop:aspect 外部。
如果写在内部,只能当前的 aop:aspect 标签使用。如果写在外部,根据约束,只能写在 aop:config 标签内部,同时写在 aop:aspect 标签前面。
2.5 环绕通知
首先,创建一个环绕通知:
public voidaroundLog(ProceedingJoinPoint pjp) {
System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。");
}
接下来在配置文件中进行配置:
<aop:pointcut id="pt1" expression="execution(* com.self.learning.service.impl.*.*(..))"/>
<!-- 配置环绕通知 -->
<aop:around method="aroundLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
然后,在测试类中进行测试:
import com.self.learning.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* 测试 AOP 的配置
*/
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AOPTest {
@Autowired
IAccountService accountService;
@Test
public void test() {
accountService.saveAccount();
}
}
结果为:
Logger 类中的 aroundPringLog 方法开始执行日志了。
发现并不会打印:执行了保存
。也就是并没有执行业务层的代码
这是什么原因呢?
我们都知道 Spring 中的方法增强实际上就是使用代理类,看一下下面的图:
我们会发现,实际上,我们的环绕方法中,缺少一个明确的切入点方法。
这个问题,是通过 Spring 框架为我们提供的一个接口来解决的,这个接口是 ProceedingJoinPoint,这个接口中有一个方法 proceed,此方法就相当于明确调用切入点方法。
所以,我们的环绕通知方法应该改成这个样子:
public Object aroundLog(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 throwable) {
System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。异常");
throw new RuntimeException(throwable);
} finally {
System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。最终");
}
}
由于 proceed 方法是有 Object 返回值的,所以需要创建一个 Object 对象来接受,并返回。而调用 proceed 方法,也就明确了切入点方法。
然后:
- 当增强方法出现在 proceed 方法前,就是前置通知
- 出现在 proceed 方法后,就是后置通知
- 出现在 catch 语句块中,就是一场方法。这里注意,它抛出的异常是一个 throwable,使用 Exception 捕获不住。
- 出现在 final 语句块中,就是最终方法
以下是运行结果:
Logger 类中的 aroundPringLog 方法开始执行日志了。前置
执行了保存
Logger 类中的 aroundPringLog 方法开始执行日志了。后置
Logger 类中的 aroundPringLog 方法开始执行日志了。最终
2.6 Spring 基于注解 AOP 配置
首先,看一下当前的 Spring 配置文件:
<?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">
<!-- 配置 Spring 的 IOC,把 Service 对象配置进来 -->
<bean id="accountService" class="com.self.learning.service.impl.AccountServiceImpl"></bean>
<!-- 配置 Logger 类 -->
<bean id="logger" class="com.self.learning.utils.Logger"></bean>
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切入点表达式 id 属性用于指定表达式的唯一标志, expression 用于指定表达式内容
此标签写在 aop:aspect 内部,只能当前切面使用。
它还可以写在 aop:aspect 外部,此时就变成了所有切面可用。
-->
<aop:pointcut id="pt1" expression="execution(* com.self.learning.service.impl.*.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知 -->
<!-- <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>-->
<!-- 配置后置通知 -->
<!-- <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>-->
<!-- 配置异常通知 -->
<!-- <aop:after-throwing method="afterThrowingLog" pointcut-ref="pt1"></aop:after-throwing>-->
<!-- 配置最终通知 -->
<!-- <aop:after method="afterLog" pointcut-ref="pt1"></aop:after>-->
<!-- 配置环绕通知 -->
<aop:around method="aroundLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
里面配置了:
- 切入点表达式
- 配置了切面
- 配置了环绕通知
- 配置了4中常用通知
- 配置了 id 为 accountService 的 bean 对象
- 配置了 Logger 类
所以,现在一个个使用注解来代替其中的配置:
首先是 AccountService 接口类的 bean 对象:
@Component("accountService")
public class AccountServiceImpl implements IAccountService {
然后是 Logger 类的 bean 对象:
@Component("logger")
@Aspect // 表示当前类是一个切面类
public class Logger {
然后配置切入点表达式(在 Logger 类中):
@Pointcut("execution(* com.self.learning.service.impl.*.*(..))")
private void pt() {}
最后配置 4 中常用通知和环绕通知(在 Logger 类中):
/**
* 前置通知:在切入点方法执行之前执行
*/
// @Before("pt()")
public void beforePrintLog(){
System.out.println("前置通知 Logger 类中的 beforePrintLog 方法开始执行日志了。");
}
/**
* 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个。
*/
// @AfterReturning("pt()")
public void afterReturningPrintLog(){
System.out.println("后置通知 Logger 类中的 afterReturningPrintLog 方法开始执行日志了。");
}
/**
* 异常通知:在切入点方法执行产生异常之后,执行。它和后置通知永远只能执行一个。
*/
// @AfterThrowing("pt()")
public void afterThrowingLog(){
System.out.println("异常通知 Logger 类中的 afterThrowingLog 方法开始执行日志了。");
}
/**
* 最终通知:无论切入点方法是否正常执行,都会在其后执行
*/
// @After("pt()")
public void afterLog(){
System.out.println("最终通知 Logger 类中的 afterPringLog 方法开始执行日志了。");
}
@Around("pt()")
public Object aroundLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs(); // 得到方法执行所需的参数
System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。前置");
rtValue = pjp.proceed(args); // 明确调用业务层方法,也叫切入点方法
// int j = 1/0;
System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。后置");
return rtValue;
} catch (Throwable throwable) {
System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。异常");
throw new RuntimeException(throwable);
} finally {
System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。最终");
}
}
最后不要忘记,因为使用的是注解代替xml配置,需要在配置文件中开启注解配置通知:
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
如果使用的是注解来标注 4 种常用的通知,那么执行顺序会产生问题:
前置通知 Logger 类中的 beforePrintLog 方法开始执行日志了。
最终通知 Logger 类中的 afterPringLog 方法开始执行日志了。
后置通知 Logger 类中的 afterReturningPrintLog 方法开始执行日志了。
当我们执行之后,会发现,最终通知在后置通知之上,这时因为 Spring 框架在使用AOP注解进行事务管理时,在调用顺序上,后置通知后与最终通知。而如果使用环绕通知则没有这个问题。
如果要使用注解类而不是配置文件,在注解类上加上以上的注解:
@Configuration
@ComponenetScan(basePackage = "com.self.learning")
@EnableAspectJAutoProxy
public class SpringConfiguration {}