上一篇我们围绕了AOP中代理模式的使用,这篇我们将主要围绕AOP的相关术语介绍,以及重点围绕基于注解的AOP进行相关知识的概述和使用说明。
AOP的相关术语
-
切面(Aspect):切面是一个模块化的横切关注点,它包含了一组通知和切点。通常,切面用于定义横切关注点的行为,如日志记录、事务管理等。
-
通知(Advice):通知是切面在特定切点上执行的动作。在 Spring AOP 中,有以下几种类型的通知:
- 前置通知(Before Advice):在目标方法执行之前执行。
- 后置通知(After Advice):在目标方法执行之后执行,无论目标方法是否抛出异常。
- 返回通知(After Returning Advice):在目标方法成功执行并返回结果后执行。
- 异常通知(After Throwing Advice):在目标方法抛出异常后执行。
- 环绕通知(Around Advice):在目标方法执行前后都执行,并可以控制目标方法的执行。
-
切点(Pointcut):切点是指在应用程序中选择连接点的表达式。它定义了哪些方法或类应该被通知所影响。切点使用表达式语言来匹配连接点。
-
连接点(Join Point):连接点是在应用程序执行过程中可以插入切面的点。它可以是方法调用、方法执行、异常抛出等。
-
引入(Introduction):引入允许向现有的类添加新的方法或属性。它允许在不修改现有类的情况下,向类添加新的功能。
-
目标对象(Target Object):目标对象是被通知的对象,它包含了切面所要应用的方法。
-
代理(Proxy):代理是一个对象,它包装了目标对象,并拦截对目标对象的访问。代理可以在目标对象的方法执行前后添加额外的逻辑。
-
织入(Weaving):织入是将切面应用到目标对象并创建新的代理对象的过程。织入可以在编译时、类加载时或运行时进行。(可以称之为横切关注点)
这些术语的作用:
- 提供统一的概念和术语:术语提供了一套统一的概念和术语,使开发人员能够更好地理解和沟通。通过使用共同的术语,可以减少误解和混淆,提高团队协作效率。
- 定义和描述 AOP 的各个组成部分:术语用于定义和描述 AOP 的各个组成部分,如切面、通知、切点等。它们提供了一种标准的方式来描述和理解 AOP 的概念和实现。
- 指导开发人员理解和使用 Spring AOP 框架:术语帮助开发人员理解和使用 Spring AOP 框架。通过熟悉和理解这些术语,开发人员可以更好地使用 Spring AOP 提供的功能和特性,实现横切关注点的处理。
- 促进知识共享和学习:术语作为一种共享的语言,促进了知识的共享和学习。开发人员可以通过使用相同的术语来交流和分享经验,从而加深对 Spring AOP 的理解和应用。
基于注解的AOP
基于注解的 AOP 是一种使用注解来定义切面和通知的方式。在传统的基于 XML 配置的 AOP 中,切面和通知的定义通常是通过 XML 配置文件来完成的,而基于注解的 AOP 则使用注解来实现这些定义,使得配置更加简洁和直观。
-
动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
-
cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
-
AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
常见的注解:
@Aspect
:用于定义切面类,标识该类为切面。@Before
:用于定义前置通知,在目标方法执行之前执行。@After
:用于定义后置通知,在目标方法执行之后执行,无论目标方法是否抛出异常。@AfterReturning
:用于定义返回通知,在目标方法成功执行并返回结果后执行。@AfterThrowing
:用于定义异常通知,在目标方法抛出异常后执行。@Around
:用于定义环绕通知,在目标方法执行前后都执行,并可以控制目标方法的执行。
案例
导入相关依赖
<?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">
<parent>
<artifactId>SSM</artifactId>
<groupId>com.miaow</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Spring-AOP</artifactId>
<description>Spring的AOP学习</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
</dependency>
<!-- 引入AOP,实际上就是spring-aspect会帮我传递过来aspectjweaver-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.8</version>
</dependency>
</dependencies>
</project>
创建接口实现注解横切
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部,result:"+result);
return result;
}
}
@Component
@Aspect //将当前组件标识为切面
public class LoggerAspect {
@Pointcut("execution(* com.miaow.aspect.CalculatorImpl.*(..))")
public void pointCut(){}
//@Before("execution(public int com.miaow.aspect.CalculatorImpl.add(int, int))")
//@Before("execution(* com.miaow.aspect.CalculatorImpl.*(..))")
@Before("pointCut()")
public void beforeAdviceMethod(JoinPoint joinPoint) {
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
//获取连接点所对应方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+ Arrays.toString(args));
}
@After("pointCut()")
public void afterAdviceMethod(JoinPoint joinPoint){
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",执行完毕");
}
/**
* 在返回通知中若要获取目标对象方法的返回值
* 只需要通过@AfterReturning注解的returning属性
* 就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数
*/
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result){
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",结果:"+result);
}
/**
* 在异常通知中若要获取目标对象方法的异常
* 只需要通过AfterThrowing注解的throwing属性
* 就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数
*/
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable ex){
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",异常:"+ex);
}
//使用了环绕通知就不需要使用上述四种通知,如果选择上面的四种通知,那么就不需要选择下面的环绕通知
@Around("pointCut()")
//环绕通知的方法的返回值一定要和目标对象方法的返回值一致
public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("环绕通知-->前置通知");
//表示目标对象方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知-->返回通知");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->异常通知");
} finally {
System.out.println("环绕通知-->后置通知");
}
return result;
}
}
/**
* 这段代码定义了一个名为 ValidateAspect 的 Aspect,它使用 @Order 注解将其优先级设置为 1,并使用 @Before 注解定义一个前置通知方法。
*/
@Component
@Aspect
@Order(1) //order=1值越小,优先级越高
public class ValidateAspect {
//@Before("execution(* com.miaow.aspect.CalculatorImpl.*(..))")
@Before("com.miaow.aspect.LoggerAspect.pointCut()")
public void beforeMethod(){
System.out.println("ValidateAspect-->前置通知");
}
}
在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:context="http://www.springframework.org/schema/context"
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/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">
<!--
基于注解的AOP的实现:
1、将目标对象和切面交给IOC容器管理(注解+扫描)
2、开启AspectJ的自动代理,为目标对象自动生成代理
3、将切面类通过注解@Aspect标识
-->
<!-- 扫码component文件-->
<context:component-scan base-package="com.miaow.aspect"></context:component-scan>
<!-- 启用基于注解AOP,否则实现不了-->
<aop:aspectj-autoproxy />
</beans>
测试类:
@Test
public void test2(){
//先获取IOC
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");
//当我们使用AOP之后无法直接获取对象,只能通过动态代理的获取
com.miaow.aspect.Calculator calculator = context.getBean(com.miaow.aspect.Calculator.class);
// calculator.add(1,2);
// calculator.sub(1,2);
// calculator.mul(1,2);
calculator.div(1,2);
}
运行结果:
ValidateAspect-->前置通知
环绕通知-->前置通知
LoggerAspect,方法:div,参数:[1, 2]
方法内部,result:0
LoggerAspect,方法:div,结果:0
LoggerAspect,方法:div,执行完毕
环绕通知-->返回通知
环绕通知-->后置通知
补充说明AOP中的切面优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
在基于注解的 AOP 中,切面的优先级可以通过以下方式进行控制:
- @Order 注解:可以在切面类上使用 @Order 注解来指定切面的优先级。@Order 注解的值越小,优先级越高。如果多个切面都使用了 @Order 注解,则优先级较高的切面将先于优先级较低的切面执行。
@Aspect
@Order(1)
public class MyAspect1 {
// ...
}
@Aspect
@Order(2)
public class MyAspect2 {
// ...
}
- 实现 Ordered 接口:可以让切面类实现 Ordered 接口,并实现其中的 getOrder() 方法来指定切面的优先级。getOrder() 方法返回的值越小,优先级越高。
@Aspect
public class MyAspect implements Ordered {
// ...
@Override
public int getOrder() {
return 1;
}
}
- @Order 和 Ordered 接口的组合使用:可以同时使用 @Order 注解和实现 Ordered 接口的方式来控制切面的优先级。当切面类既使用了 @Order 注解,又实现了 Ordered 接口时,@Order 注解的优先级高于 Ordered 接口的 getOrder() 方法返回值。
@Aspect
@Order(1)
public class MyAspect implements Ordered {
// ...
@Override
public int getOrder() {
return 2;
}
}
需要注意的是,切面的优先级仅在多个切面同时应用于同一个连接点时才会起作用。如果切面应用于不同的连接点,优先级的设置将不会生效。
了解基于XML的AOP操作
在基于 XML 的 Spring AOP 中,切面和通知的定义通常是通过 XML 配置文件来完成的。以下是基于 XML 的 Spring AOP 的配置步骤:
- 定义切面类:创建一个切面类,其中包含了切面的逻辑和通知的定义。
- 创建 XML 配置文件:创建一个 XML 配置文件,用于定义切面和通知的关系。
- 配置切面和通知:在 XML 配置文件中,使用
<aop:config>
元素来配置切面和通知。 - 定义切点:使用
<aop:pointcut>
元素来定义切点,切点用于匹配连接点。 - 配置通知:使用
<aop:advisor>
元素来配置通知,将切面和切点关联起来。 - 引入其他配置:根据需要,可以在 XML 配置文件中引入其他的配置,如引入目标对象、引入代理等。
基于XML的AOP操作展示
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部,result:"+result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部,result:"+result);
return result;
}
}
@Component
@Aspect //将当前组件标识为切面
public class LoggerAspect {
public void pointCut(){}
public void beforeAdviceMethod(JoinPoint joinPoint) {
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
//获取连接点所对应方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+ Arrays.toString(args));
}
public void afterAdviceMethod(JoinPoint joinPoint){
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",执行完毕");
}
/**
* 在返回通知中若要获取目标对象方法的返回值
* 只需要通过@AfterReturning注解的returning属性
* 就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数
*/
public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result){
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",结果:"+result);
}
/**
* 在异常通知中若要获取目标对象方法的异常
* 只需要通过AfterThrowing注解的throwing属性
* 就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数
*/
public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable ex){
//获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",异常:"+ex);
}
public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("环绕通知-->前置通知");
//表示目标对象方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知-->返回通知");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->异常通知");
} finally {
System.out.println("环绕通知-->后置通知");
}
return result;
}
}
@Component
public class ValidateAspect {
public void beforeMethod(){
System.out.println("ValidateAspect-->前置通知");
}
}
XML文件配置
<?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:c="http://www.springframework.org/schema/c"
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/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="com.miaow.xml"></context:component-scan>
<aop:config >
<!-- 将IOC容器的bean设置为切面 @Component 自动将我们标记的类设置为类名小字母开头-->
<!-- 设置一个共同的切入点表达式-->
<aop:pointcut id="pointCut" expression="execution(* com.miaow.xml.CalculatorImpl.*(..))"/>
<aop:aspect ref="loggerAspect">
<aop:before method="beforeAdviceMethod" pointcut-ref="pointCut"></aop:before>
<aop:after method="afterAdviceMethod" pointcut-ref="pointCut"></aop:after>
<aop:after-returning method="afterReturningAdviceMethod" returning="result" pointcut-ref="pointCut"></aop:after-returning>
<aop:after-throwing method="afterThrowingAdviceMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing>
<aop:around method="aroundAdviceMethod" pointcut-ref="pointCut"></aop:around>
</aop:aspect>
<!-- 设置优先级-->
<aop:aspect ref="validateAspect" order="1">
<aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
</aop:aspect>
</aop:config>
</beans>
测试类
public class AOPByXMLTest {
@Test
public void testAop(){
ApplicationContext context = new ClassPathXmlApplicationContext("Spring-aop-xml.xml");
Calculator calculator = (Calculator) context.getBean(Calculator.class);
calculator.add(1,2);
}
}
ValidateAspect-->前置通知
LoggerAspect,方法:add,参数:[1, 2]
环绕通知-->前置通知
方法内部,result:3
环绕通知-->返回通知
环绕通知-->后置通知
LoggerAspect,方法:add,结果:3
LoggerAspect,方法:add,执行完毕
通过 aop:config 元素配置了切面和通知,使用 aop:pointcut 元素定义了切点,使用 aop:before、aop:after、aop:after-returning、aop:after-throwing、aop:around 元素配置了不同类型的通知。
通过基于 XML 的配置,可以灵活地定义和配置切面和通知,实现对目标对象的横切关注点的处理。