Spring 5 中文解析核心篇-IoC容器之AOP编程(下)

5.5 基于Schema的AOP支持

如果你更喜欢基于XML的格式,Spring还提供了使用新的aop名称空间标签定义切面的支持。支持与使用@AspectJ样式时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点放在新语法上,并使读者参考上一节中的讨论(@AspectJ支持),以了解编写切入点表达式和通知参数的绑定。

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

在你的Spring配置中,所有切面和advisor元素都必须放在元素内(在应用程序上下文配置中可以有多个元素)。元素可以包含切入点,advisoraspect元素(请注意,必须按此顺序声明它们)。

的配置样式大量使用了Spring的自动代理机制。如果你已经通过BeanNameAutoProxyCreator或类似的工具使用了显式的自动代理,那么这可能会导致一些问题(比如没有编织通知)。推荐的用法模式是仅使用样式或仅使用AutoProxyCreator样式并且不要混合使用。

5.5.1 声明切面

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

你可以通过使用元素来声明一个切面,并通过使用ref属性来引用后台bean,如下面的示例所示:


    
        ...
    



    ...

支持切面的bean(在本例中为aBean)当然可以像配置其他Spring bean一样进行配置并注入依赖项。

5.5.2 声明切入点

你可以在元素内声明一个命名的切入点,让切入点定义多个切面和advisors之间共享。

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



    


请注意,切入点表达式本身使用的是@AspectJ支持中所述的AspectJ切入点表达式语言。如果使用基于schema的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下:



    


假定你具有“共享通用切入点定义”中所述的SystemArchitecture方面。

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



    

        

        ...

    


@AspectJ切面几乎相同,通过使用基于schema的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点收集此对象作为连接点上下文,并将其传递给通知:



    

        

        

        ...

    


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

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

在组合切入点子表达式时,&&在XML文档中是不合适的,所以你可以分别使用andornot关键字来代替&&||!。例如,上一个切入点可以更好地编写如下:



    

        

        

        ...
    

请注意,以这种方式定义的切入点由其XML ID引用,并且不能用作命名切入点以形成复合切入点。因此,基于schema的定义样式中的命名切入点支持比@AspectJ样式所提供的更受限制。

5.5.3 声明通知

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

前置通知

在执行匹配的方法之前,先运行通知。使用元素在中声明它,如以下示例所示:



    

    ...


在这里,dataAccessOperation是在最高()级别定义的切入点ID。要定义内联切入点,请使用以下方法将pointcut-ref属性替换为pointcut属性:



    

    ...


正如我们在@AspectJ样式的讨论中所指出的那样,使用命名的切入点可以显着提高代码的可读性。method属性标识提供通知正文的方法(doAccessCheck)。必须为包含通知的Aspect元素所引用的bean定义此方法。在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用方面bean上的doAccessCheck方法。

返回后通知

返回的通知在匹配的方法执行正常完成时运行。它以与前置通知相同的方式在中声明。以下示例显示了如何声明它:



    

    ...


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



    

    ...


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

public void doAccessCheck(Object retVal) {...

异常通知

当匹配的方法执行通过抛出异常退出时执行通知时,抛出通知。通过使用after-throwing元素在中声明它,如以下示例所示:



    

    ...


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



    

    ...


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

public void doRecoveryActions(DataAccessException dataAccessEx) {...

最终通知

无论最终如何执行匹配的方法,最终通知都会运行。你可以使用after元素声明它,如以下示例所示:



    

    ...


环绕通知

最后一种通知是环绕通知。环绕通知在匹配的方法执行“环绕”运行。它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真的执行方法。环绕通知通常用于以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态。总是使用最弱的形式的通知来满足你的要求(备注:最小范围使用)。不要使用环绕的通知,如果前置通知可以做的工作。

你可以使用元素声明环绕通知。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知的正文中,在ProceedingJoinPoint上调用proceed()会使底层方法执行。还可以使用Object []调用proceed方法。数组中的值用作方法执行时的参数。有关调用Object []的注意事项,请参见“环绕通知”。以下示例显示了如何在XML中环绕通知进行声明:



    

    ...


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

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

通知参数

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


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(..)将用作通知,如以下示例所示:

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配置影响了特定连接点的上述通知的执行:



    
    

    
    

    
        

            

            

        
    


考虑以下驱动程序脚本:

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

有了这样的Boot类,我们将在标准输出上获得类似于以下内容的输出:

StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)

通知顺序

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

5.5.4 引入

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

你可以通过在中使用元素进行引入。你可以使用元素声明匹配类型具有新的父类(因此而得名)。例如,给定一个名为UsageTracked的接口和该接口名为DefaultUsageTracked的实现,以下切面声明服务接口的所有实现者也都实现UsageTracked接口。(例如,为了通过JMX公开统计信息。)



    

    


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

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

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

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
5.5.5 切面实例化模式

模式定义切面唯一受支持的实例化模型是单例模型。在将来的版本中可能会支持其他实例化模型。

5.5.6 Advisors

`

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

Spring通过元素支持advisors概念。你通常会看到它与事务通知结合使用,事务通知在Spring中也有其自己的名称空间支持。以下示例显示advisors



    

    




    
        
    

除了在前面的示例中使用的pointcut-ref属性之外,你还可以使用pointcut属性内联定义一个pointcut表达式。

要定义advisor的优先级以便通知可以参与排序,可以使用order属性来定义advisor的排序值。

5.5.7 AOP Schema例子

本节将展示AOP示例中的并发锁定失败重试示例在使用模式支持重写时的例子。

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

因为我们想重试该操作,所以我们需要使用周围建议,以便可以多次调用proceed。下面的清单显示了基本的切面实现(它是一个使用schema支持的常规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中。我们试着继续。如果我们因为一个PessimisticLockingFailureException异常失败了,我们会再次尝试,除非我们已经耗尽了所有的重试尝试。

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

相应的Spring配置如下:



    

        

        

    




        
        

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

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

切面更改为仅重试幂等操作涉及更改切入点表达式,以便仅@Idempotent操作匹配,如下所示:


5.6 选择哪一种AOP声明格式使用

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

5.6.1 Spring AOP 或 完整的AspectJ?

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

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

5.6.2 Spring AOP中使用@AspectJXML选择?

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

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

XML样式有两个缺点。首先,它没有完全将要解决的需求的实现封装在一个地方。DRY原则说,系统中的任何知识都应该有一个单一、明确、权威的表示形式。当使用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样式中,你可以声明前两个切入点:




XML方法的缺点是你无法通过组合这些定义来定义accountPropertyAccess切入点(备注:不能组合多个切面)。

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

5.7 混合切面类型

通过使用自动代理支持,模式定义的切面、声明的advisors,甚至是同一配置中其他样式的代理和拦截器,完全可以混合@AspectJ样式的切面。所有这些都是通过使用相同的底层支持机制实现的,并且可以毫无困难地共存。

5.8 代理机制

Spring AOP使用JDK动态代理或CGLIB创建给定目标对象的代理。JDK动态代理内置在JDK中,而CGLIB是常见的开源类定义库(重新包装到spring-core中)。

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

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

  • 使用CGLIB,不能通知final方法,因为不能在运行时生成的子类中覆盖它们。
  • 从Spring 4.0开始,由于CGLIB代理实例是通过Objenesis创建的,因此不再调用代理对象的构造函数两次。只有在你的JVM不允许绕过构造函数的情况下,你才可能从Spring的AOP支持中得到两次调用和相应的调试日志条目。

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


    

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


多个部分在运行时折叠到一个统一的自动代理创建器中,该创建器将应用任何部分(通常来自不同的XML bean定义文件)指定的最强的代理设置。这也适用于元素。

为了清楚起见,在元素上使用proxy-target-class =“true”会强制对所有三个元素使用CGLIB代理其中。

5.8.1 理解AOP代理

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

首先考虑以下情况:你有一个普通的、未经代理的、无特殊要求的直接对像引用,如以下代码片段所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // 调用当前对象的bar方法
        this.bar();
    }

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

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

aop proxy plain pojo call
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();
    }
}

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

aop proxy call
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(..)方法内部的客户端代码具有对代理的引用。这意味着该对象引用上的方法调用是代理上的调用。因此,代理可以委托给与特定方法调用相关的所有拦截器(通知)。然而,一旦调用最终到达目标对象(本例中是SimplePojo,即引用),它可能对自身进行的任何方法调用,比如this.bar()或this.foo(),都将针对this引用而不是代理进行调用。这具有重要意义。这意味着自调用不会导致与方法调用相关的通知得到执行的机会(备注:通过代理调用才能触发相关的通知)。

Okay,那么该怎么办?最佳方法(在这里宽松地使用术语“最佳”)是重构代码,以免发生自调用。这确实需要你做一些工作,但这是最好的,侵入性最小的方法。下一种方法绝对可怕,我们正要指出这一点,恰恰是因为它是如此可怕。你可以(这对我们来说很痛苦)将类中的逻辑完全绑定到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.addInterface(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代理

除了通过使用声明配置中的各个切面外,还可以通过编程方式创建通知目标对象的代理。有关Spring的AOP API的完整详细信息,请参阅下一章。在这里,我们要重点介绍使用@AspectJ切面自动创建代理的功能。

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

// 创建一个工厂,这个工厂可以为给定对象生成代理
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// 增加切面,这个切面必须被标注@AspectJ注解
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// 添加存在的切面,这个切面对象背心支持@AspectJ
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

有关更多信息,请参见javadoc。

5.10 在Spring应用中使用AspectJ

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

Spring附带了一个小的AspectJ切面库,该库在你的发行版中可以作为spring-aspects.jar独立使用。你需要将其添加到类路径中才能使用其中的切面。使用AspectJ依赖于Spring和AspectJ的其他Spring切面来注入域对象以及AspectJ讨论该库的内容以及如何使用它。使用Spring IoC配置AspectJ切面讨论了如何依赖注入使用AspectJ编译器编织的AspectJ切面。最后,Spring Framework中使用AspectJ进行的加载时编织为使用AspectJ的Spring应用顺序提供了加载时编织的介绍。

5.10.1 在Spring中使用AspectJ去依赖注入领域对象

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定义的名称,则可以直接在注解中这样做如以下示例所示:

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)分别按类型或名称进行自动装配。或者,最好在字段或方法级别通过@Autowired@Inject@Configurable bean指定显式的,注解驱动的依赖项注入(有关更多详细信息,请参见基于注释的容器配置)。最后,你可以使用dependencyCheck属性(例如,@Configurable(autowire = Autowire.BY_NAME,dependencyCheck = true))为新创建和配置的对象中的对象引用启用Spring依赖检查。如果该属性设置为true,则Spring在配置后验证所有属性(不是原生类型或集合)是否已经设置。

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

上述段落中的一个关键短语是in essence(本质)。在大多数情况下,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”是指在构造对象之后注入依赖项。这意味着该依赖项不可在类的构造函数体中使用。如果你希望在构造函数主体执行之前注入依赖项,从而可以在构造函数主体中使用这些依赖项,则需要在@Configurable声明中对此进行定义,如下所示:

@Configurable(preConstruction = true)

你可以在《 AspectJ编程指南》的此附录中找到有关各种切入点类型的语言语义的更多信息。

为此,必须将带注解的类型与AspectJ编织器编织在一起。你可以使用构建时的Ant或Maven任务来执行此操作(例如,参见《 AspectJ开发环境指南》),也可以使用加载时编织(请参见Spring Framework中的使用AspectJ进行加载时编织)。Spring需要配置AnnotationBeanConfigurerAspect自身(以便获得对将用于配置新对象Bean工厂的引用)。如果使用基于Java的配置,则可以将@EnableSpringConfigured添加到任何@Configuration类中,如下所示:

@Configuration
@EnableSpringConfigured
public class AppConfig {
}

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


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



    


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

单元测试@Configurable对象

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

使用多个应用程序上下文

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

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

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

5.10.2 AspectJ的其他Spring切面

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

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

类上的@Transactional注解指定用于执行该类中任何公共操作的默认事务语义。可以注解任何可见性的方法,包括私有方法。直接注解非公共方法是执行此类方法而获得事务划分的唯一方法。

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

对于希望使用Spring配置和事务管理支持但又不想(或不能)使用注解的AspectJ编程者,spring-aspects.jar也包含抽象切面,你可以扩展它们以提供自己的切入点定义。有关更多信息,请参见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切面

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

AspectJ的大多数切面都是单例切面。这些切面的配置很容易。你可以创建一个bean定义,该bean定义按常规引用切面类型,并包括factory-method =“aspectOf” bean属性。这样可以确保Spring通过向AspectJ获取实例,而不是尝试自己创建实例。以下示例显示如何使用factory-method =“aspectOf”属性:

 //1

    

  1. 注意factory-method =“aspectOf”属性

非单例切面更难配置。但是,可以通过创建原型Bean定义并使用spring-aspects.jar中的@Configurable支持来实现,一旦它们由AspectJ运行时创建了Bean,就可以配置切面实例。

如果你有一些要与AspectJ编织的@AspectJ切面(例如,对域模型类型使用加载时编织)以及要与Spring AOP一起使用的其他@AspectJ切面,那么这些切面都已在Spring中配置,你需要告诉Spring AOP @AspectJ自动代理支持,应使用配置中定义的@AspectJ切面的确切子集进行自动代理。你可以通过在声明中使用一个或多个元素来做到这一点。每个元素都指定一个名称模式,只有名称与至少一个模式匹配的bean才可用于Spring AOP自动代理配置。以下示例显示了如何使用元素:


    
    

不要被元素的名称所迷惑。使用它可以创建Spring AOP代理。这里使用的是@AspectJ样式的切面声明,但不涉及AspectJ运行时。

5.10.4 在Spring Framework中使用AspectJ进行加载时编织

加载时编织(LTW)是指在将AspectJ切面加载到应用程序的类文件中时将其编织到Java虚拟机(JVM)中的过程。本节的重点是在Spring框架的特定上下文中配置和使用LTW。本节不是LTW的一般介绍。有关LTW的详细信息以及仅使用AspectJ配置LTW(完全不涉及Spring)的详细信息请参阅《 AspectJ开发环境指南》的LTW部分。Spring框架为AspectJ LTW带来的价值在于能够对编织过程进行更精细的控制。“ Vanilla” AspectJ LTW通过使用Java(5+)代理来实现,该代理在启动JVM时通过指定VM参数来切换。因此,它是一个JVM范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用spring的LTW允许你在每个类加载器的基础上打开LTW,这是更细粒度的,在“单jvm -多应用程序”环境中更有意义(比如在典型的应用程序服务器环境中)。

此外,在某些环境中,此支持激活装载时编织不需要应用程序服务器的启动脚本进行任何修改,也不需要添加-javaagent:path/to/aspectjweaver.jar(如本节稍后所述)或-javaagent:path/to/spring-instrument.jar。开发人员将应用程序上下文配置为启用加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。

现在介绍结束了,让我们首先浏览一个使用Spring的AspectJ LTW的快速示例,然后详细介绍示例中引入的元素。有关完整的示例,请参见Petclinic示例应用程序。

第一个例子

假设你是一位负责诊断系统中某些性能问题的原因的应用程序开发人员。我们将打开一个简单的配置切面,而不是使用配置文件工具,使我们能够快速获取一些性能指标。然后,我们可以立即在该特定区域应用更细粒度的分析工具。

此处提供的示例使用XML配置。你还可以配置@AspectJ并将其与Java配置一起使用。具体来说,你可以使用@EnableLoadTimeWeaving注解替代(有关详细信息,请参见下文)。

下面的示例显示了配置切面的信息,这并不理想。这是一个基于时间的探查器,它使用@AspectJ样式的切面声明:

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}

我们还需要创建一个META-INF/aop.xml文件,以通知AspectJ编织者我们希望将ProfilingAspect编织到类中。此文件约定,即在Java类路径上称为META-INF/aop.xml的文件,是标准的AspectJ。以下示例显示aop.xml文件:




    
        
        
    

    
        
        
    



现在,我们可以继续进行配置中特定于Spring的部分。我们需要配置一个LoadTimeWeaver(稍后说明)。该加载时织布器是必不可少的组件,负责将一个或多个META-INF/aop.xml文件中的切面配置编织到应用程序的类中。好处是,它不需要很多配置(你可以指定一些其他选项,但是稍后会详细介绍),如以下示例所示:




    
    

    
    

现在,所有必需的组件(切面,META-INF/aop.xml文件和Spring配置)均已就绪,我们可以使用main(..)方法创建以下驱动程序类,以演示LTW的实际作用:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
                (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

        // the profiling aspect is 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

我们还有最后一件事要做。本节的引言确实说过,可以使用Spring在每个ClassLoader的基础上选择性地打开LTW,这是事实。但是,对于此示例,我们使用Java代理(Spring提供)打开LTW。我们使用以下命令来运行前面显示的Main类:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent是一个标志,用于指定和启用代理以对在JVM上运行的程序进行检测。Spring框架附带了这样的代理工具InstrumentationSavingAgent,该代理文件打包在spring-instrument.jar中,在上一示例中作为-javaagent参数的值提供。

执行Main程序的输出类似于下一个示例。 (我在calculateEntitlement()实现中引入了Thread.sleep(..)语句,以便探查器实际上捕获的不是0毫秒的内容(01234毫秒不是AOP引入的开销)。以下清单显示了运行分析器时得到的输出:

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

由于此LTW是通过使用成熟的AspectJ来实现的,因此我们不仅限于为Spring Bean提供通知。在Main程序上进行以下细微改动会产生相同的结果:

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {
        new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
                new StubEntitlementCalculationService();

        // the profiling aspect will be 'woven' around this method execution
        entitlementCalculationService.calculateEntitlement();
    }
}

请注意,在前面的程序中,我们如何引导Spring容器,然后完全在Spring上下之外创建StubEntitlementCalculationService的新实例。剖析通知仍会被应用。

诚然,这个例子很简单。但是,在前面的示例中已经介绍了Spring对LTW支持的基础,本节的其余部分详细解释了每个配置和用法背后的“原因”。

在此示例中使用的ProfilingAspect可能是基本的,但它非常有用。它是开发时间方面的一个很好的例子,开发人员可以在开发期间使用它,然后很容易地从部署到UAT或生产中的应用程序的构建中排除它。

切面

你在LTW中使用的切面必须是AspectJ切面。你可以使用AspectJ语言本身来编写它们,也可以使用@AspectJ风格来编写切面。这样,你的切面是有效的AspectJ和Spring AOP方面。此外,编译的切面类需要在类路径上可用。

'META-INF/aop.xml'

通过使用Java类路径上的一个或多个META-INF/aop.xml文件(直接或通常在jar文件中)来配置AspectJ LTW基础结构。

该文件的结构和内容在AspectJ参考文档的LTW部分中进行了详细说明。由于aop.xml文件是100%的AspectJ,因此在此不再赘述。

所需的库(JARS)

至少,你需要使用以下库来使用Spring Framework对AspectJ LTW的支持:

  • spring-aop.jar
  • aspectjweaver.jar

如果使用Spring提供的代理来启用检测,则还需要:

  • spring-instrument.jar

Spring配置

Spring的LTW支持的关键组件是LoadTimeWeaver接口(在org.springframework.instrument.classloading包中),以及Spring发行版附带的众多实现。LoadTimeWeaver负责在运行时将一个或多个java.lang.instrument.ClassFileTransformers添加到ClassLoader,这为各种有趣的应用程序打开了大门,其中之一就是方面的LTW。

如果你不熟悉运行时类文件转换的概念,请在继续操作之前参阅java.lang.instrument的javadoc API文档。尽管该文档并不全面,但至少你可以看到关键的接口和类(在你通读本节文档作为参考)。

为特定的ApplicationContext配置LoadTimeWeaver就像添加一行一样容易。(请注意,你几乎肯定需要将ApplicationContext用作Spring容器-通常,仅BeanFactory是不够的,因为LTW支持使用BeanFactoryPostProcessors。)

要启用Spring Framework的LTW支持,你需要配置一个LoadTimeWeaver,通常通过使用@EnableLoadTimeWeaving注解来完成,如下所示:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

另外,如果你喜欢基于XML的配置,请使用元素。注意,该元素是在上下文名称空间中定义的。以下示例显示了如何使用




    


前面的配置会自动为你定义并注册许多LTW特定的基础结构Bean,例如LoadTimeWeaverAspectJWeavingEnabler。默认的LoadTimeWeaverDefaultContextLoadTimeWeaver类,它尝试装饰自动检测到的LoadTimeWeaver。“自动检测到”的LoadTimeWeaver的确切类型取决于你的运行时环境。下表总结了各种LoadTimeWeaver实现:

运行时环境 LoadTimeWeaver实现
运行在 Apache Tomcat TomcatLoadTimeWeaver
运行在 GlassFish (限于EAR部署) GlassFishLoadTimeWeaver
运行在 Red Hat的 JBoss AS 或 WildFly JBossLoadTimeWeaver
运行在 IBM的 WebSphere WebSphereLoadTimeWeaver
运行在 Oracle的 WebLogic WebLogicLoadTimeWeaver
JVM从Spring开始InstrumentationSavingAgent(java -javaagent:path/to/spring-instrument.jar) InstrumentationLoadTimeWeaver
期望基础ClassLoader遵循通用约定 (即addTransformer和可选的getThrowawayClassLoader方法) ReflectiveLoadTimeWeaver

请注意,该表仅列出使用DefaultContextLoadTimeWeaver时自动检测到的LoadTimeWeavers。你可以确切指定要使用的LoadTimeWeaver实现。

要使用Java配置指定特定的LoadTimeWeaver,请实现LoadTimeWeavingConfigurer接口并覆该getLoadTimeWeaver()方法。

以下示例指定了ReflectiveLoadTimeWeaver

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}

如果使用基于XML的配置,则可以在元素上将全限定的类名指定为weaver-class属性的值。同样,以下示例指定了ReflectiveLoadTimeWeaver




    


稍后可以使用众所周知的名称loadTimeWeaver从Spring容器中检索由配置定义和注册的LoadTimeWeaver。请记住,LoadTimeWeaver仅作为Spring LTW基础结构添加一个或多个ClassFileTransformers的一种机制而存在。执行LTW的实际ClassFileTransformerClassPreProcessorAgentAdapter(来自org.aspectj.weaver.loadtime包)类。有关更多详细信息,请参见ClassPreProcessorAgentAdapter类的类级javadoc,因为实际上如何实现编织的细节不在本文档的讨论范围之内。

剩下要讨论的配置的最后一个属性:aspectjWeaving属性(如果使用XML,则为Aspectj-weaving)。此属性控制是否启用LTW。它接受三个可能的值之一,如果属性不存在,则默认值为autodetect。如果属性不存在。下表总结了三个可能的值:

注解值 XML 值 Explanation
ENABLED on AspectJ正在编织,并且在加载时适当地编织了切面。
DISABLED off LTW已关闭。加载时不会编织任何切面。
AUTODETECT autodetect 如果Spring LTW基础结构可以找到至少一个META-INF / aop.xml文件,则AspectJ编织已启动。否则,它关闭。这是默认值。

特定环境配置

最后一部分包含在应用程序服务器和Web容器等环境中使用Spring的LTW支持时所需的任何其他设置和配置。

Tomcat、JBoss、WebSphere、WebLogic

TomcatJBoss/WildFlyIBM WebSphere Application ServerOracle WebLogic Server都提供了通用应用程序ClassLoader,该应用程序能够进行本地检测。Spring的本地LTW可以利用这些ClassLoader实现来提供AspectJ编织。如前所述,你可以简单地启用加载时编织。具体来说,你无需修改JVM启动脚本即可添加-javaagent:path/to/spring-instrument.jar。请注意,在JBoss上,你可能需要禁用应用服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是将一个名为WEB-INF/jboss-scanning.xml的文件添加到你的构件中,其中包含以下内容:


通用Java应用程序

如果特定LoadTimeWeaver实现不支持的环境中需要类检测,则JVM代理是通用解决方案。对于这种情况,Spring提供了InstrumentationLoadTimeWeaver,它需要特定于Spring(但非常通用)的JVM代理spring-instrument.jar,并由常见@EnableLoadTimeWeaving设置自动检测到。

要使用它,必须通过提供以下JVM选项来使用Spring代理启动虚拟机:

-javaagent:/path/to/spring-instrument.jar

请注意,这需要修改JVM启动脚本,这可能会阻止你在应用程序服务器环境中使用它(取决于你的服务器和你的操作策略)。也就是说,对于每个JVM一个应用程序的部署(例如独立的Spring Boot应用程序),无论如何,你通常都可以控制整个JVM的设置。

5.11 更多资源

可以在AspectJ网站上找到有关AspectJ的更多信息。

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年IT男 获取最新技术文章推送!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公众号:

image

技术交流群:

image

你可能感兴趣的:(Spring 5 中文解析核心篇-IoC容器之AOP编程(下))