5、Spring 面向切面的编程

5.使用 Spring 进行面向方面的编程

面向方面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。 OOP 中模块化的关键单元是类,而在 AOP 中模块化是方面。可以跨越多种类型和对象。

 

Spring 2.0 引入了一种使用schema-based approach@AspectJ 注解样式来编写自定义切面的更简单,更强大的方法。这两种样式都提供了完全类型化的建议,并使用了 AspectJ 切入点语言,同时仍使用 Spring AOP 进行编织。

本章讨论基于 Spring 2.0 模式和基于@AspectJ 的 AOP 支持。

AOP 在 Spring Framework 中用于:

  • 提供声明性企业服务,尤其是作为 EJB 声明性服务的替代品。最重要的此类服务是声明式 TransactionManagement

  • 让用户实现自定义方面,以 AOP 补充其对 OOP 的使用。

5.1. AOP 概念

  • 切面(aspect):通知+切入点。

  • 通知(advice):除了目标方法执行之外的操作都称为通知。比如:事务通知,记录目标方法执行时长的通知。通知一般由开发者开发。

  • 切入点(pointcut):指定项目中的哪些类中的哪些方法应用通知,切入点是配置得到的。

  • 连接点(Join point):例如:servlet中的longin()就是连接点;所以连接点在spring中它永远是一个方法。也可以说'目标对象中的方法就是一个连接点‘。
  • 目标对象(Target object):原始对象。

  • 代理对象(AOP proxy): 包含了原始对象的代码和增强后的代码的那个对象。

 

应用场景:日志记录,权限验证,效率检查,事务管理......

 

Spring AOP 包括以下类型的建议:

  • 在建议之前:在连接点之前运行的建议,但是它不能阻止执行流程前进到连接点(除非它引发异常)。

  • 返回建议后:在连接点正常完成后要运行的建议(例如,如果方法返回而没有引发异常)。

  • 抛出建议后:如果方法因抛出异常而退出,则执行建议。

  • 建议之后(最终):无论连接点退出的方式如何(正常或特殊返回),均应执行建议。

  • 围绕建议:围绕连接点的建议,例如方法调用。这是最有力的建议。周围建议可以在方法调用之前和之后执行自定义行为。它还负责选择是返回连接点还是通过返回其自身的返回值或引发异常来捷径建议的方法执行。

 

围绕建议是最通用的建议。由于 Spring AOP 与 AspectJ 一样,提供了各种建议类型,因此我们建议您使用功能最弱的建议类型,以实现所需的行为。例如,如果您只需要使用方法的返回值更新缓存,则最好使用返回后的建议而不是周围的建议,尽管周围的建议可以完成相同的事情。使用最具体的建议类型可以提供更简单的编程模型,并减少出错的可能性。

 

在 Spring 2.0 中,所有建议参数都是静态类型的,因此您可以使用适当类型(例如,从方法执行返回值的类型)而不是Object数组的建议参数。

 

切入点匹配的连接点的概念是 AOP 的关键,它与仅提供拦截功能的旧技术有所不同。切入点使建议的目标独立于面向对象的层次结构。例如,您可以将提供声明性事务 Management 的环绕建议应用于跨越多个对象(例如服务层中的所有业务操作)的一组方法。

 

5.2. SpringAOP 能力和目标

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

5.3. AOP 代理

Spring AOP 默认将标准 JDK 动态代理用于 AOP 代理。这使得可以代理任何接口(或一组接口)。

Spring AOP 也可以使用 CGLIB 代理。这对于代理类是必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于对接口进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。在某些情况下(可能极少发生),您需要通知未在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法,则可以使用强制使用 CGLIB

5.4. @AspectJ 支持

@AspectJ 是一种将切面声明为带有注解的常规 Java 类的样式。 @AspectJ 样式是AspectJ project作为 AspectJ 5 版本的一部分引入的。 Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ 5 相同的 注解。但是,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

5.4.1. 启用@AspectJ 支持

要在Spring配置中使用@AspectJ切面,您需要启用Spring支持,以便基于@AspectJ切面配置Spring AOP,并基于这些切面是否通知自动代理beans。我们的意思是,通过自动代理,如果 Spring 确定一个或多个切面通知一个 bean,它会自动为该 bean 生成一个代理来拦截方法调,用并确保按需执行通知。

可以使用 XML 或 Java 样式的配置来启用@AspectJ 支持。无论哪种情况,都需要确保 AspectJ 的aspectjweaver.jar库位于应用程序的 Classpath(版本 1.8 或更高版本)上。该库在 AspectJ 发行版的lib目录中或从 Maven Central 存储库中可用。

通过 Java 配置启用@AspectJ 支持

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

 

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
通过 XML 配置启用@AspectJ 支持

要通过基于 XML 的配置启用@AspectJ 支持,请使用aop:aspectj-autoproxy元素,如以下示例所示:  

<aop:aspectj-autoproxy/>

5.4.2. 声明一个切面

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

 

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

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
</bean>

 

这两个示例中的第二个示例显示了NotVeryUsefulAspect类定义,该类定义带有org.aspectj.lang.annotation.Aspect注解;  

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

切面(带有@Aspect注解 的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点,通知和引入(类型间)声明。  

 

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

可以将切面类注册为 Spring XML 配置中的常规 bean,也可以通过 Classpath 扫描来自动检测它们-与其他任何 SpringManagement 的 bean 一样。但是,请注意,@Aspect注解 不足以在 Classpath 中进行自动检测。为此,您需要添加一个单独的@Component注解(或者,或者,按照 Spring 的组件扫描程序的规则,有条件的自定义构造型注解)。

 

在 Spring AOP 中,切面本身不能成为其他切面的通知目标。类上的@Aspect注解 将其标记为一个切面,因此将其从自动代理中排除。

5.4.3. 声明切入点

切入点确定了感兴趣的连接点,从而使我们能够控制执行建议的时间。 Spring AOP 仅支持 Spring Bean 的方法执行连接点,因此您可以将切入点视为与 Spring Bean 上的方法执行相匹配。切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该切入点表达式准确确定我们感兴趣的方法执行。在 AOP 的@AspectJ 注解样式中,常规方法定义提供了切入点签名。 并通过使用@Pointcut注解 指示切入点表达式(用作切入点签名的方法必须具有void返回类型)。

一个示例可能有助于使切入点签名和切入点表达式之间的区别变得清晰。下面的示例定义一个名为anyOldTransfer的切入点,该切入点与任何名为transfer的方法的执行相匹配:

@Pointcut("execution(* transfer(..))")// 切入点表达式
private void anyOldTransfer() {}// 切入点签名

形成@Pointcut注解的值的切入点表达式是一个常规的 AspectJ 5 切入点表达式。  

 

您可以在需要切入点表达式的任何地方引用在这样的方面中定义的切入点。例如,要使服务层具有事务性,您可以编写以下内容:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

基于架构的 AOP 支持中讨论了<aop:config><aop:advisor>元素。Transaction 元素在Transaction Management中讨论。  

5.4.4. 通知

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

Before Advice

您可以使用@Before注解 在方面中在建议之前声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

如果使用就地切入点表达式,则可以将前面的示例重写为以下示例:  

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

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

}
返回建议后

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

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

Note

您可以在同一切面内拥有多个通知声明(以及其他成员)。在这些示例中,我们仅显示单个通知声明,以集中每个通知的效果。

 

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

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

returning属性中使用的名称必须与 advice 方法中的参数名称相对应。当方法执行返回时,该返回值将作为相应的参数值传递到通知方法。 returning子句还将匹配仅限制为返回指定类型值(在这种情况下为Object,该值与任何返回值匹配)的那些方法执行。  

 

抛出建议后  

抛出建议后,当匹配的方法执行通过抛出异常退出时运行建议。您可以使用@AfterThrowing注解进行声明,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

}

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

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

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

(最后)建议后

当匹配的方法执行退出时,通知(最终)运行。通过使用@After注解 进行声明。之后必须准备处理正常和异常返回条件的建议。它通常用于释放资源和类似目的。以下示例显示了最终建议后的用法:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

}
Around Advice

最后一种建议是围绕建议。围绕建议在匹配方法的执行过程中“围绕”运行。它有机会在方法执行之前和之后进行工作,并确定何时,如何以及甚至根本不执行该方法。如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用绕行建议。始终使用最不符合要求的建议形式(即,在建议可以使用之前,不要在建议周围使用)。

周围的建议通过使用@Around注解 来声明。咨询方法的第一个参数必须为ProceedingJoinPoint类型。在建议的正文中,在ProceedingJoinPoint上调用proceed()会使底层方法执行。 proceed方法也可以传入Object[]。数组中的值用作方法执行时的参数。

Note

对于由 AspectJ 编译器编译的周围建议,使用Object[]调用时proceed的行为与proceed的行为稍有不同。对于使用传统的 AspectJ 语言编写的环绕通知,传递给proceed的参数数量必须与传递给环绕通知的参数数量(而不是基础连接点采用的参数数量)相匹配,并且传递给值的值必须与给定的参数位置会取代该值绑定到的实体的连接点处的原始值(不要担心,如果这现在没有意义)。 Spring 采取的方法更简单,并且更适合其基于代理的,仅执行的语义。如果您编译为 Spring 编写的@AspectJ 方面,并将proceed与 AspectJ 编译器和 weaver 的参数一起使用,则只需要意识到这种区别。有一种方法可以编写在 Spring AOP 和 AspectJ 之间 100%兼容的方面。

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.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}

周围建议返回的值是该方法的调用者看到的返回值。例如,如果一个简单的缓存方面有一个值,则可以从缓存中返回一个值;如果没有,则调用proceed()。请注意,proceed可能在周围建议的正文中被调用一次,多次或完全不被调用。所有这些都是合法的。  

通知参数

Spring 提供了完全类型化的建议,这意味着您可以在建议签名中声明所需的参数(如我们先前在返回和抛出示例中所见),而不是一直使用Object[]数组。

本节的后面部分介绍如何使参数和其他上下文值可用于建议主体。首先,我们看一下如何编写通用建议,以了解当前建议的方法。  

 

访问当前的 JoinPoint

任何通知方法都可以将类型org.aspectj.lang.JoinPoint的参数声明为第一个参数(请注意,在周围的通知中必须声明类型JoinPoint的子类ProceedingJoinPoint的第一个参数。JoinPoint接口提供了许多有用的方法:

  • getArgs():返回方法参数。

  • getThis():返回代理对象。

  • getTarget():返回目标对象。

  • getSignature():返回建议使用的方法的描述。

  • toString():打印有关所建议方法的有用描述。

将参数传递给通知

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

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

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

 

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

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)") 
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)") public void validateAccount(Account account) { // ... }

  

代理对象(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();
    // ...
}

 

通知参数和泛型  

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
}

  

确定参数名称

通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。通过 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
}

 

如果第一个参数是JoinPointProceedingJoinPointJoinPoint.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
}

 

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

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

 

Note

如果即使没有调试信息,AspectJ 编译器(ajc)都已编译@AspectJ 方面,则无需添加argNames属性,因为编译器会保留所需的信息。

 

处理参数  

前面我们提到过,我们将描述如何使用在 Spring AOP 和 AspectJ 上始终有效的参数编写proceed调用。解决方案是确保建议签名按 Sequences 绑定每个方法参数。以下示例显示了如何执行此操作:

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

 

Advice Ordering

当多条建议都希望在同一连接点上运行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定建议执行的 Sequences。优先级最高的建议首先“在途中”运行(因此,给定两条优先建议,则优先级最高的建议首先运行)。从连接点“出路”中,优先级最高的建议将最后运行(因此,给定两条后置通知,优先级最高的建议将第二次运行)。

当在不同方面定义的两条建议都需要在同一连接点上运行时,除非另行指定,否则执行 Sequences 是不确定的。您可以通过指定优先级来控制执行 Sequences。通过在 Aspect 类中实现org.springframework.core.Ordered接口或使用Order注解对其进行 注解,可以通过普通的 Spring 方法来完成。给定两个方面,从Ordered.getValue()返回较低值(或注解值)的方面具有较高的优先级。

当在相同方面定义的两条建议都需要在同一连接点上运行时,其 Sequences 是未定义的(因为无法通过反射来获取 javac 编译类的声明 Sequences)。考虑将这些建议方法折叠成每个方面类中每个连接点的一个建议方法,或将建议重构为单独的方面类,您可以在方面级别进行 Order。

  

5.4.5. Introductions

简介使切面可以声明建议对象实现给定的接口,并代表那些对象提供该接口的实现。

您可以使用@DeclareParents注解 进行介绍。此注解用于声明匹配类型具有新的父代(因此而得名)。例如,在给定名为UsageTracked的接口和该接口名为DefaultUsageTracked的实现的情况下,以下方面声明服务接口的所有实现者也都实现UsageTracked接口(例如,通过 JMX 公开统计信息):

@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

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

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

5.4.7. AOP 示例

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

因为我们想重试该操作,所以我们需要使用“周围”建议,以便我们可以多次调用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.SystemArchitecture.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接口,因此我们可以将切面的优先级设置为高于事务建议(每次重试时都希望有新的事务)。 maxRetriesorder属性均由 Spring 配置。主要动作发生在doConcurrentOperation周围建议中。请注意,目前,我们将重试逻辑应用于每个businessService()。我们尝试 continue,如果失败了PessimisticLockingFailureException,我们将重试,除非我们用尽了所有的重试尝试。  

<aop:aspectj-autoproxy/>

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

 

为了优化切面,使其仅重试幂等操作,我们可以定义以下Idempotent注解:  

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

然后,我们可以使用注解来注解服务操作的实现。方面的更改仅重试幂等操作涉及精简切入点表达式,以便只有@Idempotent个操作匹配,如下所示:  

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

 

5.5. 基于架构的 AOP 支持

如果您更喜欢基于 XML 的格式,Spring 还支持使用新的aop名称空间标签定义方面。支持与使用@AspectJ 样式时完全相同的切入点表达式和建议类型。  

要使用本节中描述的 aop 名称空间标签,您需要导入spring-aop模式,如基于 XML 模式的配置中所述。有关如何在aop名称空间中导入标签的信息,请参见AOP 模式

在您的 Spring 配置中,所有切面和顾问元素都必须放在<aop:config>元素内(在应用程序上下文配置中可以有多个<aop:config>元素)。 <aop:config>元素可以包含切入点,顾问和切面元素(请注意,这些元素必须按此 Sequences 声明)。

 

Warning

<aop:config>样式的配置大量使用了 Spring 的auto-proxying机制。如果您已经通过使用BeanNameAutoProxyCreator或类似方法来使用显式自动代理,则可能会导致问题(例如未编制建议)。推荐的用法模式是仅使用<aop:config>样式或仅AutoProxyCreator样式,并且不要混合使用。

5.5.1. 声明一个切面

 使用模式支持时,切面是在 Spring 应用程序上下文中定义为 Bean 的常规 Java 对象。状态和行为在对象的字段和方法中捕获,切入点和建议信息在 XML 中捕获。

 您可以使用\ <>元素声明一个方面,并使用ref属性引用该支持 bean,如以下示例所示:

 

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
    ...
</bean>

支持方面(在这种情况下为aBean)的 bean 当然可以像配置任何其他 Spring bean 一样进行配置并注入依赖项。  

 

5.5.2. 声明切入点

您可以在<aop:config>元素中声明命名的切入点,从而使切入点定义在多个方面和顾问程序之间共享。

可以定义代表服务层中任何业务服务的执行的切入点 

<aop:config>

    <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

注意,切入点表达式本身使用的是与@AspectJ support中所述的 AspectJ 切入点表达式语言。

 

如果使用基于架构的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下:  

<aop:config>

    <aop:pointcut id="businessService" expression="com.xyz.myapp.SystemArchitecture.businessService()"/>

</aop:config>

  

假定您具有共享通用切入点定义中所述的SystemArchitecture外观。

然后,在方面中声明切入点与声明顶级切入点非常相似,如以下示例所示:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        ...

    </aop:aspect>

</aop:config>

 

与@AspectJ 方面几乎相同,使用基于架构的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点收集this对象作为连接点上下文,并将其传递给建议:  

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...

    </aop:aspect>

</aop:config>

必须声明通知,以通过包含匹配名称的参数来接收收集的连接点上下文,如下所示:  

public void monitor(Object service) {
    ...
}

 

组合切入点子表达式时,XML 文档中的&&很尴尬,因此可以分别使用andornot关键字代替&&||!。例如,上一个切入点可以更好地编写如下:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service..(..)) and this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>
</aop:config>

  

5.5.3. 宣告建议

基于模式的 AOP 支持使用与@AspectJ 样式相同的五种建议,并且它们具有完全相同的语义。

Before Advice

在运行匹配的方法之前,建议运行之前。使用\ <>元素在<aop:aspect>内部声明它,如以下示例所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

  

在这里,dataAccessOperation是在最高(<aop:config>)级别定义的切入点的id。要定义切入点内联,请用pointcut属性替换pointcut-ref属性,如下所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before
        pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
        method="doAccessCheck"/>

    ...

</aop:aspect>

method属性标识提供建议正文的方法(doAccessCheck)。必须为包含建议的 Aspect 元素所引用的 bean 定义此方法。在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用 Aspect Bean 上的doAccessCheck方法。  

返回建议后

返回的建议在匹配的方法执行正常完成时运行。在<aop:aspect>内部以与建议之前相同的方式声明它。以下示例显示了如何声明它:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        method="doAccessCheck"/>

    ...

</aop:aspect>

  

与@AspectJ 样式一样,您可以在建议正文中获取返回值。为此,使用 returning 属性指定返回值应传递到的参数的名称,如以下示例所示:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

doAccessCheck方法必须声明一个名为retVal的参数。该参数的类型以与@AfterReturning相同的方式约束匹配。例如,您可以声明方法签名,如下所示:  

public void doAccessCheck(Object retVal) {...

 

提出建议后

抛出建议后,当匹配的方法执行通过抛出异常退出时执行建议。通过使用掷后元素在<aop:aspect>内部声明它,如以下示例所示:  

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

  

与@AspectJ 样式一样,您可以在通知正文中获取引发的异常。为此,请使用 throwing 属性指定异常应传递到的参数的名称,如以下示例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

doRecoveryActions方法必须声明一个名为dataAccessEx的参数。该参数的类型以与@AfterThrowing相同的方式约束匹配。例如,方法签名可以声明如下:  

public void doRecoveryActions(DataAccessException dataAccessEx) {...

  

(最后)建议后

无论最终如何执行匹配的方法,建议(最终)都会运行。您可以使用after元素对其进行声明,如以下示例所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>

  

环绕通知

最后一种建议是围绕建议。围绕建议在匹配的方法执行过程中“围绕”运行。它有机会在方法执行之前和之后进行工作,并确定何时,如何以及什至根本不执行该方法。周围建议通常用于以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态。始终使用最不强大的建议形式,以满足您的要求。如果建议可以完成这项工作,请不要在建议周围使用。

 

您可以使用aop:around元素在建议周围进行声明。咨询方法的第一个参数必须为ProceedingJoinPoint类型。在建议的正文中,在ProceedingJoinPoint上调用proceed()会导致基础方法执行。 proceed方法也可以用Object[]调用。数组中的值用作方法执行时的参数。有关使用Object[]调用proceed的说明,请参见Around Advice。以下示例显示了如何在 XML 中围绕建议进行声明:

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...

</aop:aspect>

doBasicProfiling通知的实现可以与@AspectJ 示例完全相同(当然要减去 注解),如以下示例所示:  

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

  

通知参数  

基于架构的声明样式以与@AspectJ 支持相同的方式支持完全类型的建议,即通过名称与建议方法参数匹配切入点参数。有关详情,请参见Advice Parameters。如果您希望显式指定建议方法的参数名称(不依赖于先前描述的检测策略),则可以通过使用建议元素的arg-names属性来实现,该属性与argNames属性的处理方式相同。建议 注解(如确定参数名称中所述)。以下示例显示如何在 XML 中指定参数名称:

<aop:before
    pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
    method="audit"
    arg-names="auditable"/>

arg-names属性接受逗号分隔的参数名称列表。

以下基于 XSD 的方法中涉及程度稍高的示例显示了一些与一些强类型参数结合使用的建议:  

package x.y.service;

public interface PersonService {

    Person getPerson(String personName, int age);
}

public class DefaultFooService implements FooService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}

接下来是方面。请注意,profile(..)方法接受许多强类型的参数,第一个恰好是用于进行方法调用的连接点。此参数的存在表明profile(..)用作around建议,如以下示例所示:  

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class SimpleProfiler {

    public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
        StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
        try {
            clock.start(call.toShortString());
            return call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
    }
}

最后,以下示例 XML 配置影响了特定连接点的上述建议的执行:  

<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 http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
    <bean id="personService" class="x.y.service.DefaultPersonService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomePersonServiceMethod"
                expression="execution(* x.y.service.PersonService.getPerson(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
                method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>

考虑以下驱动程序脚本:  

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        PersonService person = (PersonService) ctx.getBean("personService");
        person.getPerson("Pengo", 12);
    }
}

  

通知顺序

当需要在同一连接点(执行方法)上执行多个建议时,排序规则如Advice Ordering中所述。方面之间的优先级是通过将Order注解添加到支持方面的 Bean 或通过使 Bean 实现Ordered接口来确定的。

 

5.5.4. Introductions

简介(在 AspectJ 中称为类型间声明)使方面可以声明建议的对象实现给定的接口,并代表那些对象提供该接口的实现。

您可以使用aop:aspect内的aop:declare-parents元素进行介绍。您可以使用aop:declare-parents元素来声明匹配类型具有新的父代(因此而得名)。例如,给定名为UsageTracked的接口和该名为DefaultUsageTracked的接口的实现,以下方面声明服务接口的所有实现者也都实现UsageTracked接口。 (例如,为了通过 JMX 公开统计信息.)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.SystemArchitecture.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

支持usageTracking bean 的类将包含以下方法:  

public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

要实现的接口由implement-interface属性确定。 types-matching属性的值是 AspectJ 类型的模式。任何匹配类型的 bean 都实现UsageTracked接口。请注意,在前面示例的建议中,服务 Bean 可以直接用作UsageTracked接口的实现。要以编程方式访问 bean,可以编写以下代码:  

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

 

5.5.6. Advisors

“顾问”的概念来自 Spring 中定义的 AOP 支持,并且在 AspectJ 中没有直接等效的概念。顾问就像一个独立的小方面,只有一条建议。通知本身由 bean 表示,并且必须实现Spring 的建议类型中描述的建议接口之一。顾问可以利用 AspectJ 切入点表达式。

Spring 通过<aop:advisor>元素支持顾问程序概念。您通常会看到它与事务建议结合使用,事务建议在 Spring 中也有自己的名称空间支持。以下示例显示顾问程序:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor
        pointcut-ref="businessService"
        advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

除了前面的示例中使用的pointcut-ref属性,还可以使用pointcut属性来内联定义切入点表达式。

要定义顾问程序的优先级,以便该建议书可以参与 Order,请使用order属性来定义顾问程序的Ordered值。

 

5.5.7. AOP 模式示例

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

因为我们想重试该操作,所以我们需要使用“周围”建议,以便我们可以多次调用proceed。以下清单显示了基本方面的实现(这是使用模式支持的常规 Java 类):

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;
    }

    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接口,因此我们可以将方面的优先级设置为高于事务建议(每次重试时都希望有新的事务)。 maxRetriesorder属性均由 Spring 配置。主要动作发生在doConcurrentOperation周围建议方法中。我们尝试 continue。如果我们失败了PessimisticLockingFailureException,我们将重试,除非我们用尽了所有的重试尝试。  

  

Note

该类与@AspectJ 示例中使用的类相同,但是除去了 注解。

相应的 Spring 配置如下:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

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

请注意,目前我们假设所有业务服务都是幂等的。如果不是这种情况,我们可以通过引入Idempotent注解 并使用该注解来注解服务操作的实现,来改进方面,使其仅重试 true 的幂等操作,如以下示例所示:  

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

方面的更改仅重试幂等操作涉及精简切入点表达式,以便只有@Idempotent个操作匹配,如下所示:  

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

  

5.6. 选择要使用的 AOP 声明样式

一旦确定方面是实现给定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ 以及在 Aspect 语言(代码)样式,@ AspectJ 注解样式或 Spring XML 样式之间做出选择?这些决定受许多因素影响,包括应用程序需求,开发工具以及团队对 AOP 的熟悉程度。  

5.6.1. Spring AOP 还是 Full AspectJ?

使用最简单的方法即可。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/编织器。如果您只需要建议在 Spring bean 上执行操作,则 Spring AOP 是正确的选择。如果您需要建议不受 Spring 容器 Management 的对象(通常是域对象),则需要使用 AspectJ。如果您希望建议除简单方法执行以外的连接点(例如,字段 get 或设置连接点等),则还需要使用 AspectJ。

使用 AspectJ 时,可以选择 AspectJ 语言语法(也称为“代码样式”)或@AspectJ注解 样式。显然,如果您不使用 Java 5,那么将为您做出选择:使用代码样式。如果方面在您的设计中起着重要作用,并且您能够将AspectJ 开发工具(AJDT)插件用于 Eclipse,则 AspectJ 语言语法是首选。它更干净,更简单,因为该语言是专为编写方面而设计的。如果您不使用 Eclipse 或只有少数几个方面在您的应用程序中不起作用,那么您可能要考虑使用@AspectJ 样式,在 IDE 中坚持常规 Java 编译,并向其中添加方面编织阶段您的构建脚本。

 

5.6.2. @AspectJ 或 Spring AOP 的 XML?

如果选择使用 Spring AOP,则可以选择@AspectJ 或 XML 样式。有各种折衷考虑。

XML 样式可能是现有 Spring 用户最熟悉的,并且得到了 true 的 POJO 的支持。当使用 AOP 作为配置企业服务的工具时,XML 可能是一个不错的选择(一个很好的测试是您是否将切入点表达式视为配置的一部分,您可能希望独立更改)。使用 XML 样式,可以说从您的配置中可以更清楚地了解系统中存在哪些方面。

XML 样式有两个缺点。首先,它没有完全将要解决的需求的实现封装在一个地方。 DRY 原则说,系统中的任何知识都应该有单一,明确,Authority 的表示形式。当使用 XML 样式时,关于如何实现需求的知识会在配置文件中的后备 bean 类的声明和 XML 中分散。当您使用@AspectJ 样式时,此信息将封装在一个单独的模块中:方面。其次,与@AspectJ 样式相比,XML 样式在表达能力上有更多限制:仅支持“单例”方面实例化模型,并且无法组合以 XML 声明的命名切入点。例如,使用@AspectJ 样式,您可以编写如下内容:

@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

  

在 XML 样式中,您可以声明前两个切入点:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML 方法的缺点是无法通过组合这些定义来定义accountPropertyAccess切入点。

@AspectJ 样式支持其他实例化模型和更丰富的切入点组合。它具有将方面保持为模块化单元的优势。它还具有的优点是,Spring AOP 和 AspectJ 都可以理解@AspectJ 方面。因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,则可以轻松地迁移到基于 AspectJ 的方法。总而言之,只要您拥有比简单地配置企业服务更多的功能,Spring 团队就会喜欢@AspectJ 样式。

  

 

5.7. 混合方面类型

通过使用自动代理支持,模式定义的<aop:aspect>方面,<aop:advisor>声明的顾问程序,甚至是在同一配置中使用 Spring 1.2 样式定义的代理和拦截器,完全可以混合使用@AspectJ 样式的方面。所有这些都是通过使用相同的基础支持机制实现的,并且可以毫无困难地共存。

5.8. 代理机制

Spring AOP 使用 JDK 动态代理或 CGLIB 创建给定目标对象的代理。 (只要有选择,首选 JDK 动态代理)。

如果要代理的目标对象实现至少一个接口,则使用 JDK 动态代理。代理了由目标类型实现的所有接口。如果目标对象未实现任何接口,则将创建 CGLIB 代理。

如果要强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅是由其接口实现的方法),都可以这样做。但是,您应该考虑以下问题:

  • 不能建议final方法,因为它们不能被覆盖。

  • 从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的项目 Classpath 中,因为 CGLIB 类在org.springframework下重新打包并直接包含在 spring-core JAR 中。这意味着基于 CGLIB 的代理支持“有效”,就像 JDK 动态代理始终具有的一样。

  • 从 Spring 4.0 开始,由于 CGLIB 代理实例是通过 Objenesis 创建的,因此不再调用代理对象的构造函数两次。仅当您的 JVM 不允许绕过构造函数时,您才可能从 Spring 的 AOP 支持中看到两次调用和相应的调试日志条目。

 

要强制使用 CGLIB 代理,请将<aop:config>元素的proxy-target-class属性的值设置为 true,如下所示:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用@AspectJ 自动代理支持时强制 CGLIB 代理,请将<aop:aspectj-autoproxy>元素的proxy-target-class属性设置为true,如下所示:  

<aop:aspectj-autoproxy proxy-target-class="true"/>

  

Note

多个<aop:config/>节在运行时折叠到一个统一的自动代理创建器中,该创建器将应用<aop:config/>节中的任何(通常来自不同 XML bean 定义文件)指定的* strong *代理设置。这也适用于<tx:annotation-driven/><aop:aspectj-autoproxy/>元素。

明确地说,在<tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/>元素上使用proxy-target-class="true"会强制对所有三个元素*使用 CGLIB 代理。

 

5.8.1. 了解 AOP 代理

Spring AOP 是基于代理的。在编写自己的方面或使用 Spring Framework 随附的任何基于 Spring AOP 的方面之前,掌握最后一条语句实际含义的语义至关重要。

首先考虑您有一个普通的,未经代理的,没有什么特别的,直接的对象引用的情况,如以下代码片段所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图和清单所示:  

 

 

public class Main {

    public static void main(String[] args) {

        Pojo pojo = new SimplePojo();

        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

当 Client 端代码具有的引用是代理时,情况会稍有变化。考虑以下图表和代码片段:  

 

 

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

这里要理解的关键是,Main类的main(..)方法内部的 Client 端代码具有对代理的引用。这意味着该对象引用上的方法调用是代理上的调用。结果,代理可以委派给与该特定方法调用相关的所有拦截器(建议)。但是,一旦调用最终到达目标对象(在这种情况下为SimplePojo,则为this.bar()this.foo()),则将针对this引用而不是对this引用调用它可能对其自身进行的任何方法调用。代理。这具有重要的意义。这意味着自调用不会导致与方法调用相关的建议得到执行的机会。  

好吧,那么该怎么办?最好的方法是重构代码,以免发生自调用。这确实需要您做一些工作,但这是最好的,侵入性最小的方法。

 

下一种方法绝对可怕,我们正要指出这一点,恰恰是因为它是如此可怕。您可以完全将类中的逻辑与 Spring AOP 绑定在一起,如以下示例所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

这将您的代码完全耦合到 Spring AOP,并且使类本身意识到在 AOP 上下文中使用它这一事实,而 AOP 却是事实。创建代理时,还需要一些其他配置,如以下示例所示:  

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.adddInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

最后,必须注意,AspectJ 没有此自调用问题,因为它不是基于代理的 AOP 框架。

 

5.9. 以编程方式创建@AspectJ 代理

除了使用<aop:config><aop:aspectj-autoproxy>声明配置中的各个方面外,还可以通过编程方式创建建议目标对象的代理。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory类为一个或多个@AspectJ 方面建议的目标对象创建代理。此类的基本用法非常简单,如以下示例所示:

// create a factory that can generate a proxy for the given target object创建一个可以为给定目标对象生成代理的工厂
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// 添加一个方面,类必须是@AspectJ方面
// 对于不同的方面,您可以根据需要多次调用它
factory.addAspect(SecurityManager.class);

// 您还可以添加现有的方面实例,提供的对象类型必须是@AspectJ方面
factory.addAspect(usageTracker);

// 现在获取代理对象...
MyInterfaceType proxy = factory.getProxy();

  

5.10. 在 Spring 应用程序中使用 AspectJ

到目前为止,本章介绍的所有内容都是纯 Spring AOP。在本节中,我们将研究如果您的需求超出了 Spring AOP 所提供的功能,那么如何使用 AspectJ 编译器或 weaver 代替 Spring AOP 或除 Spring AOP 之外使用。

Spring 附带了一个小的 AspectJ 方面库,该库在您的发行版中可以作为spring-aspects.jar独立使用。您需要将其添加到 Classpath 中才能使用其中的方面。 

 

5.10.1. 使用 AspectJ 通过 Spring 依赖注入域对象

Spring 容器实例化并配置在您的应用程序上下文中定义的 bean。给定包含要应用的配置的 Bean 定义的名称,也可以要求 Bean 工厂配置预先存在的对象。 spring-aspects.jar包含注解驱动的切面,该切面利用此功能允许任何对象的依赖项注入。该支架旨在用于在任何容器的控制范围之外创建的对象。域对象通常属于此类,因为它们通常是通过new运算符或通过 ORM 工具以数据库查询的方式通过程序创建的。

@Configurable注解 将一个类标记为符合 Spring 驱动的配置。在最简单的情况下,您可以将其纯粹用作标记 注解,如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

当以这种方式用作标记接口时,Spring 通过使用具有与完全限定类型名称(com.xyz.myapp.domain.Account)同名的 bean 定义(通常为原型作用域)来配置带注解类型的新实例(在本例中为Account)。 。由于 Bean 的默认名称是其类型的完全限定名称,因此声明原型定义的便捷方法是省略id属性,如以下示例所示:  

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要显式指定要使用的原型 bean 定义的名称,则可以直接在注解中这样做,如以下示例所示:  

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring 现在查找名为account的 bean 定义,并将其用作配置新Account实例的定义。

您也可以使用自动装配来避免完全指定专用的 bean 定义。要让 Spring 应用自动装配,请使用@Configurable注解的autowire属性。您可以分别按类型或名称指定@Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME自动布线。或者,从 Spring 2.5 开始,最好在字段或方法级别使用@Autowired@Inject@Configurable bean 指定显式的,注解 驱动的依赖项注入

最后,您可以使用dependencyCheck属性(例如@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))为新创建和配置的对象中的对象引用启用 Spring 依赖项检查。如果此属性设置为true,则 Spring 在配置后验证是否已设置所有属性(不是基元或集合)。  

请注意,单独使用注解不会执行任何操作。注解 中存在的是spring-aspects.jar中的AnnotationBeanConfigurerAspect。从本质上讲,方面说:“在从带有@Configurable注解 的类型的新对象的初始化返回之后,根据注解的属性使用 Spring 配置新创建的对象”。在这种情况下,“初始化”是指新实例化的对象(例如,用new运算符实例化的对象)以及正在反序列化(例如,通过readResolve())的Serializable对象。

 

Note

上段中的关键短语之一是“本质上”。对于大多数情况,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”是指在构造对象之后注入依赖项。这意味着该依赖项不可在类的构造函数体中使用。

如果要在构造函数主体执行之前注入依赖项,从而可以在构造函数主体中使用这些依赖项,则需要在@Configurable声明中定义此变量,如下所示:

@Configurable(preConstruction=true)

  

 为此,必须将带注解的类型与 AspectJ 编织器编织在一起。您可以使用构建时 Ant 或 Maven 任务来执行此操作,也可以使用加载时编织。 AnnotationBeanConfigurerAspect本身需要由 Spring 配置(以获取对将用于配置新对象的 Bean 工厂的引用)。

 如果使用基于 Java 的配置,则可以将@EnableSpringConfigured添加到任何@Configuration类中,如下所示:

 

@Configuration
@EnableSpringConfigured
public class AppConfig {

}

如果您喜欢基于 XML 的配置,Spring context namespace定义了一个方便的context:spring-configured元素,您可以按以下方式使用它:  

<context:spring-configured/>

  

 在配置方面之前创建的@Configurable个对象的实例导致向调试日志发出一条消息,并且未进行任何对象配置。一个示例可能是 Spring 配置中的 bean,当它由 Spring 初始化时会创建域对象。在这种情况下,可以使用depends-on bean 属性来手动指定该 bean 取决于配置方面。下面的示例演示如何使用depends-on属性:

 

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>

Note

除非您真的想在运行时依赖它的语义,否则不要通过 bean configurer 方面激活@Configurable处理。特别是,请确保不要在已通过容器注册为常规 Spring bean 的 bean 类上使用@Configurable。这样做将导致两次初始化,一次是通过容器,一次是通过方面。

  

单元测试@Configurable 对象

@Configurable支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果 AspectJ 尚未编织@Configurable类型,则注解在单元测试期间不起作用。您可以在被测对象中设置模拟或存根属性引用,然后照常进行。如果@Configurable类型是 AspectJ 编织的,您仍然可以像往常一样在容器外部进行单元测试,但是每次构造@Configurable对象时,都会看到一条警告消息,指示该对象尚未由 Spring 配置。

 

 

处理多个应用程序上下文

用于实现@Configurable支持的AnnotationBeanConfigurerAspect是 AspectJ 单例方面。单例方面的范围与static成员的范围相同:每个类加载器都有一个方面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在哪里定义@EnableSpringConfigured bean 以及在哪里将spring-aspects.jar放在 Classpath 上。

考虑一个典型的 Spring Web 应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用的业务服务,支持那些服务所需的一切,以及每个 Servlet 的一个子应用程序上下文(其中包含该 Servlet 的特定定义)。所有这些上下文共存于相同的类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能保留对其中一个的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured bean。这定义了您可能想注入域对象的服务。结果是,您无法使用@Configurable 机制来配置域对象,该域对象引用的是在子(特定于 servlet 的)上下文中定义的 bean 的引用(无论如何,这可能不是您想做的事情)。

在同一容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序使用自己的类加载器(例如,将spring-aspects.jar放在'WEB-INF/lib'中)加载spring-aspects.jar中的类型。如果spring-aspects.jar仅添加到容器级的 Classpath 中(并因此由共享的父类加载器加载),则所有 Web 应用程序都共享相同的方面实例(这可能不是您想要的)。

 

5.10.2. AspectJ 的其他 Spring 方面

除了@Configurable方面之外,spring-aspects.jar还包含一个 AspectJ 方面,您可以使用它来驱动 Spring 的事务 Management,该事务 Management 使用@Transactional注解 进行注解的类型和方法。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。

解释@Transactional注解 的方面是AnnotationTransactionAspect。使用此方面时,必须注解实现类(或该类中的方法或两者),而不是注解该类所实现的接口(如果有)。 AspectJ 遵循 Java 的规则,即不继承接口上的 注解。

类上的@Transactional注解 指定用于执行该类中任何公共操作的默认事务语义。

类内方法上的@Transactional注解 将覆盖类 注解(如果存在)给出的默认事务语义。可以标注任何可见性的方法,包括私有方法。直接注解非公共方法是执行此类方法而获得事务划分的唯一方法。

Tip

从 Spring Framework 4.2 开始,spring-aspects提供了类似的方面,为标准javax.transaction.Transactional注解 提供了完全相同的功能。检查JtaAnnotationTransactionAspect了解更多详细信息。

对于希望使用 Spring 配置和事务 Management 支持但又不想(或不能)使用注解的 AspectJ 程序员,spring-aspects.jar还包含abstract个方面,您可以扩展它们以提供自己的切入点定义。有关更多信息,请参见AbstractBeanConfigurerAspectAbstractTransactionAspect方面的资源。例如,以下摘录显示了如何编写方面来使用与完全限定的类名匹配的原型 Bean 定义来配置域模型中定义的对象的所有实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

    public DomainObjectConfiguration() {
        setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
    }

    // the creation of a new bean (any object in the domain model)
    protected pointcut beanCreation(Object beanInstance) :
        initialization(new(..)) &&
        SystemArchitecture.inDomainModel() &&
        this(beanInstance);

}

  

 

5.10.3. 使用 Spring IoC 配置 AspectJ Aspects

 

当您将 AspectJ 方面与 Spring 应用程序一起使用时,既自然又希望能够使用 Spring 配置这些方面。 AspectJ 运行时本身负责方面的创建,并且通过 Spring 配置 AspectJ 创建的方面的方法取决于方面所使用的 AspectJ 实例化模型(per-xxx子句)。

AspectJ 的大多数方面都是单例方面。这些方面的配置很容易。您可以创建一个正常引用外观类型并包含factory-method="aspectOf" bean 属性的 bean 定义。这可以确保 Spring 通过向 AspectJ 索要长宽比实例,而不是尝试自己创建实例来获得长宽比实例。下面的示例演示如何使用factory-method="aspectOf"属性:

 

<bean id="profiler" class="com.xyz.profiler.Profiler"
        factory-method="aspectOf"> (1)

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
  • (1) 请注意factory-method="aspectOf"属性

非单一方面很难配置。但是,可以通过创建原型 bean 定义并使用spring-aspects.jar@Configurable支持来配置方面实例(一旦 AspectJ 运行时创建了 bean)来实现。

如果您有一些要与 AspectJ 编织的@AspectJ 方面(例如,对域模型类型使用加载时编织)以及要与 Spring AOP 一起使用的其他@AspectJ 方面,那么这些方面都已在 Spring 中配置,您需要告诉 Spring AOP @AspectJ 自动代理支持,应使用配置中定义的@AspectJ 方面的确切子集进行自动代理。您可以使用<aop:aspectj-autoproxy/>声明中的一个或多个<include/>元素来完成此操作。每个<include/>元素都指定一个名称模式,并且只有名称与至少一个模式匹配的 bean 才可用于 Spring AOP 自动代理配置。以下示例显示了如何使用<include/>元素:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

Note

不要被<aop:aspectj-autoproxy/>元素的名称所迷惑。使用它可以创建 Spring AOP 代理。此处使用了@AspectJ 样式的声明,但是不涉及 AspectJ 运行时。

  

  

 

 

 

 

 

 

 

 

 

  

 

posted @ 2020-12-31 13:18  节日快乐  阅读(213)  评论(0编辑  收藏  举报