Spring 框架之 Spring AOP

 2022-09-01
原文地址:https://www.51cto.com/article/710896.html

202209012340121871.png

AOP概念

让我们首先定义一些核心的AOP概念和术语:

  • Aspect:切面。一个跨越多个类的关注点的模块化。在Spring AOP中,切面是通过使用常规类(基于模式的方法)或使用@Aspect注解的常规类(@AspectJ风格)来实现的。
  • Join point:连接点。在程序执行过程中的一个点,例如一个方法的执行或一个异常的处理。在Spring AOP中,一个连接点总是代表一个方法的执行。
  • Advice:通知。一个切面在一个特定的连接点采取的行动。不同类型的Advice包括 "around"、"before "和 "after "的通知。(许多AOP框架,包括Spring,都将通知建模为一个拦截器,并在连接点周围维护一个拦截器链。
  • Pointcut:切点。带有通知的连接点,在程序中主要体现为书写Pointcut表达式。Spring默认使用AspectJ的切点表达式语言。
  • Introduction:代表一个类型声明额外的方法或字段。Spring AOP允许你为任何Advice的对象引入新的接口(以及相应的实现)。例如,你可以使用引入来使一个bean实现IsModified接口,以简化缓存。(Introduction在AspectJ社区中被称为类型间声明)。
  • Target object:目标对象。被一个或多个切面通知的对象。也被称为 "Advice对象"。由于Spring AOP是通过使用运行时代理来实现的,这个对象总是一个被代理的对象。
  • AOP proxy:AOP代理。一个由AOP框架创建的对象,以实现切面契约(通知方法执行等)。在Spring框架中,AOP代理是一个JDK动态代理或CGLIB代理。
  • Weaving:将aspect与其他应用类型或对象联系起来,以创建一个Advice对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。Spring AOP和其他纯Java AOP框架一样,在运行时进行织入。

Spring AOP包括以下类型的Advice:

  • Before advice:在连接点之前运行的通知,但它没有能力阻止执行流进行到连接点(除非它抛出一个异常)。
  • After returning advice:在一个连接点正常完成后运行的Advice(例如,如果一个方法返回而没有抛出异常)。
  • After throwing advice:当一个方法通过抛出异常退出时要运行的Advice。
  • After (finally) advice:无论连接点以何种方式退出(正常或异常返回),都要运行Advice。
  • Around advice:围绕连接点的Advice,如方法调用。这是最强大的一种Advice。Around advice可以在方法调用之前和之后执行自定义行为。它还负责选择是否继续进行连接点,或者通过返回自己的返回值或抛出一个异常来缩短Advice方法的执行。

Around advice是最通用的一种Advice。由于Spring AOP和AspectJ一样,提供了全面的Advice类型,我们建议你使用功能最弱的Advice类型来实现所需的行为。例如,如果你只需要用一个方法的返回值来更新缓存,那么你最好实现一个After returning advice,而不是一个Around advice,尽管Around advice可以完成同样的事情。使用最具体的Advice类型可以提供一个更简单的编程模型,减少错误的可能性。

所有的Advice参数都是静态类型的,因此你可以使用适当类型的Advice参数(例如,方法执行的返回值的类型),而不是Object数组。

由切点匹配的连接点的概念是AOP的关键,它区别于只提供拦截的旧技术。Pointcuts使Advice能够独立于面向对象的层次结构而成为目标。例如,你可以将提供声明性事务管理的建议应用于跨越多个对象的一组方法(如服务层的所有业务操作)。

AOP Proxy

Spring AOP默认使用标准的JDK动态代理作为AOP代理。这使得任何接口(或一组接口)都可以被代理。

Spring AOP也可以使用CGLIB代理。这对于代理类(不是接口)来说是必要的。默认情况下,如果一个业务对象没有实现一个接口,就会使用CGLIB。由于对接口而不是类进行编程是很好的做法,业务类通常实现一个或多个业务接口。在那些(希望是罕见的)需要向未在接口上声明的方法提供Advice的情况下,或者需要将代理对象作为具体类型传递给方法的情况下,可以强制使用CGLIB。

@AspectJ

@AspectJ指的是将aspect作为带有注解的普通Java类来声明的一种风格。

启用@AspectJ

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

@AspectJ支持可以通过XML或Java风格的配置来启用。在这两种情况下,你还需要确保AspectJ的aspectjweaver.jar库在你的应用程序的classpath上(版本1.8或更高)。

用Java配置启用@AspectJ 要通过Java @Configuration启用@AspectJ支持,请添加@EnableAspectJAutoProxy注解,如下例所示:

    @Configuration
    @EnableAspectJAutoProxy
    public class AppConfig {
    
    }

Aspect声明

在启用@AspectJ的情况下,任何在你的应用程序上下文中定义的Bean,其类是@AspectJ切面(具有@Aspect注解),会被Spring自动检测到,并用于配置Spring AOP。接下来的两个例子显示了一个不怎么有用的切面所需的最小定义:

    package org.xyz;
    import org.aspectj.lang.annotation.Aspect;
    @Aspect
    public class NotVeryUsefulAspect {
    
    }
    @Configuration
    @EnableAspectJAutoProxy
    public class AppConfig {
        @Bean
        public NotVeryUsefulAspect myAspect(){
           return new NotVeryUsefulAspect();
        }
    }

Pointcut声明

Pointcuts确定连接点,从而使我们能够控制Advice的运行时间。Spring AOP只支持Spring Bean的方法执行连接点,所以你可以把pointcut看作是与Spring Bean上的方法执行相匹配。一个切点声明有两个部分:一个由名称和任何参数组成的签名,以及一个切点表达式,它决定了我们到底对哪些方法的执行感兴趣。在AOP的@AspectJ注解式中,一个pointcut签名是由一个常规的方法定义提供的,而pointcut表达式是通过使用@Pointcut注解来表示的(作为pointcut签名的方法必须有一个void返回类型)。

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

Pointcut表达式

Spring AOP支持以下AspectJ的切点表达式,以便在切点表达式中使用:

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

Pointcut表达式组合

你可以通过使用&&、||和!来组合pointcut表达式。你也可以通过名字来引用pointcut表达式。下面的例子显示了三个切点表达式:

    @Pointcut("execution(public * *(..))")
    private void anyPublicOperation() {} 
    @Pointcut("within(com.xyz.myapp.trading..*)")
    private void inTrading() {} 
    @Pointcut("anyPublicOperation() && inTrading()")
    private void tradingOperation() {}

共享通用pointcut定义

在处理企业应用程序时,开发人员经常想从几个方面来引用应用程序的模块和特定的操作集。我们建议定义一个CommonPointcuts切面,它可以为这个目的捕获通用的pointcut表达。

    package com.xyz.myapp;
    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() {}
    }

Advice声明

Advice与一个切点表达式相关联,在切点匹配的方法执行之前、之后或around 运行。该切点表达式可以是对一个命名的切点的简单引用,也可以是一个就地声明的切点表达式。

Before Advice

你可以通过使用@Before注解在一个切面声明before advice:

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

如果我们使用pointcut表达式,我们可以把前面的例子改写成下面的例子:

    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() {
            // ...
        }

After Returning Advice

当一个匹配的方法执行正常返回时,返回后的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() {
            // ...
        }
    }

有时,你需要访问Advice中返回的值。你可以使用绑定返回值的@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) {
            // ...
        }
    }

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() {
            // ...
        }
    }

通常情况下,你希望Advice只在给定类型的异常被抛出时运行,而且你也经常需要在Advice中访问被抛出的异常。你可以使用throwing属性来限制匹配(如果需要的话--否则使用Throwable作为异常类型),并将抛出的异常绑定到advice参数上。

    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) {
            // ...
        }
    }

After (Finally) Advice

当一个匹配的方法执行退出时,After (Finally) Advice会运行。它是通过使用@After注解来声明的。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

最后一种advice是Around Advice。Around Advice是一个与匹配的方法的执行而运行。它有机会在方法运行之前和之后进行工作,并决定何时、如何、甚至是否真正运行该方法。如果你需要以线程安全的方式分享方法执行前后的状态,例如启动和停止一个定时器,那么Around Advice经常被使用。

Around Advice是通过用@Around注解来声明一个方法的。该方法应该声明Object为其返回类型,并且该方法的第一个参数必须是ProceedingJoinPoint类型。在Advice中,你必须在ProceedingJoinPoint上调用proceed(),以使底层方法运行。在没有参数的情况下调用proceed()将导致调用者的原始参数在底层方法被调用时被提供给它。对于高级用例,有一个重载的 proceed() 方法,它接受一个参数数组(Object[] )。当底层方法被调用时,数组中的值将被用作该方法的参数。

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

Advice Parameters

Spring提供了完全类型化的Advice,这意味着你可以在Advice签名中声明你需要的参数(就像我们在前面看到的返回和抛出的例子那样),而不是一直使用Object[]数组。我们将在本节后面看到如何使参数和其他上下文值对Advice主体可用。首先,我们看一下如何编写通用Advice,它可以找出advice当前所通知的方法。

访问当前连接点

任何Advice方法都可以声明一个org.aspectj.lang.JoinPoint类型的参数,作为它的第一个参数。请注意,aroud advice需要声明一个ProceedingJoinPoint类型的第一个参数,它是JoinPoint的一个子类。

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

  • getArgs():返回方法的参数。
  • getThis():返回代理对象。
  • getTarget():返回目标对象。
  • getSignature():返回正在被通知方法的描述。
  • toString():打印被通知的方法的有用描述。

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

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

pointcut表达式args(account,..)部分有两个目的。首先,它将匹配限制在方法的执行上,即方法至少需要一个参数,并且传递给该参数的参数是一个账户的实例。其次,它使实际的账户对象通过账户参数对advice可用。

另一种写法是声明一个pointcut,当它与一个连接点匹配时 "提供 "账户对象的值,然后从advice中引用这个命名的pointcut:

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

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
    }

指定参数名称Advice调用中的参数绑定依赖于将pointcut表达式中使用的名称与advice和pointcut方法签名中声明的参数名称相匹配。参数名称无法通过Java反射获得,因此Spring AOP使用以下策略来确定参数名称。

如果用户已经明确指定了参数名,则使用指定的参数名。Advice和pointcut注解都有一个可选的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属性的值中省略参数的名称。例如,如果你修改前面的advice以接收连接点对象,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
    }

带参数的进程 我们在前面说过,我们将描述如何编写一个在Spring AOP和AspectJ中一致运行的带参数的继续调用。解决方案是确保advice签名按顺序绑定每个方法参数。下面的例子展示了如何做到这一点:

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

Advice排序

当多个advice都想在同一个连接点运行时,会发生什么?Spring AOP遵循与AspectJ相同的优先级规则来决定advice的执行顺序。优先级最高的advice在 "进入 "时首先运行。从一个连接点 "出来 "时,优先级最高的advice最后运行。

Introductions

Introductions(在AspectJ中被称为类型间声明)使一个切面能够声明advice对象实现一个给定的接口,并代表这些对象提供该接口的实现。

你可以通过使用@DeclareParents注解来做一个Introductions。这个注解被用来声明匹配的类型有一个新的父类(因此得名)。例如,给定一个名为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.CommonPointcuts.businessService() && this(usageTracked)")
        public void recordUsage(UsageTracked usageTracked) {
            usageTracked.incrementUseCount();
        }
    }

切面实例模型

默认情况下,每个切面在应用上下文中都有一个实例。AspectJ把这称为单例实例模型。我们可以用其他的生命周期来定义方面。Spring支持AspectJ的perthis和pertarget实例化模型;目前不支持percflow、percflowbelow和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() {
            // ...
        }
    }

AOP demo

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

因为我们想重试操作,所以我们需要使用around advice,以便我们可以多次调用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接口,这样我们就可以把切面的优先级设置得比事务advice高(我们希望每次重试都是一个新的事务)。maxRetries和order属性都是由Spring配置的。主要的动作发生在around advice的doConcurrentOperation中。请注意,就目前而言,我们将重试逻辑应用于每个businessService()。我们尝试进行,如果失败了,出现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>

为了细化这个切面,使它只重试幂等操作,我们可以定义下面的幂等注解:

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

然后我们可以使用注解来注解服务操作的实现。对只重试幂等操作切面的修改涉及到细化pointcut表达式,以便只有@Idempotent操作可以匹配,如下所示:

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