Spring AOP原理详解-Spring官方原版

简介: Spring AOP原理详解-Spring官方原版

一、概述

面向方面编程(AOP)补充了面向对象编程(OOP) 提供了另一种思考程序结构的方式。模块化的关键单元 在OOP中是类,而在AOP中,模块化的单位是方面。方面 实现跨越问题(如事务管理)的模块化 多种类型和对象。(这种关切通常被称为“跨领域”关切

Spring 的关键组件之一是 AOP 框架。虽然春季国际奥委会 容器不依赖于AOP(这意味着如果您不需要,则不需要使用AOP ),AOP补充了Spring IoC,提供了一个非常有能力的中间件解决方案。

带AspectJ切入点的弹簧AOP

Spring 提供了简单而强大的方法来编写自定义方面,方法是使用基于模式的方法或@AspectJ注释样式。 这两种风格都提供了完全键入的建议和使用AspectJ切入点语言 同时仍然使用春季AOP进行编织。

本章讨论基于架构和@AspectJ的 AOP 支持。

AOP在Spring Framework中用于:

  • 提供声明式企业服务。此类服务最重要的服务是声明式事务管理。
  • 让用户实现自定义方面,通过 AOP 补充他们对 OOP 的使用。

二、AOP 概念

让我们从定义一些核心的AOP概念和术语开始。这些术语不是 弹簧专用。不幸的是,AOP术语并不是特别直观。 但是,如果Spring使用自己的术语,那就更令人困惑了。

  • Aspect:跨多个类的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在SpringAOP中,方面是通过使用正则类(基于模式的方法)或用@Aspect注释(@AspectJ样式)注释的正则类来实现的。
  • Join point:程序执行期间的点,例如执行 方法或异常的处理。在春季AOP中,连接点始终 表示方法执行。
  • Advice:某个方面在特定连接点执行的操作。不同类型的 建议包括“周围”、“之前”和“之后”通知。(建议类型讨论 后来。许多AOP框架,包括Spring,将建议建模为拦截器和 在连接点周围维护拦截器链。
  • Pointcut:与连接点匹配的谓词。建议与 切入点表达式,并在与切入点匹配的任何连接点处运行(例如, 执行具有特定名称的方法)。连接点匹配的概念 by pointcut 表达式是 AOP 的核心,Spring 使用 AspectJ 切入点 默认情况下为表达式语言。
  • Introduction:代表类型声明其他方法或字段。Spring AOP 允许您将新接口(和相应的实现)引入任何 建议对象。例如,您可以使用介绍使 Bean 实现接口IsModified,以简化缓存。(简介称为 AspectJ 社区中的类型间声明。
  • 目标对象:由一个或多个方面建议的对象。也称为 “建议对象”。由于 Spring AOP 是使用运行时代理实现的,因此这 对象始终是代理对象。
  • AOP代理:由AOP框架创建的对象,以实现方面 合约(建议方法执行等)。在 Spring 框架中,AOP 代理 是 JDK 动态代理或 CGLIB 代理。
  • 织入:将方面与其他应用程序类型或对象链接以创建 建议对象。这可以在编译时完成(使用 AspectJ 编译器,用于 示例)、加载时间或运行时。Spring AOP,像其他纯Java AOP框架一样, 在运行时执行编织。

Spring AOP 包括以下类型的通知:

  • @Before(): 切点方法执行之前执行
  • @After(): 切点方法执行之后执行
  • @Around(): 在切点方法执行前、后都可以做一些操作
  • @AfterReturning():切点方法返回结果之后执行
  • @AfterThrowing():异常通知,切点抛出异常时执行

三、SpringAOP能力和目标

Spring AOP是用纯Java实现的。无需特殊编译 过程。Spring AOP 不需要控制类加载器层次结构,因此 适用于 Servlet 容器或应用程序服务器。

Spring AOP 目前仅支持方法执行连接点(建议执行Spring beans的方法)。未实现字段拦截,尽管支持 可以在不破坏核心 Spring AOP API 的情况下添加字段拦截。如果您需要 若要建议字段访问和更新连接点,请考虑使用诸如 AspectJ 之类的语言。

Spring AOP的AOP方法与大多数其他AOP框架不同。目标是 不提供最完整的AOP实现(尽管Spring AOP相当 有能力)。相反,其目的是在AOP实现和 Spring IoC,帮助解决企业应用中的常见问题。

因此,例如,Spring 框架的 AOP 功能通常用于 与 Spring IoC 容器结合使用。使用普通 Bean 配置方面 定义语法(尽管这允许强大的“自动代理”功能)。这是一个 与其他 AOP 实现的关键区别。你不能做某些事情 使用Spring AOP轻松或有效地,例如建议非常细粒度的对象(通常, 域对象)。在这种情况下,AspectJ是最佳选择。然而,我们的 经验是,Spring AOP为大多数问题提供了出色的解决方案 适用于 AOP 的企业 Java 应用程序。

Spring AOP从未试图与AspectJ竞争以提供全面的AOP解决方案。我们认为,基于代理的框架(如SpringAOP)和成熟的框架(例如AspectJ)都是有价值的,它们是互补的,而不是竞争对手。Spring将SpringAOP和IoC与AspectJ无缝集成,以在一致的基于Spring的应用程序架构中实现AOP的所有使用。这种集成不会影响Spring AOP API或AOP Alliance API。Spring AOP保持向后兼容。

Spring 框架的核心原则之一是非侵入性。这 是不应该强迫您引入特定于框架的类的想法,并且 连接到您的业务或领域模型。但是,在某些地方,Spring 框架 确实为您提供了将特定于 Spring 框架的依赖项引入您的 代码库。为您提供此类选项的理由是因为,在某些情况下,它 可能只是更容易阅读或编码某些特定的功能,例如 一种方式。但是,Spring 框架(几乎)总是为您提供选择:您有 自由地做出明智的决定,选择哪种选项最适合您的特定用途 案例或场景。

与本章相关的一个这样的选择是哪个AOP框架(和 哪个 AOP 样式)来选择。您可以选择AspectJ,Spring AOP或两者兼而有之。你 也可以选择@AspectJ注释样式的方法或Spring XML 配置样式方法。本章选择介绍的事实 首先@AspectJ式的方法不应被视为Spring团队的标志 偏爱@AspectJ注释样式的方法,而不是Spring XML配置样式。

四、AOP 代理

Spring AOP 默认对 AOP 代理使用标准 JDK 动态代理。这 允许代理任何接口(或接口集)。

Spring AOP也可以使用CGLIB代理。这对于代理类而不是 接口。默认情况下,如果业务对象未实现 接口。由于对接口而不是类进行编程是一种很好的做法,因此业务 类通常实现一个或多个业务接口。在那些(希望很少见的)情况下,可以强制使用 CGLIB,在这种情况下 需要建议未在接口上声明的方法或需要的地方 将代理对象作为具体类型传递给方法。

重要的是要掌握Spring AOP是基于代理的事实。请参阅了解 AOP 代理以彻底检查这到底是什么 实现细节实际上意味着。

五、@AspectJ支持

@AspectJ是指将方面声明为常规 Java 类的样式,该类使用 附注。@AspectJ风格是由AspectJ项目作为AspectJ 5版本的一部分引入的。春天 使用AspectJ提供的库解释与AspectJ 5相同的注释 用于切入点解析和匹配。不过,AOP 运行时仍然是纯粹的 Spring AOP,并且 不依赖于AspectJ编译器或Weaver。

1. 启用@AspectJ支持

要在Spring配置中使用@AspectJ方面,需要启用Spring支持,以基于@AspectJ方面配置SpringAOP,并基于这些方面是否建议使用自动代理bean。通过自动代理,我们的意思是,如果Spring确定一个bean被一个或多个方面建议,它会自动为该bean生成一个代理,以拦截方法调用,并确保根据需要运行建议。

@AspectJ支持可以通过XML或Java风格的配置启用。在这两种情况下,您还需要确保AspectJ的aspectjweaver.jar库位于应用程序的类路径中(1.8或更高版本)。该库位于AspectJ发行版的lib目录中,或Maven Central存储库中。

使用 Java 配置启用@AspectJ支持

要使用Java@Configuration启用@AspectJ支持,请添加@EnableAspectJAutoProxy注释,如下例所示:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

2.声明切面

启用@AspectJ支持后,Spring会自动检测在应用程序上下文中定义的任何bean,该bean的类是@AspectJaspect(具有@aspect注释),并用于配置SpringAOP。接下来的两个示例显示了一个不太有用的方面所需的最小定义。

两个示例中的第一个显示了应用程序上下文中的常规bean定义,该定义指向具有@Aspect注释的bean类:

@Aspect
public class NotVeryUsefulAspect {
}

Aspect(用@Aspect注释的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点、建议和介绍(类型间)声明。

通过组件扫描自动检测方面

您可以通过@configuration类中的@Bean方法将方面类注册为Spring XML配置中的常规Bean,或者让Spring通过类路径扫描自动检测它们 — 与任何其他Spring管理的bean相同。但是,请注意,@Aspect注释不足以在类路径中自动检测。为此,您需要添加一个单独的@Component注释(或者,根据Spring组件扫描程序的规则,添加一个符合条件的自定义原型注释)。

3. 声明切入点

Spring AOP只支持Spring bean的方法执行连接点,因此您可以将切入点视为与Spring bean上的方法执行相匹配。切入点声明有两个部分:一个由名称和任何参数组成的签名,以及一个切入点表达式,它精确地确定我们感兴趣的方法执行。在AOP的@AspectJ注释样式中,切入点签名由一个常规方法定义提供,并且通过使用@pointcut注释来指示切入点表达式(用作切入点签名的方法必须具有void返回类型)。

一个示例可以帮助明确切入点签名和切入点表达式之间的区别。以下示例定义了一个名为anyOldTransfer的切入点,该切入点与名为transfer的任何方法的执行相匹配:

@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature

形成@pointcut注释值的切入点表达式是一个常规AspectJ切入点表达式。有关AspectJ切入点语言的完整讨论,请参阅《AspectJ编程指南》(以及AspectJ 5开发人员笔记本)或关于AspectJ的书籍之一(如Colyer等人的EclipseAspectJ,或RamnivasLadad的AspectJ in Action)。

支持的切入点

Spring AOP支持以下用于切入点表达式的AspectJ切入点指示符(PCD):

  • execution:用于匹配方法执行连接点。这是使用SpringAOP时要使用的主要切入点指示符。
  • within:将匹配限制为特定类型内的连接点(使用SpringAOP时在匹配类型内声明的方法的执行)。
  • this:将匹配限制为连接点(使用Spring AOP时方法的执行),其中bean引用(Spring AOP代理)是给定类型的实例。
  • target:将匹配限制为连接点(使用SpringAOP时方法的执行),其中目标对象(被代理的应用程序对象)是给定类型的实例。
  • args:将匹配限制为连接点(使用SpringAOP时方法的执行),其中参数是给定类型的实例。
  • @target:将匹配限制为连接点(使用SpringAOP时方法的执行),其中执行对象的类具有给定类型的注释。
  • @args:将匹配限制为连接点(使用SpringAOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。
  • @within:限制匹配到具有给定注释的类型内的连接点(使用SpringAOP时,使用给定注释在类型中声明的方法的执行)。
  • @annotation:将匹配限制为连接点的主题(在Spring AOP中运行的方法)具有给定注释的连接点。

其他切入点

完整的AspectJ切入点语言支持Spring中不支持的其他切入点指示符:call、get、set、预初始化、static初始化、初始化、处理程序、adviceexecution、withincode、cflow、cflowbelow、if、@this和@withincode。在Spring AOP解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException。

SpringAOP支持的一组切入点指示符可以在未来的版本中扩展,以支持更多的AspectJ切入点指示符。

由于Spring AOP仅将匹配限制为方法执行连接点,因此前面对切入点指示符的讨论给出了比AspectJ编程指南中更窄的定义。此外,AspectJ本身具有基于类型的语义,在执行连接点,this和target都引用同一个对象:执行方法的对象。Spring AOP是一个基于代理的系统,它区分了代理对象本身(绑定到这个)和代理后面的目标对象(绑定到目标)。

组合切入点表达式

可以使用&&、|和!组合切入点表达式!。还可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} (1)
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} (2)
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} (3)
  1. anyPublicOperation如果方法执行连接点表示执行,则匹配 任何公共方法。
  2. inTrading如果方法执行在交易模块中,则匹配。
  3. tradingOperation如果方法执行表示 交易模块。

如前所示,最好用较小的命名组件构建更复杂的切入点表达式。当按名称引用切入点时,正常的Java可见性规则适用(您可以看到相同类型的私有切入点、层次结构中的受保护切入点、任何位置的公共切入点等等)。可见性不影响切入点匹配。

共享公共切入点定义

在使用企业应用程序时,开发人员通常希望从几个方面引用应用程序的模块和特定的操作集。为此,我们建议定义一个捕获公共切入点表达式的CommonPointcuts方面。此方面通常类似于以下示例:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.myapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.web..*)")
    public void inWebLayer() {}
    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.myapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.service..*)")
    public void inServiceLayer() {}
    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.myapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    public void inDataAccessLayer() {}
    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
     * the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
    public void businessService() {}
    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}
}

例子

Spring AOP用户可能最经常使用执行切入点指示符。执行表达式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)

除了返回类型模式(前面片段中的ret-type模式)、名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么,以便匹配连接点。*最常用作返回类型模式。它匹配任何返回类型。只有当方法返回给定类型时,完全限定的类型名称才匹配。名称模式与方法名称匹配。您可以使用*通配符作为名称模式的全部或部分。如果指定声明类型模式,请包含尾随。以将其连接到名称模式组件。参数模式稍微复杂一些:()匹配一个不带参数的方法,而(..)匹配任意数量(零个或更多)的参数。(*)模式匹配采用任意类型的一个参数的方法。(*,String)匹配采用两个参数的方法。第一个可以是任何类型,而第二个必须是字符串。有关更多信息,请参阅AspectJ编程指南的语言语义部分。

以下示例显示了一些常见的切入点表达式:

  • 任何公共方法的执行:
execution(public * *(..))
  • 执行任何名称以set开头的方法:
execution(* set*(..))
  • AccountService接口定义的任何方法的执行:
execution(* com.xyz.service.AccountService.*(..))
  • 服务包中定义的任何方法的执行:
execution(* com.xyz.service.*.*(..))
  • 服务包或其子包中定义的任何方法的执行:
execution(* com.xyz.service..*.*(..))
  • 服务包中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service.*)
  • 服务包或其子包中的任何连接点(仅在Spring AOP中执行方法):
within(com.xyz.service..*)
  • 代理实现AccountService接口的任何连接点(仅在Spring AOP中执行方法):
this(com.xyz.service.AccountService)
  • 目标对象实现AccountService接口的任何连接点(仅在Spring AOP中执行方法):
target(com.xyz.service.AccountService)
  • 任何接受单个参数且在运行时传递的参数为Serializable的连接点(仅在Spring AOP中执行方法):
args(java.io.Serializable)

注意,本示例中给出的切入点与执行不同(**(java.io.Serializable))。如果在运行时传递的参数是Serializable,则args版本匹配;如果方法签名声明了Serializable类型的单个参数,则执行版本匹配。

  • 目标对象具有@Transactional注释的任何连接点(仅在Spring AOP中执行方法):
@target(org.springframework.transaction.annotation.Transactional)
  • 目标对象的声明类型具有@Transactional注释的任何连接点(仅在Spring AOP中执行方法):
@within(org.springframework.transaction.annotation.Transactional)
  • 执行方法具有@Transactional注释的任何连接点(仅在Spring AOP中执行方法):
@annotation(org.springframework.transaction.annotation.Transactional)

4. 声明Advice

通知与切入点表达式关联,并在切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。

Before Advice

  • 可以使用@before注释在方面中声明before建议:
@Aspect
public class BeforeExample {
    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}

如果我们使用 in-place pointcut 表达式,我们可以将前面的示例重写为以下示例:

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

After Returning Advice

返回后,当匹配的方法执行正常返回时,将运行建议。您可以使用@AfterReturning注释声明它:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}

有时,您需要在建议主体中访问返回的实际值。您可以使用@AfterReturning的形式来绑定返回值以获得该访问,如下例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}

返回属性中使用的名称必须与通知方法中的参数名称相对应。当方法执行返回时,返回值作为相应的参数值传递给通知方法。return子句还限制仅匹配返回指定类型值的方法执行(在本例中为Object,它匹配任何返回值)。

请注意,在返回建议后使用时,不可能返回完全不同的引用。

After Throwing Advice

当匹配的方法执行通过抛出异常退出时,将运行After throwing advice。您可以使用@AfterThrowing注释来声明它,如下例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }
}

通常,您希望建议仅在引发给定类型的异常时运行,而且您还经常需要访问建议主体中引发的异常。您可以使用through属性来限制匹配(如果需要 — 否则使用Throwable作为异常类型),并将抛出的异常绑定到通知参数。以下示例显示了如何执行此操作:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}

引发属性中使用的名称必须与建议方法中的参数名称相对应。当方法执行通过抛出异常退出时,该异常将作为相应的参数值传递给通知方法。抛出子句还将匹配限制为仅匹配那些抛出指定类型异常(在本例中为DataAccessException)的方法执行。

After (Finally) Advice

After(finally)建议在匹配的方法执行退出时运行。它是通过使用@After注释声明的。事后通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似目的。以下示例显示了如何使用after finally通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}

Around Advice

最后一种建议是围绕着建议。Around建议“围绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定方法何时、如何以及是否真正运行。如果您需要以线程安全的方式共享方法执行前后的状态(例如,启动和停止计时器),通常会使用Around建议。

通过使用@Around注释注释方法来声明Around建议。该方法应将Object声明为其返回类型,并且该方法的第一个参数必须为ProceedingJoinPoint类型。在advice方法的主体中,必须在ProceedingJoinPoint上调用proceed(),才能运行基础方法。在没有参数的情况下调用proceed()将导致调用方的原始参数在被调用时被提供给基础方法。对于高级用例,proceed()方法有一个重载变体,它接受一个参数数组(Object[])。调用基础方法时,数组中的值将用作基础方法的参数。

around通知返回的值是方法调用方看到的返回值。例如,如果一个简单的缓存方面有一个值,它可以从缓存中返回一个值;如果没有,它可以调用proceed()(并返回该值)。请注意,proceed可能被调用一次、多次,或者根本不在周围建议的主体中调用。所有这些都是合法的。

以下示例显示了如何使用规避建议:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}

5.Advice参数

Spring提供了完全类型化的Advice,这意味着您在Advice签名中声明所需的参数(正如我们前面在返回和抛出示例中看到的那样),而不是一直使用Object[]数组。我们将在本节稍后介绍如何使参数和其他上下文值可供建议机构使用。首先,我们看一下如何编写通用建议,以了解建议当前建议的方法。

6.Access to the Current JoinPoint

任何通知方法都可以将org.aspectj.lang.JoinPoint类型的参数声明为其第一个参数。请注意,around通知需要声明ProceedingJoinPoint类型第一个参数,这是JoinPoint的子类。

JoinPoint接口提供了许多有用的方法:

  • getArgs():返回方法参数。
  • getThis():返回代理对象。
  • getTarget():返回目标对象。
  • getSignature():返回建议的方法的说明。
  • toString():打印建议方法的有用说明。

7.向Advice传递参数

我们已经看到了如何绑定返回的值或异常值(在返回后和抛出建议后使用)。要使参数值对建议主体可用,可以使用args的绑定形式。如果在args表达式中使用参数名代替类型名,则在调用建议时,相应参数的值将作为参数值传递。一个例子应该让这一点更清楚。假设您希望建议执行以Account对象作为第一个参数的DAO操作,并且需要访问建议主体中的帐户。您可以编写以下内容:

@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

切入点表达式的args(account,..)部分有两个用途。首先,它将匹配限制为仅在方法至少接受一个参数的情况下执行,并且传递给该参数的参数是Account的实例。其次,它通过Account参数使实际Account对象可用于通知。

另一种编写方法是声明一个切入点,当它与连接点匹配时,该切入点“提供”Account对象值,然后从通知中引用命名的切入点。如下所示:

@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

有关详细信息,请参阅AspectJ编程指南。

代理对象(this)、目标对象(target)和注释(@within、@target、@annotation和@args)都可以以类似的方式绑定。接下来的两个示例显示如何匹配使用@Auditable注释注释的方法的执行并提取审计代码:

两个示例中的第一个显示了@Auditable注释的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

两个示例中的第二个显示了与@Auditable方法的执行相匹配的建议:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}

8.Advice参数和泛型

Spring AOP可以处理类声明和方法参数中使用的泛型。假设您有一个如下所示的泛型类型:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

您可以通过将advice参数绑定到要侦听方法的参数类型,将方法类型的侦听限制为某些参数类型:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

这种方法不适用于泛型集合。因此,不能按如下方式定义切入点:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

为了实现这一点,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理一般的空值。要实现与此类似的功能,必须将参数键入Collection<?>并手动检查元件的类型。

9.确定参数名称

建议调用中的参数绑定依赖于切入点中使用的匹配名称 建议和切入点方法签名中声明的参数名称的表达式。 参数名称无法通过 Java 反射获得,因此 Spring AOP 使用 以下策略来确定参数名称:

  • 如果用户已明确指定参数名称,则使用指定的参数名称。建议和切入点注释都有一个可选的argNames属性,您可以使用它来指定带注释方法的参数名称。这些参数名称在运行时可用。以下示例显示了如何使用argNames属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

如果第一个参数是JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart类型,则可以从argNames属性的值中省略参数的名称。例如,如果修改前面的建议以接收连接点对象,argNames属性不需要包含它:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

对JoinPoint、ProceedingJoinPoint和JoinPoint.StaticPart类型的第一个参数的特殊处理对于不收集任何其他连接点上下文的建议实例来说特别方便。在这种情况下,可以省略argNames属性。例如,以下建议不需要声明argNames属性:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}
  • 使用argNames属性有点笨拙,因此如果尚未指定argNames特性,Spring AOP会查看类的调试信息,并尝试从本地变量表中确定参数名称。只要用调试信息编译了类(至少为-g:vars),就会出现此信息。启用此标志进行编译的后果是:(1)代码稍微容易理解(反向工程),(2)类文件大小稍微大一点(通常无关紧要),(3)编译器不应用删除未使用的局部变量的优化。换言之,打开此标志进行构建时,您应该不会遇到任何困难。
  • 如果代码是在没有必要的调试信息的情况下编译的,Spring AOP会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,而advice方法只接受一个参数,那么这种配对是显而易见的)。如果给定可用信息,变量的绑定不明确,则会引发AmbiguousBindingException。
  • 如果以上所有策略都失败,将抛出IllegalArgumentException。

10.继续处理参数

我们之前说过,我们将描述如何使用在SpringAOP和AspectJ中一致工作的参数编写proceed调用。解决方案是确保建议签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}

六、AspectJ简介

介绍(在AspectJ中称为类型间声明)允许一个方面声明建议的对象实现给定的接口,并代表这些对象提供该接口的实现。

您可以使用@DeclarePrents注释进行介绍。此注释用于声明匹配的类型具有新的父级(因此得名)。例如,给定名为UsageTracked的接口和名为DefaultUsageTracked的接口实现,以下方面声明服务接口的所有实现者也实现UsageTrackedInterface(例如,通过JMX进行统计):

@Aspect
public class UsageTracking {
    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;
    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }
}

要实现的接口由带注释字段的类型确定。@DeclarePrents注释的value属性是AspectJ类型模式。任何匹配类型的bean都实现UsageTracked接口。注意,在前面示例的before建议中,服务bean可以直接用作UsageTracked接口的实现。如果以编程方式访问bean,您将编写以下内容:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

七、Aspect 实例化模型

默认情况下,应用程序上下文中每个方面都有一个实例。AspectJ将其称为单例实例化模型。可以定义具有交替生命周期的方面。Spring支持AspectJ的perthis和pertarget实例化模型;当前不支持perflow、perflowbelow和pertypewithin。

您可以通过在@aspect注释中指定perthis子句来声明perthis方面。考虑以下示例:

@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
    private int someState;
    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}

在前面的示例中,perthis子句的效果是为执行业务服务的每个唯一服务对象创建一个方面实例(每个唯一对象在切入点表达式匹配的连接点处绑定到该对象)。在服务对象上首次调用方法时,将创建方面实例。当服务对象超出范围时,方面超出范围。在创建方面实例之前,其中的任何建议都不会运行。一旦创建了方面实例,在其中声明的建议就会在匹配的连接点运行,但仅当服务对象与此方面关联时。有关每个子句的更多信息,请参阅AspectJ编程指南。

pertarget实例化模型的工作方式与perthis完全相同,但它在匹配的连接点为每个唯一的目标对象创建一个方面实例。

八、AOP 示例

既然您已经了解了所有组成部分是如何工作的,我们可以将它们放在一起做一些有用的事情。

业务服务的执行有时会由于并发问题而失败(例如,死锁失败)。如果重试该操作,下次尝试时可能会成功。对于在这种情况下适合重试的业务服务(不需要返回用户以解决冲突的幂等操作),我们希望透明地重试操作,以避免客户端看到悲观的LockingFailureException。这是一个明显跨越服务层中多个服务的需求,因此非常适合通过一个方面实现。

因为我们想重试该操作,所以需要使用around建议,以便我们可以多次调用proceed。以下列表显示了基本方面的实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {
    private static final int DEFAULT_MAX_RETRIES = 2;
    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;
    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }
    public int getOrder() {
        return this.order;
    }
    public void setOrder(int order) {
        this.order = order;
    }
    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }
}

注意,方面实现了Ordered接口,因此我们可以将方面的优先级设置为高于事务建议(我们希望每次重试时都有一个新的事务)。maxRetries和order财产都是由Spring配置的。主要操作发生在围绕建议的doConcurrentOperation中。注意,目前,我们将重试逻辑应用于每个businessService()。我们尝试继续,如果我们以悲观锁定失败异常失败,我们会再次尝试,除非我们已用尽所有重试尝试。

相应的Spring配置如下:

<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

为了改进方面,使其仅重试幂等操作,我们可以定义以下幂等注释:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

然后,我们可以使用注释来注释服务操作的实现。将方面更改为仅重试幂等操作涉及细化切入点表达式,以便仅匹配@Idempontent操作,如下所示

@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

文章下方有交流学习区!一起学习进步!也可以前往官网,加入官方微信交流群你的支持和鼓励是我创作的动力❗❗❗

官网:Doker 多克; 官方旗舰店:首页-Doker 多克 多克创新科技企业店-淘宝网 全品优惠

目录
相关文章
|
1月前
|
XML 监控 安全
Spring特性之一——AOP面向切面编程
Spring特性之一——AOP面向切面编程
29 1
|
1天前
|
Java Maven 数据安全/隐私保护
详解 Java AOP:面向方面编程的核心概念与 Spring 实现
详解 Java AOP:面向方面编程的核心概念与 Spring 实现
10 1
|
1天前
|
缓存 Java uml
Spring压轴题:当循环依赖遇上Spring AOP
Spring压轴题:当循环依赖遇上Spring AOP
11 1
|
2天前
|
XML 监控 Java
Java一分钟之-Spring AOP:基于Spring的AOP
【6月更文挑战第13天】Spring框架集成AOP支持,便于实现如日志、监控和事务管理等关注点的集中管理。本文探讨Spring AOP的核心概念(切面、切入点、通知和代理),常见问题(代理对象理解不清、切入点表达式错误、通知类型混淆和事务管理配置不当)及其对策,并提供注解式日志记录代码示例。通过学习和实践,开发者能更好地运用Spring AOP提升代码质量。
14 2
|
12天前
|
Java Spring
【JavaEE进阶】 Spring AOP源码简单剖析
【JavaEE进阶】 Spring AOP源码简单剖析
|
12天前
|
Java Spring
【JavaEE进阶】 Spring AOP详解
【JavaEE进阶】 Spring AOP详解
|
12天前
|
数据采集 Java 程序员
【JavaEE进阶】 Spring AOP快速上手
【JavaEE进阶】 Spring AOP快速上手
|
17天前
|
Java Spring
|
18天前
|
Java Spring
|
18天前
|
前端开发 Java Maven
Spring AOP
Spring AOP
20 1