首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

SpringAOP的概念还搞不清,代码概念相结合,一文保你懂

一、@Aspect声明一个切面

切面是一个类,其中包含了横切关注点的代码以及指定这些代码应该被执行的时机。通常,切面包括了通知(advice)和切点(pointcut)两个主要部分。

1、@Aspect源码

package org.aspectj.lang.annotation;

import java.lang.annotation.Target;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface Aspect { /** * 默认singleton切面 */ public String value() default "";}

@Aspect有一个value属性,默认单例切面。

2、示例

@Aspect // 声明一个切面,默认单例切面public class MyAspect{ //...}

二、@Pointcut声明一个切点

切点是指在应用中哪些地方应该应用通知的定义。它定义了一组匹配规则,用于确定哪些方法应该被通知。

1、@Pointcut源码

package org.aspectj.lang.annotation;

import java.lang.annotation.Target;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;

/** * 声明一个切点 */@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Pointcut {

/** * 切点表达式 */ String value() default ""; /** * 参数名称,用于指定切点表达式中方法参数的名称,默认为空 */ String argNames() default "";}

argNames 这个参数在 AspectJ 注解风格中并不常用,因为切点表达式通常直接指定方法的参数类型而不是参数名称。但是,当需要在通知中引用方法参数时,指定参数名称可以使代码更加清晰易懂。

2、示例

@Aspect // 声明一个切面,默认单例切面public class MyAspect{ // 1、表示任何一个公共方法 @Pointcut("execution(public * *(..))") // 切点表达式 public void publicMethod() {} // 切入点签名 // 2、表示某一模块 @Pointcut("within(com.xyz.trading..*)") public void inTrading() {} // 3、某一模块的任何公共方法 @Pointcut("publicMethod() && inTrading()") public void tradingOperation() {} }

3、表达式类型

1)execution:用于匹配方法执行连接点。

// 任何公共方法的执行execution(public * *(..))// 任何名称以set开头的方法的执行execution(* set*(..))// 由AccountService接口定义的任何方法的执行execution(* com.xyz.service.AccountService.*(..))// 包中定义的任何方法的执行:execution(* com.xyz.service.*.*(..))// 包或其子包中定义的任何方法的执行:execution(* com.xyz.service..*.*(..))

这是使用SpringAOP时要使用的主要切入点指示符。

2)within:限制与某些类型内的连接点的匹配。

// 包中的任何连接点within(com.xyz.service.*)// 包或其一个子包内的任何连接点within(com.xyz.service..*)

3)this:将匹配限制为连接点,其中bean引用(Spring AOP代理)是给定类型的实例。

// 代理实现AccountService接口的任何连接点this(com.xyz.service.AccountService)

4)target:限制与连接点的匹配,其中目标对象(被代理的应用程序对象)是给定类型的实例。

// 目标对象实现AccountService接口的任何连接点target(com.xyz.service.AccountService)

5)args:限制与连接点的匹配,其中参数是给定类型的实例。

// 任何接受单个参数并且在运行时传递的参数是Serializable的连接点args(java.io.Serializable)

6)@target:限制与连接点的匹配,其中执行对象的类具有给定类型的注释。

// 目标对象具有@Transactional注释的任何连接点@target(org.springframework.transaction.annotation.Transactional)

7)@args:限制与连接点的匹配,在连接点中,传递的实际参数的运行时类型具有给定类型的注释。

// 任何接受单个参数的连接点,并且其中传递的参数的运行时类型具有@Classified注释@args(com.xyz.security.Classified)

8)@within:限制与具有给定注释的类型内的连接点的匹配。

// 目标对象的声明类型具有@Transactional注释@within(org.springframework.transaction.annotation.Transactional)

9)@annotation:将匹配限制为连接点的主题具有给定注释的连接点。

// 执行方法具有@Transactional注释@annotation(org.springframework.transaction.annotation.Transactional)

10)其他

// 名为 tradeService 的 Spring bean 上的任何连接点bean(tradeService)// Spring bean上名称与通配符表达式匹配的任何连接点bean(*Service)

三、五种通知类型

通知是切面中的具体行为,它定义了在何时、何地执行切面代码。在Spring中,它包括了以下5种类型。

1、@Before源码

在连接点之前执行的通知。

package org.aspectj.lang.annotation;import java.lang.annotation.Target;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Before { // 切点表达式 String value(); // 携带的参数 String argNames() default "";}

value 切点表达式,如果写了切点,可以在通知里直接引用。如果没写切点,在通知里也是可以写的。

2、@AfterReturning 源码

在连接点正常完成后运行的通知。

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface AfterReturning { // 切点表达式 String value() default ""; // 绑定切点 String pointcut() default ""; // 绑定返回值,默认空 String returning() default ""; // 携带的参数 String argNames() default "";}

pointcut 直接指向切点,还可以指定默认返回值。

3、@Around 源码

包围连接点的通知,可自由控制连接点的执行。

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Around { // 切点表达式 String value(); // 携带的参数 String argNames() default "";}

4、@AfterThrowing 源码

在连接点抛出异常后执行的通知。

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface AfterThrowing { // 切点表达式 String value() default ""; // 绑定切点表达式 String pointcut() default ""; // 绑定异常 String throwing() default ""; // 携带的参数 String argNames() default "";}

5、@After 源码

无论连接点以何种方式退出(正常或异常返回),都要执行的通知。

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface After { // 切点表达式 String value(); // 携带的参数 String argNames() default "";}

根据需求选择通知类型,然后就可以在通知里为所欲为了。

6、应用示例

public class MyAspect{ @Pointcut("execution(public * *(..))") public void publicMethod() {}

@Before("execution(* com.xyz.dao.*.*(..))") public void doAccessCheck() { // ... }

@AfterReturning("execution(* com.xyz.dao.*.*(..))") public void doAccessCheck() { // ... }

@Around("execution(* com.xyz..service.*.*(..))") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; }

@AfterThrowing("execution(* com.xyz.dao.*.*(..))") public void doRecoveryActions() { // ... }

@After("publicMethod()") // 声明一个通知 public void printLog(JoinPoint joinPoint) { // 打印日志 } }

官网资料参考:https://docs.spring.io/spring-framework/reference/6.0/core/aop/ataspectj/advice.html

四、连接点

连接点是在程序执行过程中可以应用切面的点,例如方法调用、方法执行、异常处理等。

1、JoinPoint

JoinPoint是一个接口,是通知的第一个参数,在程序执行过程中它通过反射机制来获取目标对象的方法、参数等等。

JoinPoint接口的主要方法有:

getArgs(): 获取方法的参数数组。

getSignature(): 获取连接点的签名,包括方法名、声明类型等信息。

getTarget(): 获取目标对象。

getThis(): 获取代理对象。

toString(): 获取连接点的信息。

2、ProceedingJoinPoint

ProceedingJoinPoint是JoinPoint的子接口,是通知的第一个参数,主要扩展了2个方法。

proceed(): 执行连接点的方法。如果环绕通知中调用了 proceed() 方法,它会继续执行原始方法;如果不调用,则方法执行将被终止。

proceed(Object[] args): 带参数的 proceed() 方法,可以传入自定义的参数数组。

3、应用示例

@After("publicMethod()") // 声明一个通知public void printLog(JoinPoint joinPoint) { // 打印日志}@Around("execution(* com.xyz..service.*.*(..))")public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { return pjp.proceed();}

连接点到底使用JoinPoint还是ProceedingJoinPoint,根据需求来判断,如果需要继续执行原方法那就使用ProceedingJoinPoint,如果不需要继续执行原方法,基本上使用JoinPoint就够了。

五、目标对象

目标对象是应用程序中真正实现业务逻辑的对象。它是被代理的对象,通常是通过接口或者类来定义的。通过JoinPoint 连接点获取。

joinPoint.getTarget();

六、代理对象

代理对象是在运行时动态创建的对象,它包装了目标对象并提供了额外的功能,也通过JoinPoint 连接点获取。

joinPoint.getThis();

七、引入

引入(Introduction)是一种在现有类中添加新方法或属性的方式,而不需要修改原始类的代码。这可以通过引入新的接口或抽象类来实现。

一个常见的应用示例是通过引入接口来实现日志记录功能。

假设我们有一个接口 MyLoggable 包含一个方法 log(),我们可以通过引入这个接口来给现有的类添加日志记录功能,而不需要修改原始类的代码。

示例代码:

public interface MyLoggable { void log();}

@Component("myLoggable")public class MyLoggableImpl implements MyLoggable { private final Logger logger = LoggerFactory.getLogger(MyLoggableImpl.class);

@Override public void log() { logger.info("-----SpringAOP 引入方式的应用-------"); }}

public class MyService { public void doSomething() { // 业务逻辑 }}

@Aspect@Componentpublic class LoggingAspect { // 1、声明继承关系 @DeclareParents(value = "com.example.service.*", defaultImpl = MyLoggableImpl.class) private Loggable loggable;

// 2、通知 @After("execution(* *(..)) && this(loggable)") public void logBefore(Loggable loggable) { loggable.log(); }}

在这个示例中,MyService类并没有直接实现Loggable接口,但通过AOP里进行继承关系声明,我们就可以在LoggingAspect切面中引入Loggable接口,并在After通知中调用log()方法来实现日志记录。

引入可以用于向目标对象引入新的接口及其实现,从而为目标对象提供额外的功能,而不需要修改目标对象的源代码。

通过这段代码可以发现,引入就是这么一回事,看概念百遍,不如运行代码一遍,有了代码,这个概念就非常好懂了。

八、织入

织入(Weaving)是将切面逻辑应用到目标对象的过程。

一个常见的应用示例是在方法执行前后记录日志。

示例代码:

@Aspect@Componentpublic class LoggingAspect {

@Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("Before method: " + joinPoint.getSignature()); }

@After("execution(* com.example.service.*.*(..))") public void logAfter(JoinPoint joinPoint) { System.out.println("After method: " + joinPoint.getSignature()); }}

我们平时用到的织入偏多。比如利用切面,更改目标对象的某个属性或行为等等。

九、最后总结

上面列出了SpringAOP中的八个概念,每个概念都有在代码中有体现,现在我们写一个综合的代码,把这些概念在复述一遍。

import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.DeclareParents;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Component;

// 一、声明切面@Aspect@Componentpublic class MyLoggingAop { // 二、声明切点 @Pointcut("execution(public * com.example.controller..*(..))") private void pointCutMethod() {}

// 三、1. 通知-----------八、织入方式 @After("pointCutMethod()") public void recordLog(JoinPoint joinPoint) { // 四、连接点应用 // 获取目标对象中的方法 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 五、更改目标对象 joinPoint.getTarget(); // 六、获取代理对象 joinPoint.getThis(); } // 三、2. 通知----------七、引入方式 /** * 七、1. 声明继承关系 */ @DeclareParents(value = "com.example.service.impl.*", defaultImpl = MyLoggableImpl.class) private MyLoggable myLoggable; /** * 七、2. 织入方式应用 * @param myLoggable */ @After("execution(* com.example.service.impl..*.select*(..)) && this(myLoggable)") public void logBefore(MyLoggable myLoggable) { myLoggable.log(); }}

通过上面的代码我们发现,切点是可以省略的,在通知上直接写切点表达式即可。那么实现一个切面功能两步就够了。

概念和代码一一对应,这样概念就更容易理解了,也更容易记住了。有没有感觉,今天的代码比昨天的纯理论有意思多了,可读性也高了。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/O9UEAJg5YAtrGFouCesRm9qg0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券