当前位置: 首页 > news >正文

Spring学习笔记:Spring AOP入门以及基于Spring AOP配置的深入学习与使用

前面是IoC,就是把需要的bean装载到IoC容器中。跟前面的IoC一样,先是XML再注解。

AOP的概念以及基于XML的核心,以及基于XML的核心Spring AOP机制的配置和使用。
包括Spring AOP的开启,< aop:config >,<.aop:aspect >,<.aop:pointcut >,<.aop:declare-parents > ,< aop:advisor >等标签的详细配置以及切入点表达式的详细语法。

1 AOP的概述

AOP面向切面编程,通过提供一种程序结构的思考方式来补充面向对象的编程。(面向对象编程 OOP)

首先代码是面向业务的,业务也有轻重缓急,也就是有的业务是核心业务,有的业务是非核心业务。
比如说消息系统,接受和发送消息是核心业务,对于日志,异常报错处理,以及具体的校验就是非核心业务。这些业务是在一起的,这样让系统运作起来,只有核心业务可能系统最起码的安全性和稳定性都提供不了。但是没有核心业务,这些非核心业务也没有什么必要。

OOP的编程思想,基本模块单元是class,OOP将不同的业务抽象成一个个类,不同的业务操作抽象成不同的方法。这样呢逻辑清晰,作为一个完整的逻辑处理,如同一个流水线,这个流水线上是由核心业务和非核心业务连接成的。各个业务按照顺序执行的。

这样的代码也有自己的问题,解释代码间的耦合很严重,各个业务都掺在一起。核心业务代码之间要手动插入一大堆非业务代码,比如说日志,报错处理这些。对于这写跨对象和跨业务重复的,公共的非核心的逻辑代码,OOP没有特别好的方法来处理这些。

OOP编程是流水线这种上到下的,业务非业务揉在一起。
AOP代码就是对不同业务代码,抽取出来一个“横切面”,实现代码复用减少代码量,提高扩展性和可维护性。

OOP对业务中每一个功能进行抽取,封装成类,方法让代码模块化。AOP对业务中重复调用的模块进行抽取,更高的层次实现了代码的复用,有利于代码的复用。
AOP的层次更高,AOP和OOP不是竞争关系,AOP是OOP的补充。

2 AOP的概念

2.1 核心概念

这些概念是通用的,但是还是有各自的框架的区别。

2.1.1 Joinpoint
连接点,程序执行时的一些特定位置/点位,这些点位就是可能被AOP框架拦截被织入代码的地方。常见的点位:
在这里插入图片描述
Spring AOP中,连接点只支持method excution即方法执行连接点,并且不能应用于在同一个类相互调用的方法。

2.1.2 pointcut
切入点,用来匹配要进行切入的Joinpoint集合的表达式,通过切点表达式(类似于正则表达式),可以确定符合条件的连接点作为切入点。

2.1.3 advice
通知,切面的具体行为/功能,在pointcut匹配到joinpoint位置,会插入指定类型的Advice。

Spring的通知类型:
1Before Advice:前置通知,在切入点的方法正常完成要运行但不能组织执行到切入点方法的通知。(除非他返回异常)

2After Returning Advice:后置通知,在切入点方法正常要完成要运行的通知(例如:如果方法返回并且不返回异常)

3 After Throwing Advice:异常通知,如果切入点方法通过引发异常而退出,执行通知

4After Finally Advice:最终通知,无论切入点方法退出的方式如何(正常或者异常返回)都要执行的通知。

5Around Advice:环绕通知,Around通知可以在切入点方法调用前后执行自定义行为。它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。

2.1.4 Aspect
切面,切入点(PointCut)和该位置的通知(Advice)的结合,跨多个业务的被抽离出来的各个公共业务模块,像一个“横截面”一样。对应Java代码中被@AspectJ标注的切面类或者XML配置的切面。

2.1.5 其他概念
1 Introduction:引介,一种特殊的通知(advice),在不修改代码的前提下,Introduction可以在运行期为类动态的添加一些额外的方法或者属性,实际使用的比较少。

2 Target Object:目标对象,代理的目标对象,被代理对象被称为“advised object”。Spring AOP是通过使用运行时动态代理实现的。

3 AOP Proxy:代理,一个类被称为AOP织入增强,会产生一个代理对象,在Spring框架中,AOP代理是JDK动态代理或者CGLIB代理。

4 Weaving:织入,是指把切面应用到目标对象,来创建新的代理对象的过程,织入的时期可以是编译时织入(AspectJ),也可以使用运行时织入。(Spring AOP)

2.2 Spring AOP 与AspectJ

最常用的AOP框架,一个是Spring AOP,另一个是AspectJ框架,上面的那些概念名词,这两个AOP框架

2.2.1 来源和目的
AspectJ是由Eclipse的开源的一个AOP框架,基于Java平台,提供完整的AOP的实现方式。

Spring AOP是Spring提供的一个AOP框架,目的和AspectJ不一样,不是提供完整的AOP的实现。而是让AOP和Spring AOP的结合得更紧密。

2.2.2 织入方式和性能
AspectJ属于静态织入
使用了专门的AspectJ编译器的编译器,在Java源码编译的时候,将切面织入到目标对象所属类的字节码文件中,并不会生成新的代理类字节码。因此,AspectJ在运行时不做任何事情。没有任何额外的开销,切面在类编译的时候织入。

Spring AOP属于动态织入
原理是动态代理,运行时会临时动态生成目标对象的代理对象,但是其性能不如AspectJ(AspectJ静态,在编译的时候直接织入到文件中)

Spring AOP的两种动态代理机制:
1JDK动态代理
要求目标对象必须至少实现一个合理的接口(要通过JDK动态代理,代理类通过实现目标的接口,这个是代理的基础。代理类实现目标的接口,继承java.lang.reflect.Proxy类所有JDK动态代理类的公共父类,提供代理的基础能力,才能重写接口方法并将调用转发给InvocationHandler.invoke())。

借助Java内部的反射,动态实现一个和目标对象所属相同接口的代理类,然后通过接口中的方法名,在动态代理生成的代理类调用业务类同名方法并进行代理。

因此只能代理接口的方法,这是Spring AOP的默认代理方式,这个是旧的SpringBoot版本(从SpringBoot2.X版本以后默认版本是CGLIB)

2CGLIB动态代理
要求目标对象的类是不能是final修饰的,目标对象是没有实现任何接口,会选择使用CGLIB代理。借助asm机制,会把目标对象的class文件加载进来,修改其字节码生成一个继承了目标对象的子类。重写这个方法来完成代理,需要的代理方法也不能是priva/final/static。

在默认情况下不管是旧的JDK动态代理,还是SpringBoot2以上的版本的CGLIB上,同一个目标对象中的方法相互调用的时候不会触发后续方法的代理机制。

默认情况下最终this即目标对象去调用方法,可以手动解决

1将该类“本身”实例注入到该类中,实际注入的是一个代理对象,也算是自身依赖,通过这个代理对象手动去调用另一个方法即可。

2将两个对象分开打不同的目标类里面

3对于XML,设置< aop:config/>标签的expose-proxy属性为true,对于注解,设置@EnableAspectJAutoProxy的exposeProxy属性为true,其目的就是将代理对象暴露出来,然后代码中使用AopContext.currentProxy()即可获取代理对象,然后强制转型去调用方法即可(注意类型兼容性)。

4以上方式对于事务方法也有效,但是如果该类存在@Async异步任务方法,那么@Async方法应该使用第一种方式并且在引入的自身代理对象上加上@Lazy注解,让其再进行代理封装。

2.2.3 切入点支持
Spring AOP致力于提供企业级的解决方案,仅仅支持Method Executation被作为切入点,是开发中最常见的切入点。

使用JDK的动态代理,那么被代理类必须实现接口,只能代理接口方法。
CGLIB动态代理,那么被代理类必须被继承,不能是final的,并且被代理的方法不能是private/final/static方法,因为它们不能被继承或者覆盖(重写、代理)。

2.2.4 使用范围
Spring AOP 只能和Spring IoC联合使用,AOP作用的对象之能是被IOC容器管理的bean。

AspectJ则可以单独使用,作用于任何pojo对象上。

2.2.5 其他
Spring支持无缝集成AspectJ。

Spring2.0以后新增了对AspectJ切点表达式的支持。AspectJ框架在1.5版本时,通过JDK5的注解技术引入了一批注解,比如@AspectJ、@Pointcut、相关Advice注解,支持使用注解的声明式方式来定义切面,Spring同样支持使用和AspectJ相同的注解,并且。但是,这相当于一个模版,底层仍然是走的Spring AOP的动态代理逻辑,并且不依赖于AspectJ的编译器或者织入器。

Spring AOP相比于AspectJ,它的学习难度更低,更容易上手。特别是如果我们开发时使用了Spring框架,那么建议就使用Spring AOP。

2.2.6 应用
Spring AOP中的主要应用就是:
1 声明式服务的支持,比如声明式的事务控制支持。
2 让用户自定义切面,用AOP和OOP的使用,比如通过AOP实现多数据源切换。

3 基于XML的AOP的配置

3.1 Spring AOP第一例

pom.xml引入相关依赖

<properties><spring-framework.version>5.2.8.RELEASE</spring-framework.version><aspectjweaver>1.9.6</aspectjweaver>
</properties>
<dependencies><!--spring 核心组件所需依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring-framework.version}</version></dependency><!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --><!--用于解析AspectJ的切入点表达式语法--><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>${aspectjweaver}</version></dependency>
</dependencies>
3.2 相关依赖

Spring AOP的需要引入的两个依赖,同样也可以引入spring-context依赖来直接引入。包括Spring AOP 的依赖,spring-context里面有很多的依赖组件包括在里面。

但是对于要支持的AspectJ,需要再引入一个aspectjweaver依赖。

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<!--用于解析AspectJ的切入点表达式语法-->
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>${aspectjweaver}</version>
</dependency>
3.3 引入AOP schema

Spring原始配置文件仅支持IoC支持的配置,如果想要使用AOP的XML配置,要手动引入AOP Schema。

普通的schema

<beans xmlns=http://www.springframework.org/schema/beansxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"></beans>

加入AOP的schema

<!--需要添加Spring AOP的Schema支持-->
<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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttps://www.springframework.org/schema/aop/spring-aop.xsd"></beans>
3.4 IoC管理bean

使用Spring AOP,无论是AOP增强的类还是定义通知的切面类,都需要交给IoC容器来管理。

<!--被代理类和通知类都交给IoC管理-->
<bean class="com.spring.aop.FirstAopTarget" name="firstAopTarget"/>
<bean class="com.spring.aop.FirstAopAspect" name="firstAopAspect"/>

3.5 aop:config配置
aop的相关配置都写在< aop:config >标签,实现Spring自动代理

<!--aop的相关配置都写到aop:config标签中-->
<aop:config><!--相关配置-->
</aop:config>

< aop:config >标签可以配置proxy-target-class属性为"true",这表示强制使用CGlib动态代理,多个< aop:config >的属性是共享的。

3.6 aop:aspect
<.aop:aspect >标签用于配制切面,其内部可以定义在具体定义的应用的那些切入点的通知。

< aop:aspect > 标签可以配置三个属性:
1 id 一个切面的唯一标识符

2 ref 用于引用的外部专门定义的通知bean,bean的内部定义了一系列的advice通知的逻辑,在<.aop:aspect >内部定义通知的时候可以通过method属性引用的方法。

通知bean就是一个普通的bean,只不过内部的方法逻辑作为通知的bean。

3 order:切面的排序,当有多个通知在同一个切入点去切入的时候,指定通知的先后顺序。未指定order属性时默认值为Integer.MAX_VALUE,即2147483647。order越小的切面,其内部定义的前置通知越先执行,后置通知越后执行。相同的order的切面则按照切面从上到下定义顺序先后执行前置通知,反向执行后置通知。
(就是数字越大执行的越靠前,同样或者没有指定的则按照定义顺序来执行。)

3.6.1 配置通知
在<.aop:aspect >切面标签内部可以对应的标签配置对应的类型的5种配置,有如下属性
1 method :通知的逻辑,这个逻辑写在方法中,就是对应上面<.aop:aspect >标签对应的bean中的方法名

2 pointcut:一个切入点表达式,用于指定通知应用到切入点集合,通过这个表达式可以匹配到某些方法作为该通知的切入点。

3 pointcut-ref: 用于指定切入点的表达式的引用,通过< aop:pointcut >单独定义切入点。

4 arg-names:按顺序使用“,”分隔的需要匹配的方法参数名字符串。用于配合切入点表达式,更加细致的匹配方法,更重要的是可以进行参数值的传递(后面会将)。

5种类型的通知对应就会有5种标签(对应前面的5种通知,before advice前置通知,afterc returning advice,after throwing advice,after finally advice 后置通知,异常通知,最后通知, around advice环绕通知)

对应的分别是:

1 < aop:before >标签用于配制前置通知 before advice,在切入点方法执行之前执行。
2 <.aop:after-returning >标签用于配置后置通知after-returning advic。
切入点方法正常执行之后可能会执行。后置通知的方法能够接收切入点方法的返回值作为参数,只需要配置returning属性,returning属性值就是后置通知方法的参数名,参数类型需要与返回值类型匹配(基本类型可以自动拆装箱,但是不能自动强制转型),或者向上兼容(可使用父类、父接口接收),否则后置通知不会被执行。
3< aop:after-throwing >标签用于配置异常通知,after-throwing advice这个在切入点方法,以及前面的前置和后置通知方法报错的话都会出现执行。

可以把对应抛出的方法的异常作为参数,抛出的异常需要匹配或者向上兼容才可以抛出(和处理抛出报错的逻辑一样,要不就一样,要不就包括是他的上一层)

4 < aop:after >标签用于配置最终通知,类似我们代码中的final代码块,无论切入点的方法怎么样都会被执行。

5 < aop:around >标签用于环绕通知around advice

如果都配置了前置通知,后置通知,环绕通知,最终通知可能的执行流程如下:(就是按照这个前置,切入点,后置,最终,来判断异常情况导致异常通知 触发情况)

1 如果正常执行,执行流程:前置通知-〉切入点方法-〉后置通知-〉最终通知
1 如果后置通知方法设置了参数并且正常捕获了切入点方法的返回值,或者没有设置参数,那么后置通知不会被执行。

2 如果前置通知中抛出异常,那么执行流程是:前置通知()-〉异常通知-〉最终通知
1 如果异常通知方法设置了参数并且正常捕获了该异常,或者没有设置参数,那么异常通知可以执行,否则异常通知不会被执行

3 如果切入点方法中抛出异常,执行流程:前置通知-〉切入点方法(抛出异常)-〉异常通知-〉最终通知
1 如果异常通知方法设置了参数并且捕获了该异常,或者没有设置参数,那么异常通知不会被执行

4 如果后置通知中抛出异常,那么执行流程是:前置通知-〉切入点方法-〉后置通知(抛出异常)-〉异常通知-〉最终通知
1如果异常通知方法设置了参数并且正常捕获了该异常,或者没有设置参数,那么异常通知可以执行,否则异常通知不会被执行。

5 如果异常通知中抛出异常,那么前面的执行流程不会改变,区别就是最终通知,也会抛出自己的流程而已,后置通知也会执行。

6 如果最终通知中抛出异常,那么前面的执行流程不会改变,区别就是最终通知也会抛出异常

7 异常最终会在最后一个流程,执行完毕输出到控制台抛出,无论异常通知有没有捕获。
不同的执行步骤中都可以抛出自己的异常,比如:前置通知->切入点方法(抛出自己的异常)->异常通知(抛出自己的异常)->最终通知(抛出自己的异常)。实际上,最终,后面步骤中抛出的异常信息会覆盖前面步骤中抛出的异常信息,即,最终控制台输出的只是最后面的步骤中抛出的异常的信息,因此可能会对异常的定位有一些干扰。所以,我们尽量不要在通知方法中抛出自己的异常!
(通知的异常会覆盖前面的异常,我门最后的定位肯定是最前面的原因,所以尽量不要在通知方法抛出自己的异常)

3.6.2 环绕通知
环绕通知和前面的前置,后置,最终通知都不一样,他没有固定位置,他在切入点之前,之后,抛出异常都可以执行。环绕通知是一种可以自己控制什么时候执行,怎么执行的一种通知。

环绕通知用的是<.aop:around >标签,通常情况环绕通知独立使用。即其他通知基本上可以相互配合。但是环绕通知是独立使用的,不能像前面的可以连在一起使用。环绕通知单独使用,基本不会配置前面那些通知。

环绕通知的方法,第一个参数必须是ProceedingJoinPoint类型,在通知逻辑(方法体)中,对ProceedingJoinPoint调用process的方法将会导致执行切入点方法的逻辑。proceed方法的返回值就是切入点方法的返回值。
如果没有返回值,那么外部调用切入点方法获取的最终返回值为null。环绕通知的返回值类型应该和切入点方法的返回值类型一致或者兼容。

proceed方法还能传递一个数组,该数组就是切入点方法所需的参数。可以通过对ProceedingJoinPoint调用getArgs获取外部调用切入点方法时传递进来的参数数组,也可以在环绕通知的逻辑中自己设置参数。

先写好被设置环绕的方法,另一方面设置环绕通知方法(环绕通知方法。可以根据proceed方法来,根据位置设置前置后置异常最终,也就是说环绕通知一个通知可以完成集成前者所有的通知。)

/*** 测试环绕通知** @author lx*/
public class AopTargetAround {public int target(int x,int y) {System.out.println("---test around advice target---");//引发一个异常//int j=1/0;//返回值return x+y;}
}
//-------------
/*** 环绕通知** @author lx*/
public class AopAspectAround {/*** 一定要有ProceedingJoinPoint类型的参数*/public int around(ProceedingJoinPoint pjp) {int finalReturn = 0;Object[] args = pjp.getArgs();System.out.println("外部传递的参数: " + Arrays.toString(args));System.out.println("==前置通知==");try {//proceed调用切入点方法,args表示参数,proceed就是切入点方法的返回值Object proceed = pjp.proceed(args);//也可以直接掉用proceed方法,它会自动传递参数外部的参数//Object proceed = pjp.proceed(args);System.out.println("切入点方法的返回值: " + proceed);System.out.println("==后置通知==");finalReturn = (int) proceed;} catch (Throwable throwable) {throwable.printStackTrace();System.out.println("==异常通知==");finalReturn = 444;} finally {System.out.println("==最终通知==");}//外部调用切入点方法获取的最终返回值return finalReturn;}
}

xml的环绕通知设置方法

<!--环绕通知测试-->
<bean class="com.spring.aop.AopAspectAround" name="aopAspectAround"/>
<bean class="com.spring.aop.AopTargetAround" name="aopTargetAround"/>
<aop:config><aop:aspect id="around" ref="aopAspectAround"><!--配置环绕通知 和其他通知的配置都差不多--><aop:around method="around" pointcut="execution(public int com.spring.aop.AopTargetAround.target(int,int))"/></aop:aspect>
</aop:config>
@Test
public void aroundAdivce() {//1.获取容器ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");//2.获取对象AopTargetAround aopTargetAround = (AopTargetAround) ac.getBean("aopTargetAround");//3.尝试调用被代理类的相关方法int x = 2;int y = 1;System.out.println("-----外部调用切入点方法传递的参数: " + x + " " + y);int target = aopTargetAround.target(x, y);//最终返回值System.out.println("-----外部调用切入点方法获取的最终返回值: " + target);
}

3.6.3 JoinPoint
任何advice通知方法第一个参数可以设置为org.aspectj.lang.JoinPoint类型,JoinPoint是连接点方法的抽象,提供了访问当前通知方法的目标对象,代理对象,方法参数等方法。

JoinPoint的相关方法以及解释如下:
在这里插入图片描述
ProceedingJoinPoint有两个额外的方法:
在这里插入图片描述
3.6.4 传递参数
可以通过returning传递目标方法的返回值或者throwing传递抛出的异常给相关的后置通知或者异常通知方法。

3.6.4.1 传递参数值

/*** @author lx*/
public class ParamAspectTarget {public ParamAspectTarget target(int i, Date date, String string) {//构造一个异常//int y=1/0;ParamAspectTarget paramAspectTarget = new ParamAspectTarget();System.out.println("target: "+paramAspectTarget);return paramAspectTarget;}
}
//-----------------
/*** @author lx*/
public class ParamAspectAdvice {public void before(JoinPoint joinPoint, int i2, Date date, String string ) {System.out.println("-----before-----");System.out.println(joinPoint);System.out.println(i2);System.out.println(date);System.out.println(string);System.out.println("-----before-----");}public void afterReturning(JoinPoint joinPoint, Date date, String string, ParamAspectTarget returned) {System.out.println("-----afterReturning-----");System.out.println(joinPoint);System.out.println(date);System.out.println(string);System.out.println(returned);System.out.println("-----afterReturning-----");}public void afterThrowing(JoinPoint joinPoint, int i, Exception e ,Date date ) {System.out.println("-----afterThrowing-----");System.out.println(joinPoint);System.out.println(i);System.out.println(date);System.out.println(e);System.out.println("-----afterThrowing-----");}public void after(JoinPoint joinPoint, int i, Date date, String string) {System.out.println("-----after-----");System.out.println(joinPoint);System.out.println(i);System.out.println(date);System.out.println(string);System.out.println("-----after-----");}public Object around(ProceedingJoinPoint joinPoint, int i, Date date, String string) {System.out.println("-----around-----");System.out.println(joinPoint);System.out.println(i);System.out.println(date);System.out.println(string);System.out.println("-----around-----");Object proceed = null;try {proceed = joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}finally {return proceed;}}
}
<!--传递参数-->
<bean class="com.spring.aop.param.ParamAspectAdvice" name="paramAspectAdvice"/>
<bean class="com.spring.aop.param.ParamAspectTarget" name="paramAspectTarget"/>
<!--通知参数-->
<aop:config><!--args参数名与通知方法的参数名一致--><aop:pointcut id="par1"expression="execution(* com.spring.aop.param.ParamAspectTarget.target(..)) and args(i2,date,string) "/><!--args参数名与通知方法的参数名不一致,并且执行传递某些位置的参数--><aop:pointcut id="par2"expression="execution(* com.spring.aop.param.ParamAspectTarget.target(..)) and args(*,date1,str) "/><aop:pointcut id="par3"expression="execution(* com.spring.aop.param.ParamAspectTarget.target(..)) and args(i,date,..) "/><aop:pointcut id="par4"expression="execution(* com.spring.aop.param.ParamAspectTarget.target(..)) and args(i,date,string) "/><aop:aspect ref="paramAspectAdvice"><!--args参数名与通知方法的参数名一致时,arg-names属性可以省略--><aop:before method="before" pointcut-ref="par1" arg-names="joinPoint,i2,date,string"/><!--arg-names按照顺序为每一个参数命名,想要参入参数,那么需要对应该args中的名字--><!--将会按照顺序和类型进行赋值,如果类型不匹配,那么该通知方法不会被调用--><aop:after-returning method="afterReturning" pointcut-ref="par2"arg-names="joinPoint,date1,str,return" returning="return"/><!--args参数名与通知方法的参数名一致时,arg-names属性可以省略--><aop:after-throwing method="afterThrowing" pointcut-ref="par3" throwing="e"/><!--args参数名与通知方法的参数名一致时,arg-names属性可以省略--><aop:after method="after" pointcut-ref="par4" /><!--args参数名与通知方法的参数名一致时,arg-names属性可以省略--><!--<aop:around method="around" pointcut-ref="par4"/>--></aop:aspect>
</aop:config>

一般来说,我们的通知方法的参数与目标方法的参数名称、顺序、类型一致,然后只需要在切入点表达式中配置args(),并且按目标方法的顺序声明参数名称,即可实现参数值通过方法参数的传递,并且可以省略arg-names属性。

3.6.4.2 传递方法注解
实际上,使用@annotation()的PCD也可以将方法上的注解传递给通知方法,可以在方法中拦截注解的内容。

@Scope(value = "111")
@Description("描述")
@Lazy
public void target() {System.out.println("annotation target");
}

注意一个@annotation只能传递一个注解,如果一个通知方法要传递多个注解,需要使用多个@anatation注解。

实际上,@within, @target, @annotation, 和 @args、this()、target()被称为不同类型的切入点表达式,即不同的PCD。

3.7 aop:pointcut切入点表达式

每一个通知中,可以配置自己的切入点表达式,很多时候切入点表达式是一样的,可以单独配置一个唯一的表示,使用< aop:pointcut >标签定义一个独立的切入点表达式,使得多个切面和advisor通过pointcut-ref属性共享一个切入点表达式。

expression属性:用于定义切入点表达式
id属性:用于给切入点表达式提供一个唯一标识,通过该表示引用对应的切面表达式

< aop:pointcut >可以定义在< aop:aspect >内部,这表示该表达式只能在当前切面内部的通知中引用,也可以定义在< aop:config >内部

< aop:pointcut >顺序在< aop:aspect >之前,另外,如果有多个< aop:config >中有相同id的切入点表达式,则最终只会应用最后定义的切入点表达式,它们的切面最终会合在一起。

Spring AOP 仅支持Spring bean方法执行连接点,可以将切入点表达式视为用来匹配Spring bean的某些方法。

3.7.1 切入点指示符PCD
AspectJ表示指示符(pointcut designators 简称PCD),PCD用来指明切入点表达式的大概目的。由于在Spring AOP中目前只有执行方法这一个连接点,目前Spring 5.x的AOP支持使用如下PCD:
1 execution方法,通过匹配某些类型的某些方法签名来匹配连接点方法

2 within方法,通过匹配某些类型来匹配连接点方法·

3 this方法,通过匹配AOP的代理对象类型来匹配连接点方法

4 target方法,通过匹配AOP的目标对象类型来匹配连接点方法

5 args方法,通过匹配方法参数的数量,类型,顺序来匹配连接点方法

6 @target方法,通过匹配某些注解类型来匹配连接点方法

7 @args方法,通过匹配方法参数的所属类型上某些注解来匹配连接点方法

8 @within方法,通过匹配类型及子类型上的某些注解类型来匹配连接点方法

9 @anatation方法,通过匹配方法上的某些注解类型来匹配连接点方法

10 bean,通过匹配某些bean name来匹配连接点方法,Spring AOP特有的。

其他的AspectJ的PCD,比如call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this和@withincode等等目前的Spring AOP均不支持。在使用Spring AOP的情况下,在切入点表达式中使用这些PCD会导致引发IllegalArgumentException,但是未来可能会扩展。

Spring AOP 的PCD支持是AspectJ的子集(仅10种),无法支持call,get,cflow等高级指示符。

PCD还支持通配符:

* 任意数量的字符
.. 任意项的重复,主要用于execution的declaring-type-pattern中,表示匹配当前包及其子包,以及execution的param-pattern中,表示匹配任意数量参数。
+ 该类型以及其子类型

PCD还支持运算符(基于XML配置,建议使用and,or,not)

&& and
|| or
! not

3.7.2 excution PCD
Spring AOP 仅支持方法的切入点,execution的切入点表达式使用方法的签名来筛选切入点方法。excution是Spring使用最多的PCD。

1 带有符号的表示可选项
2 modifiers-pattern 方法的访问修饰符(可选),public,protected
3 ret-type-pattern 方法的返回值类型全路径名,java.lang包下的类可以写简单类名
4 declaring-type-pattern:方法所属类的全路径名(可选);
5 name-pattern:方法名;
6 param-pattern:方法参数列表类型,要求使用全路径类名,java.lang包下的类可以写简单类名。多个参数类型使用“,”分隔,按照指定顺序匹配。
7 throws-pattern:匹配异常类型列表(可选),之前需要添加throws关键字,后面同样是异常的全路径类名,一般使用“,”分隔,java.lang包下的异常类可以写简单类名。没有就表示有没有抛出异常都无所谓。
8 表达式中匹配的都是声明的类型,而不是实际类型,并且默认不向下兼容,可是手动使用“+”。

3.7.2.1 常见语法
1 一个完整的excution表达式

 execution(public String com.spring.aop.execution.AopTarget.target(String,Object) throws NullPointerException)

修饰符为public,返回类型为String,
所属类路径和方法名为com.spring.aop.execution.AopTarget.target,

2访问修饰符可以省略,表示匹配访问任何修饰符

execution(String com.spring.aop.execution.AopTarget.target(String,Object))

3 可以使用void表示无返回值,使用*表示任意类型的返回值(包括void)

execution(* com.spring.aop.execution.AopTarget.target(String,Object))

4 可以使用 * ,表示匹配任意包名,有几级子包路径就要几个 *

execution(* com.*.*.aop.*.AopTarget.target(String,Object))

5 可以使用 …,来表示匹配当前包及下面的任意多级路径子包,及其任意名字的子包

execution(* com..AopTarget.target(String,Object))

这个 … 表示com包下任意多级包类型,任意名字的子包下面多级包路径、任意名字的子包下面的AopTarget类的target方法。

因此,如果存在com.AopTarget、com.aa.bb.cc.AopTarget……等路径的同名target方法,那么都能被匹配(假设其他条件也匹配)。

6 可以使用 * ,表示匹配任意类名,或者类名的前缀/后缀/中缀

execution(* *..*.target(String,Object))

匹配任意包名、包路径下的任意类中的任意名字的方法,参数类型为String和object,返回值任意

execution(* *..AopTarget*.target(String,Object))

匹配任意包名、包路径下的任意类中的方法名以“target”为前缀的任意方法,参数类型为String和object,返回值任意。

execution(* *..*Execution*.target(String,Object))

匹配任意包名、包路径下的任意类中的方法名以“tar”为前缀,以“et”为后缀的任意方法,参数类型为String和object,返回值任意。

9 参数列表可以使用*某个位置的参数可以是任何类型必须有参数

execution(* *..*.*(*))
execution(* *..*.*(*,*))

匹配任意包名、包路径下的任意类中的任意名字的方法,只能且必须有两个参数,参数类型任意,返回值任意。

execution(* *..*.*(String,*))

和前面类似,只能且有且只有两个参数,只不过第一个参数指定类型为String,第二个可以是任意的。

10 参数列表可以使用… 表示任意数量,类型的参数

execution(* *..*.*(..))

匹配任意包名、包路径下的任意类中的任意名字的方法,参数类型、数量、顺序任意,返回值任意。

execution(* *..*.*(String,..))

和前面类似,区别就是第一个参数指定为String,后续的参数任意。

11 可以在throw之后声明匹配多个异常,异常的类路径当然也可以使用*、…

execution(* *..*.*(..)throws NullPointerException,*.omg..SystemException)

需要匹配抛出两个异常的方法,一个是NullPointerException,另一个是SystemException。

execution(* *..*.*(..)throws Exception+)
execution(* *..*.*(..)throws not NullPointerException)
execution(* *..*.*(..)throws  NullPointerException or NumberFormatException)

匹配的方法至少抛出一个异常,该异常可以是NullPointerException类型或者NumberFormatException类型。

12 实际上涉及到类型都可以在类的后面使用+,表示匹配到某个类型以及其子类型

execution(String+ *..AopTarget*+.target(String+,Object))

匹配任意包名、包路径下的类名以“AopTarget”为前缀的任意类及其子类中的target方法,参数类型为String及其子类型和object类型,返回值为String及其子类型。

13 实际上涉及到类型的都可以使用and,or,not运算符

execution(* (!com.spring..AopTarget+).target(..))
execution(* (com..AopTarget+ and java.lang.Comparable+).*(..))
execution(* (com..AopTarget+ or java.lang.Comparable+).*(..))
execution(* *(String || Object,Object))
execution(* *(String+ and Object+,Object))

匹配任何包的任何类的任何返回值的方法,且第一个参数必须是String类及其子类型,并且还属于Object类及其子类型,第二个类型为Object类型。

execution(* *(!Object,Object))

匹配任何包的任何类的任何返回值的方法,且第一个参数必须非Object类型,第二个类型为Object类型。

execution((String or Integer) *(..))

匹配任何包的任何类的任何方法,要求返回值类型为String或者Integer类型。前面是返回类型,后面是参数类型,根正常的Java方法顺序一样。

14 可以通过方法上的注解来筛选

execution(@org.junit.Test * *(..))

任何标注了@Test注解的方法

execution(@org..Value @org..Scope * *(..))

任何标注了@Value和@Scope注解的方法。注解的类路径也可以使用*和…。

execution(@(org..Value || org..Scope) * *(..))

任何标注了@Value或者@Scope注解的方法。

execution(@(org..Value || !org..Scope) * *(..))

任何具有注解,并且(标注了@Value或者没有标注@Scope注解的方法。即如果表达式有通过方法上的注解来筛选的行为,那么任何没有注解的方法都将不会被选中。

15 可以通过方法的返回值类型所属类的注解来筛选

execution((@org..Repository *) *(..))

返回值的所属类上标注了@Repository注解的方法。

execution((@org..Repository @org..Scope *) *(..))

返回值的所属类上标注了@Repository注解和@Scope注解的方法。

execution((@(org..Repository or org..Scope) *) *(..))

返回值的所属类上标注了@Repository注解或@Scope注解的方法。(和前面比就是多了一个or的运算符)

execution((@(!org..Repository or org..Scope) *) *(..))

返回值的所属类上,必须有注解,并且(没有标注@Repository注解或标注了@Scope注解)的方法。即如果表达式有通过方法的返回值所属类上的注解来筛选的行为,那么任何方法的返回值所属类没有注解的方法都将不会被选中。

16 可以通过方法本身所属类上的注解来筛选

execution(* (@org..Repository *..AopTarget*).*(..))
execution(* (@org..Repository @org..Scope *).*(..))
execution(* (@(org..Repository || org..Scope) *).*(..))
execution(* (@(!org..Repository || !org..Scope) *).*(..))

标注有注解,并且(没有标注@Repository注解或没有标注@Scope注解)的类的方法。即如果表达式有通过方法本身所属类上的注解来筛选的行为,那么任何方法本身所属类上的没有注解的方法都将不会被选中。

17 可以通过方法参数上的注解来筛选

execution(* *(@org..Value (java.io.Serializable+ and Comparable+),..))
execution(* *(@org..Value (*),@org..Value (*)))
execution(* *(@org..Value @org..Qualifier (String),@org..Value (*)))
execution(* *(@(org..Value or org..Qualifier) (*),..))
execution(* *(!@org..Value @org..Qualifier (*),..))
execution(* *(@(!org..Value or org..Qualifier) (*),..))

18 可以通过方法参数的所属类上注解来筛选

execution(* *(@org..Repository *,..))
execution(* *(!@org..Repository @org..Scope *,..))

至少一个参数的方法,且第一个参数的所属类上标注了@scope注解,但是不能标注Repository注解

execution(* *(@(!org..Repository or org..Scope) *,..))
execution(* *(@org..Value (@org..Repository *),@org..Value (@org..Component *)))

至少一个参数的方法,第一个参数为List类型,并且要求泛型参数所属的类型上必须标注有注解,并且(没有标注@Repository注解或者标注了@Component注解)。
泛型匹配可能有些兼容问题,不建议使用!

(至少一个参数,看逗号后面,如果是… ,就表示一个参数列表)

3.7.3 within PCD
within的切入点表达式使用类型筛选切入点方式,很明显within PCD的粒度控制不及execution PCD,

within表达式的格式为:within(declaring-type-pattern)

within(*)

星号* 表示任何,任何包下任何方法

within(com.spring.*.AopTargetExecution2)
within(com.spring..AopTarget*)
within(com.spring..AopTargetExecution || com.spring..AopTargetExecution2)
within(*..AopTarget* and Comparable+)

任何包下的以“AopTarget”为前缀类名,并且属于Comparable及其子类类型的类的全部方法。

3.7.4 this和target PCD
this()表示匹配某类及其代理子类的方法,target()表示匹配某类的及其子类方法。内部的表达式不支持 * 和 … ,只能使用全路径名(java.lang包下的类同样可以写简单类名)。上一部分也说过,它们还能传递参数,分别传递代理对象和目标对象。

(通配符 单点通配*:用于返回值,类名,方法名或者单参数类型
通配符 多点通配…:用于包路径(子包路径)或任意数量参数

根据Spring AOP使用动态代理的方式的区别,this和target PCD在某些情况具有一致的含义,以下是不同:
1 方法所属目标类实现接口,Spring默认实现JDK的代理,目标类和代理类实现相同的接口,在代理对象中调用目标对象来进行增强,从容器中获取的bean是一个代理类对象。

2 如果方法所属目标类没有实现接口,那么Spring代理的方式是Cglib。代理类继承目标类,代理类对象会增强父类的方法,也是从容器中获取一个bean对象。
(有接口的就是JDK代理,没有接口就采用cglib。cglib代理对象的本质是一个目标类的子类的实例。JDK的代理对象的本质是接口的实例。)

3 可以手动指定使用CGlib代理,< aop:aspectj-autoproxy proxy-target-class=“true”/>或者< aop:config proxy-target-class=“true”>

PS:代理方式的不同适用不同的情况,有接口就可以用JDK动态代理,没有就用cglib方式。

代理方式不同生成的实例也不同,JDK动态代理生成的代理对象就是接口的实例对象,cglib生成的代理对象是对应目标类的子类的实例。

还要结合this和target
1 如果表达式指向一个接口,无论是JDK代理和cglib代理,该接口的目标类生成的代理对象类实现接口,this()和target()具有相同的结果。

2 如果表达式指向一个类,该类没有实现接口。对应的目标类和他的子类就会使用cglib动态代理,生成的代理类会继承目标类,this()和target()的结果也会是一样的。

3 如果表达式指向一个类,该类有实现接口。该类以及子类的默认使用JDK代理。代理类和目标类使用实现的是相同的接口,但是他们之间没有相关关系。但是this()和target()将具有不同的结果。如果使用的代理方式是cglib,会有相同的结果。

3.7.5 args PCD
args()通过匹配方法参数的数量,类型,顺序来匹配连接点方法。用的比较少,语法格式args(param-pattern)

参数类型为全路径名,不支持 * 和 … 。

java.lang包下的类可以写简单类名。args属于运行时动态匹配,可以匹配的某个参数的运行时传递的类型及其子类型,开销比较大。

execution表达式中的param-pattern严格的类型匹配,不支持向下兼容。

args(*)

匹配只有一个参数的任何方法

args(java.io.Serializable,..)

匹配至少有一个参数的任何方法(多项通配符…),且第一个参数类型为运行时传递的Serializable类型及其子类型 (java.io.Serializable指定类)。

3.7.6 @args PCD
通过匹配方法参数的所属类型上的某些注解来匹配方法,语法为@args(annotation-type),而且可以传递参数,传递方法上所属类型上的注解。

(这个路径名是不支持通配符“*”和“…”,,和上面的参数这些可以支持的不一样,可以在中间穿插一些通配符。)

@args(org.springframework.stereotype.Repository,..)

第一个参数所属类型标注有@Repository注解的方法。

3.7.7 @target和@within PCD
@target通过类型上的某些注解类型来匹配连接点方法,语法为@target(annotation-type)

@within通过类型上的某些注解类型来匹配连接点方法,还会匹配该注解的类的子类,语法@within(annotation-type)

@target

@target(org.springframework.stereotype.Repository)

匹配标注了@Repository注解的类的全部方法。

@within(org.springframework.stereotype.Repository)

匹配标注了@Repository注解的类的全部方法,以及通过这些类的子类(要求同样交给IoC管理)bean调用的父类方法。

适用场景:
@target:精确匹配当前目标对象的类的注解(无始继承)
@within:匹配方法声明类的注解(支持父类未重写的方法的继承)

这些还是要结合对应的完整的代码:

// 父类
@MyAnnotation("father")
public class Father {public void hello2() {}  // 未重写方法
}// 子类
@MyAnnotation("son")
public class Son extends Father {@Overridepublic void hello() {}  // 重写方法
}// 切面配置
@Aspect
public class InheritanceAspect {// @within 测试:关注方法声明类的注解@Before("@within(com.example.annotation.MyAnnotation)")public void withinAdvice(JoinPoint joinPoint, MyAnnotation ann) {System.out.println("@within: " + ann.value());}// @target 测试:关注目标对象的注解@Before("@target(com.example.annotation.MyAnnotation)")public void targetAdvice(JoinPoint joinPoint, MyAnnotation ann) {System.out.println("@target: " + ann.value());}
}

方法的调用 @within输出 @target输出
son.hello() son. son
son.hello2() father. son

结论:
@within 未重写方法,取方法声明类的注解
@target 始终取目标对象的实际类型(子类的)注解

target更准确,within会把父类的带出来

3.7.8 @annotation PCD
通过匹配方法上的某些注解类型来匹配连接点方法,语法为@annotation (annotation-type)。

注解类型使用全路径名,不支持使用“*”和“…”,Java.lang包下的注解可以使用简单类名。

@annotation(org.springframework.beans.factory.annotation.Value)

匹配方法上标注有@Value注解的方法。

3.7.9 bean PCD
除了上面这些匹配within,target这种路径名来对应的连接点,还可以通过bean name的来匹配连接点方法,语法为:bean(Bean id/name),允许*。

bean PCD仅在Spring AOP中受支持,也就是单独使用AspectJ是没用的,虽然SPringAOP是依赖AspectJ来完成的。但是bean PCD是依靠Spring AOP来完成的。也就是你单独使用@Aspect注解,是不起作用。

bean(aopTarget*)

匹配以aopTarget为前缀名的bean的全部方法。

3.7.10 组合PCD
可以使用不同类型的PCD使用 ||(or) &&(and) !(not)组合起来

execution(public String *(..)) and args(Comparable,..)

方法访问修饰符为public,返回值类型为String,第一个参数传递的类型为Comparable及其子类类型,如果execution中有指定,那么以execution为主。

3.8 aop:declare-parents 引介

在不修改代码的情况下,introduction可以在运行期为类动态添加一些额外的方法或属性。

在用到< aop:declare-parents >标签,这个标签< aop:aspect>标签的子标签,用于定义bean的动态增强的行为。内部4个属性:
1 type-matching:需要增强的类扫描路径,该路径下的被Spring管理的bean都将被增强
2 implement-interface:新的增强方法所属的接口,实际上Spring创建的目标类的代理类会同时实现一个接口(无论是JDK得反射,还是cglib),因此多一些功能
3 default-impl:一个实现了上面接口的类,作为新的增强方法的默认实现类。
4 delegate:引入的一个增强方法的实现类bean的id。

3.9 aop:advisor通知器

标签<aop: advisor>用于定义一个通知器,他和标签< aop:aspect>类似,也可以想象成一个切面。

这两个的区别,aspect可以配置不同的类型的通知,引用的通知方法可以是一个普通的Spring bean中的方法。

advisor只需要一个通知类bean即可,也就是这个bean实现Advice的接口,Advice是“通知”的抽象,一般是实现MethodBeforeAdvice, AfterReturningAdvice, ThrowsAdvice这几个接口。

3.10 scope-proxy作用域代理

长生命周期的bean调用短生命周期bean,这样会导致一个问题就是这个短生命周期bean不能被正常的管理。

之前解决这个问题的两个处理方式:
第一种是基于XML的< lookup-method>查找方法并注入标签

第二种基于注解@Scope(proxyMode=xx)注解

现在还可以多一种解决方法,基于XML< aop:scoped-proxy/>AOP作用域代理标签,这个标签更加的独立,和此前的< aop:config/>标签并无多大联系。

这个标签同样具有proxy-target-class属性,这个属性用于确定要使用什么代理方式,默认置为true,表示创建基于CGLIB的代理,这要求目标类不能是final的,可以设置为false,表示创建基于JDK的动态代理,这要求目标类实现了接口。

4 总结
AOP相对于IOC可能更容易被忽略,另一方面也更不好理解,因为这个IOC,容器管理,依赖相对来说概念更直观,就是交给容器去管理bean,然后处理实例化的时候处理依赖关系。这个在实际使用中用的是也比较多,不再自己去new一个对象,一行xml配置,或者一个注解就可以,体验感很强。

但是这个AOP的,对于切面这个概念实际项目中肯定不如IOC多,更多的都是比较低层,对于动态代理出来的代理对象,大家也不是很直观,更何况还要根据情况去分析使用什么方式来完成代理。什么时候用JDK得代理方式,什么时候用cglib的代理方式。

切面涉及到配制切面和切入点,AOP的概念以及基于XML的Spring AOP配置,常用标签就是:< aop:config >:用来写aop的相关配置以及指定代理方式;< aop:aspect >:用于配置切面;< aop:pointcut >:用于配置切入点表达式;< aop:declare-parents >用于配置Introduction;< aop:advisor >用于配置一个通知器,这些标签都能和最开始的概念对应上,还是比较简单的。

http://www.dtcms.com/a/322366.html

相关文章:

  • Vue3的简单学习
  • docker基础前置
  • day18 - CSS函数
  • ADB(Android Debug Bridge)—— Android调试桥
  • Android MediaMetadataRetriever取视频封面,Kotlin(1)
  • 【Android调用相册、拍照、录像】等功能的封装
  • Milvus 向量数据库基础操作解析
  • 进阶向:Python编写网页爬虫抓取数据
  • vulnhub-Beelzebub靶场通关攻略
  • 【Spring Boot 快速入门】八、登录认证(二)统一拦截
  • Android中RecyclerView基本使用
  • 鸿蒙分布式任务调度深度剖析:跨设备并行计算的最佳实践
  • Java安全-组件安全
  • 哈希与安全
  • Red Hat Enterprise Linux 7.9安装Oracle 11.2.0.4单实例数据库-图文详解
  • urmom damn the jvm
  • vscode/trae 的 settings.json 中配置 latex 的一些记录
  • QT环境搭建
  • 数学学习 | 高数、线代、概率论及数理统计荐书
  • 【Task2】【Datawhale AI夏令营】多模态RAG
  • 图片拆分工具,自定义宫格切割
  • 第4章 程序段的反复执行4.2while语句P128练习题(题及答案)
  • CVPR中深度学习新范式:通用性、鲁棒性与多模态的创新突破
  • 六、CV_图像增强方法
  • Python 程序设计讲义(66):Python 的文件操作——数据的处理
  • 多模态RAG赛题实战--Datawhale AI夏令营
  • 计算BERT-BASE参数量
  • 基于windows10/11的可用的自动日记启动脚本
  • Irix HDR Pro:专业级 HDR 图像处理软件
  • STM32H503不同GPIO速度配置(HAL库)对应的最高速度