美文网首页
spring-core-5 AOP

spring-core-5 AOP

作者: xzz4632 | 来源:发表于2019-06-27 16:25 被阅读0次
    5.1 介绍

    AOP(Aspect-Oriented Progerammint) 是对OOP编程的一种补充. OOP编程的关键单元是类(Class), 而AOP编程的关键单元是切面(aspect),方面支持关注点模块化,如横跨多个类和对象的事务管理, 这些关注点在AOP中常被称为横切关注点.
    AOP框架是Spring的一个关键组件,虽然Spring IoC容器不依赖AOP,这意味着如果您不想使用AOP,就不需要使用AOP,但是AOP补充了Spring IoC,提供了一个非常有用的中间件解决方案。

    在spring 2.0中引入了基于xml或@AspectJ注解的方式使用AOP

    AOP在spring框架中的使用:

    • 提供声明性企业服务,特别是作为EJB声明性服务的替代。最重要的服务是声明性事务管理
    • 允许用户实现自定义方面,对OOP进行补充。
    5.1.1 AOP概念
    • Aspect: 切面, 即跨多个类的关注点模块。Java企业应用程序中的事务管理就是一个很好的例子。在Spring AOP中,Aspect是使用常规类(基于xml的方法)或使用@Aspect注释的常规类(@AspectJ样式)实现的。
    • Join point: 连接点, 即程序执行过程中的一个点,如方法的执行或异常的处理, 在Spring AOP中,连接点总是用一个方法表示。
    • Advice: 通知, 切面在特定的连接点上执行的操作.分为around, before, after, 许多AOP框架,包括Spring,都将通知建模为拦截器,维护一个围绕连接点的拦截器链。
    • Pointcut: 切入点, 即匹配连接点的定义.通知与切入点表达式相关联,并在与切入点匹配的任何连接点上运行, 连接点与切入点表达式匹配的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。
    • Introduction: 引入, 为一个类添加新的方法或字段, Spring AOP允许您向任何被通知的对象引入新的接口(以及相应的实现), 例如,可以通过引入使bean实现IsModified接口,以简化缓存.
    • Target object: 目标对象, 被一个或多个切面通知的对象, 也称为被通知对象。由于Spring AOP是使用运行时代理实现的,所以这个对象将始终是代理对象。
    • AOP proxy: AOP代理, AOP框架为了实现方面契约(建议方法执行等等)而创建的一个对象,在Spring框架中,AOP代理为JDK动态代理或CGLIB代理。
    • Weaving: 织入, 即将切面与其他应用程序类型或对象链接,以创建通知的对象. 这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。与其他纯Java AOP框架一样,Spring AOP在运行时完成。

    通知的类型:

    • Before advice: 在连接点之前执行的通知,但不能阻止执行流继续到连接点(除非抛出异常)。
    • After (finally) advice: 无论连接点以何种方式退出,都会执行的通知.
    • After throwing advice: 如果方法抛出异常,则要执行的通知。
    • After returning advice: 在连接点正常完成后执行的通知
    • Around advice: 围绕连接点的通知。Around通知可以在连接点调用前后执行自定义行为。它还负责选择是继续到连接点,还是通过返回它自己的返回值或抛出异常来简化通知的方法执行。
      环绕通知是功能最强的通知, 在AOP以及其他AOP框架中都提供了完整的通知类型, 但建议在使用通知时尽可能使用功能最弱的通知, 例如,如果只需要用方法的返回值更新缓存,那么最好实现after return通知,而不是around通知,尽管around通知可以完成相同的任务。使用最合适的通知类型可以提供更简单的编程模型,出错的可能性更小。
    5.1.2 Spring AOP的功能和目标

    Spring AOP是用纯Java实现的。Spring AOP目前只支持方法作为连接点(建议在Spring bean上执行方法). 没有实现字段拦截,如果需要建议字段访问和更新连接点,请考虑AspectJ之类的语言。

    5.1.3 AOP代理

    AOP默认使用标准JDK动态代理。这允许代理任何接口(或一组接口)。
    Spring AOP还可以使用CGLIB代理。这对于代理类是必要的, 如果业务对象没有实现接口,则默认使用CGLIB。

    5.2 @AspectJ支持

    @AspectJ是一个将常规JAVA类声明为切面的注解. @AspectJ样式是AspectJ项目作为AspectJ 5发行版的一部分引入的。Spring使用了与AspectJ 5相同的注解样式, 但是Spring AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。

    5.2.1 开启@AspectJ支持

    可以通过XML或Java样式配置启用@AspectJ支持。在这两种情况下,还需要确保AspectJ的aspectjweaver.jar库位于应用程序的类路径上(版本1.8或更高).
    java方式开启:

    @Configuration
    @EnableAspectJAutoProxy
    public class AppConfig {
    
    }
    

    xml方式开启:

    <aop:aspectj-autoproxy/>
    
    5.2.2 声明切面

    启用@AspectJ支持后,在应用程序上下文中使用@AspectJ方面(具有@Aspect注释)类定义的任何bean都将被Spring自动检测到,并用于配置Spring AOP。
    示例:
    应用程序上下文中的常规bean定义,指向具有@Aspect注释的bean类

    <bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
        <!-- configure properties of aspect here as normal -->
    </bean>
    // 
    package org.xyz;
    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    public class NotVeryUsefulAspect {
    
    }
    

    切面也可以通过组件扫描自动发现, 由于组件扫描不能自动发现@Aspect注解, 因此要加上@Component注解.
    在Spring AOP中,不可能让方面本身成为来自其他方面的通知的目标。类上的@Aspect注释将其标记为一个方面,spring会将其排除在自动代理之外。

    5.2.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
    

    切入点标识符
    Spring AOP支持在切入点表达式中使用的以下AspectJ切入点标识符(PCD):

    • execution: 匹配方法执行连接点.
    • 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中执行的方法)具有给定注释的连接点

    Spring AOP还支持一个名为bean的PCD, 这允许您将连接点的匹配限制为特定名称的一个或一组bean(使用通配符时)。其定义格式为:

    bean(idOrNameOfBean)
    

    idOrNameOfBean可以是任何spring定义的bean. 通配符*支持有限的通配,还可以使用&&, ||, !.

    请注意,bean PCD只在Spring AOP中受支持,而在AspectJ编织中不受支持.
    bean PCD在实例级(基于Spring bean名称概念)而不是仅在类型级进行操作。

    组合切点表达式
    切入点表达式可以使用“&&”、“||”和“!”组合。还可以通过名称引用切入点表达式。下面的例子显示了三个切入点表达式:anyPublicOperation(如果方法执行连接点表示任何公共方法的执行,则匹配该表达式);inTrading(如果方法执行在交易模块中,它就匹配)和tradingOperation(如果方法执行代表交易模块中的任何公共方法,它就匹配)。

    @Pointcut("execution(public * *(..))")
    private void anyPublicOperation() {}
    
    @Pointcut("within(com.xyz.someapp.trading..*)")
    private void inTrading() {}
    // 通过名称引入
    @Pointcut("anyPublicOperation() && inTrading()")
    private void tradingOperation() {}
    

    最好的实践是用上面所示的较小的命名组件构建更复杂的切入点表达式。当按名称引用切入点时,应用普通的Java可见性规则(您可以看到相同类型的私有切入点、层次结构中的受保护切入点、任何地方的公共切入点,等等)。可见性不影响切入点匹配。

    定义共享的公共切入点
    在处理企业应用程序时,您通常希望从几个方面引用应用程序的模块和特定的操作集。我们建议定义一个“SystemArchitecture”方面,它捕获用于此目的的公共切入点表达式.

    package com.xyz.someapp;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    
    @Aspect
    public class SystemArchitecture {
    
        /**
         * web层的连接点, 在com.xyz.someapp.web包或其子包中定义连接点
         */
        @Pointcut("within(com.xyz.someapp.web..*)")
        public void inWebLayer() {}
    
        /**
         * service层定义的连接点,连接点定义在com.xyz.someapp.service包及其子包中
         */
        @Pointcut("within(com.xyz.someapp.service..*)")
        public void inServiceLayer() {}
    
        /**
         * dao层接连点, 在com.xyz.someapp.dao包及其子包中定义
         */
        @Pointcut("within(com.xyz.someapp.dao..*)")
        public void inDataAccessLayer() {}
    
        /**
         *
         * 如果按功能区域对service 接口进行分组 (如在com.xyz.someapp.abc.service and com.xyz.someapp.def.service) 
         * 那么这个切点表达式为"execution(* com.xyz.someapp..service.*.*(..))"
         * 你也可以用bean PCD来定义表达式,即"bean(*Service)".此处的前提是你的service bean的命名方式一致.
         */
        @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
        public void businessService() {}
    
        /**
         * 假设这个dao接口定义在 "dao" 包中, 它的实现类定义在其子包中
         */
        @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
        public void dataAccessOperation() {}
    
    }
    

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

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

    示例
    Spring AOP用户可能最经常使用execution 切入点标识符。其格式如下:

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

    除了ret-type-pattern, name-pattern, param-pattern之外, 其他都是可选的.
    ret-type确定了匹配的连接点方法的返回类型.最常见的情况是使用作为返回类型模式,它匹配任何返回类型。只有当方法返回给定类型时,才会匹配完全限定类型名称。
    name表示匹配的方法名称, 可以使用
    通配符作为名称的全部或部分。
    param表示参数, ()表示无参, (..)匹配任何参数(0个或多个). (*)匹配一个参数(类型不限), (*, String)匹配两个参数, 第一个可以是任何类型, 第二个必须 是String.

    常见切入点表达式的例子:
    执行任何公共方法:

    execution(public * *(..))
    

    执行任何以set开头的方法:

    execution(* set*(..))
    

    执行在AccountService接口中定义的任何方法:

    execution(* com.xyz.service.AccountService.*(..))
    

    执行定义在service包中的任何方法:

    execution(* com.xyz.service.*.*(..))
    

    执行定义在service包及其子包中的任何方法:

    execution(* com.xyz.service..*.*(..))
    

    service包中的任何连接点:

    within(com.xyz.service.*)
    

    service包及其子包中的任何连接点:

    within(com.xyz.service..*)
    

    代理实现AccountService接口的任何连接点(仅在Spring AOP中执行方法):

    this(com.xyz.service.AccountService)
    

    AccountService接口实现中定义的任何连接点(仅在Spring AOP中执行方法):

    target(com.xyz.service.AccountService)
    

    只含有一个可序列化的参数的连接点:

    args(java.io.Serializable)
    

    这与execution(* *(java.io.Serializable))不同, args版本是如果在运行时传递的参数是可序列化的,则匹配. 后者是如果方法签名声明一个Serializable类型的参数则匹配.

    目标对象上声明了@Transactional注解的连接点:

    @target(org.springframework.transaction.annotation.Transactional)
    

    目标对象的声明类型具有@Transactional注释的任何连接点:

    @within(org.springframework.transaction.annotation.Transactional)
    

    任何连接点(只在Spring AOP中执行方法),其中执行方法具有@Transactional注释:

    @annotation(org.springframework.transaction.annotation.Transactional)
    

    任何接受单个参数的连接点(仅在Spring AOP中执行方法),其中传递的参数的运行时类型有@ classification注释:

    @args(com.xyz.security.Classified)
    

    在名为tradeService的Spring bean中定义的任何连接点(仅在Spring AOP中执行方法):

    bean(tradeService)
    

    在bean名称是以Service结尾的bean中定义的连接点:

    bean(*Service)
    

    编写好的切入点
    在编译期间,AspectJ处理切入点时会尝试优化匹配性能。检查代码并确定每个连接点是否匹配(静态或动态)给定的切入点是一个代价高昂的过程。在第一次遇到切入点声明时,AspectJ将把它重写为匹配过程的最佳形式。基本上切入点是用DNF(Disjunctive Normal Form)重写的,切入点的组件被排序,以便首先检查那些计算成本更低的组件。这意味着您不必担心理解各种切入点设计器的性能,并且可以在切入点声明中以任意顺序提供它们。
    然而,AspectJ只能处理它被告知的内容,为了获得最佳匹配性能,应该考虑它们试图实现什么,并在定义中尽可能缩小匹配的搜索空间。现有的标识符可以分为三类:kinded、scoping和context:

    • kinded: 表示选择一种特定类型的连接点.如execution, get, set, call, handler.
    • scoping: 选择一组感兴趣的连接点(可能有多种).如within, withincode.
    • context; 根据context来匹配的.如this, target, @annotation.
      一个编写良好的切入点应该尝试至少包含前两种类型(kinded和scoping),而如果希望基于连接点上下文进行匹配,或者绑定上下文以便在通知中使用,则可以包含上下文指示符。提供一个kinded类型的指示符或context类的标识符都可以,但是由于所有额外的处理和分析,可能会影响编织性能(使用的时间和内存)。scoping类的标识符匹配起来非常快,而且它们的使用意味着AspectJ可以非常快地取消不应该进一步处理的连接点组——这就是为什么一个好的切入点应该总是包含一个连接点(如果可能的话)。
    5.2.4 声明通知

    通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或前后运行. 切入点表达式可以是对指定切入点的简单引用,也可以是在适当位置声明的切入点表达式。

    Before通知
    Before通知在切面中使用@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() {
            // ...
        }
    
    }
    

    有时候,您需要在advice主体中访问返回的实际值。您可以在@Afterreturn中绑定这个返回值:

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

    returning属性中使用的名称必须与advice方法中的参数名称对应。returning子句还将匹配限制为只匹配那些返回指定类型值的方法执行.

    后置异常通知
    使用@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作为异常类型),并将抛出的异常绑定到通知参数。

    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建议。
    使用@Around声明.advice方法的第一个参数必须是ProceedingJoinpoint类型,
    在通知的方法体中,调用对ProceedingJoinpoint的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.SystemArchitecture.businessService()")
        public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
            // start stopwatch
            Object retVal = pjp.proceed();
            // stop stopwatch
            return retVal;
        }
    
    }
    

    around通知的返回值将是方法调用者得到的返回值, 请注意,proceed可以被调用一次、多次,或者根本不在around建议的主体中调用.

    通知参数
    任何advice方法都可以声明一个org.aspectj.lang.JoinPoint类型的参数为它的第一个参数,请注意,around通知的第一个参数要求是proceedingJoinpoint类型,它是JoinPoint的子类。JoinPoint接口提供了许多有用的方法,比如getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()(返回目标对象)、getSignature()(返回被建议的方法的签名)和toString()(打印被建议的方法的有用描述)。

    给通知传递参数
    要使参数值对通知主体可用,可以使用args的绑定形式.

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

    通知参数和泛型
    假设您有这样一个泛型类型:

    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<?>并手动检查元素的类型。

    确定参数名称
    Spring AOP的参数名称是不能通过反射来获取的,而是通过切点表达式中声明的参数与切点方法的参数名称来匹配的, 因此spring定义了以下规则:

    • 在切点表达式中通过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属性).

    @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
    }
    
    • 不指定参数名称, 可以用debug模式进行编译, spring aop将根据debug模式下的本地变量来确定参数.
    • 如果在编译时没有包含必需的调试信息, spring aop也会推断参数的绑定信息(如只有一个参数时,那么参数的绑定是很明确的), 如果无法确定参数, 则会抛出AmbiguousBindingException.
    • 如果以上策略都失败了, 则会抛出IllegalArgumentException.
    • 通过execution()表达式.

    通知的执行顺序
    当在一个连接点方法上执行多个通知时会发生什么呢? spring遵循与AspectJ相同的通知执行优先级规则, 对于进入通知(如before通知), 最高优先级的通知最先执行,对于退出通知(如after通知), 优先级最高的最后执行.
    当两个在不同切面定义的通知运行在同一个连接点上, 如果没有指定, 则其运行顺序是不确定的. 可以通过实现Ordered接口或@Order注解来定义顺序,较小的值具有高的优先级.
    在同一个切面中定义的两个通知运行在同一个连接点上,其执行顺序也是未定义的,因为无法通过反射来确定注解的编译顺序,此时考虑分开定义并指定其执行顺序.

    5.4.5 引入

    引入允许一个切面声明某个通知对象实现一个指定的接口并代表这个通知对象实现这个接口.

    @DeclareParents
    这个注解声明了被注解的类具有一个新的父类,例如有一个UsageTracked接口, 其有一个实现DefaultUsageTracked, 则下面的切面声明了所有实现了service的接口也都实现UsageTracked接口.

    @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接口, defaultImpl属性指定了实现类.

    5.4.6 切面实例化

    默认情况下, 切面实例在容器中是单例的, AspectJ调用这些单例模块. 但是也可以定义可选生命周期的切面.通过prethispertarget指定.

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

    在上面的例子中, perthis的作用是为每个执行businessService的唯一service对象创建一个切面实例.(这个唯一对象将会通过连接点上的切点表达式绑定this).切面实例将在第一次调用这个方法时被创建.

    5.4.7 示例

    在并发条件下, 有些业务操作可能会执行失败, 如果再次重新执行则可能成功, 此时我们可以通过around通知来操作.

    @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接口, 因此可以切面的优先级高于事务通知的优先级(每次重试都需要一个新的事务).
    xml配置如下:

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

    可以使用这个注解杰注释service操作的实现.

    @Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
            "@annotation(com.xyz.myapp.service.Idempotent)")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        ...
    }
    
    5.5 基于XML的AOP支持

    在xml配置中, 要提供aop命名空间.要引入spring-aop schema.

    在xml配置中, 所有的aspectadvisor元素都必须包含在<aop:config/>元素中.

    <aop:config>配置大量的使用了spring的自动代理机制, 如果你通过BeanNameAutoProxy或类似的方式显式的指定了自动代理, 这可能会引发一些问题.因此永远不要混合使用它们.

    5.5.1声明切面

    切面是一个被声明为bean的常规的java对象.
    通过<aop:aspect>元素声明一个切面并通过其ref属性引用一个bean.

    <aop:config>
        <aop:aspect id="myAspect" ref="aBean">
            ...
        </aop:aspect>
    </aop:config>
    
    <bean id="aBean" class="...">
        ...
    </bean>
    
    5.5.2 声明切入点<aop:pointcut>

    可以在<aop:config>元素中声明一个切入点, 使其可以在多个切面中共享.

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

    或者:

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

    在切面中声明切入点:

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

    @AspectJ切面类似, 使用xml配置也可以包含切入点上下文,

    <aop:config>
    
        <aop:aspect id="myAspect" ref="aBean">
    
            <aop:pointcut id="businessService"
                expression="execution(* com.xyz.myapp.service.*.*(..)) &amp;&amp; this(service)"/>
    
            <aop:before pointcut-ref="businessService" method="monitor"/>
    
            ...
    
        </aop:aspect>
    
    </aop:config>
    
    public void monitor(Object service) {
        ...
    }
    

    在xml配置中, 在切点子表达式中定义&&, ||, !可以使用and, or, not替换.

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

    注意,以这种方式定义的切入点由它们的XML id引用,不能作为命名切入点来使用,以形成复合切入点。

    5.5.3 声明通知

    Before

    <aop:aspect id="beforeExample" ref="aBean">
    
        <aop:before
            pointcut-ref="dataAccessOperation"
            method="doAccessCheck"/>
    
        ...
    
    </aop:aspect>
    

    此处, dataAccessOperation是在<aop:cofig>元素中定义的一个切入点的id. 也可以通过pointcut`属性创建内联切入点:

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

    使用切入点名称可以提高代码的可读性.
    method属性指定了通知中的一个方法.

    After Returning

    <aop:aspect id="afterReturningExample" ref="aBean">
    
        <aop:after-returning
            pointcut-ref="dataAccessOperation"
            returning="retVal" //定义返回值参数名称
            method="doAccessCheck"/>
    
        ...
    
    </aop:aspect>
    

    After Throwing

    <aop:aspect id="afterThrowingExample" ref="aBean">
    
        <aop:after-throwing
            pointcut-ref="dataAccessOperation"
            throwing="dataAccessEx"  //定义具体的异常类型
            method="doRecoveryActions"/>
    
        ...
    
    </aop:aspect>
    

    After Finally

    <aop:aspect id="afterFinallyExample" ref="aBean">
    
        <aop:after
            pointcut-ref="dataAccessOperation"
            method="doReleaseLock"/>
    
        ...
    
    </aop:aspect>
    

    Around

    <aop:aspect id="aroundExample" ref="aBean">
    
        <aop:around
            pointcut-ref="businessService"
            method="doBasicProfiling"/>
    
        ...
    
    </aop:aspect>
    
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
    

    确定通知参数
    使用arg-names显式指定参数名称:

    <aop:before
        pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
        method="audit"
        arg-names="auditable"/>// 多个参数用逗号分隔
    
    

    强类型参数示例:

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

    通知:

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

    声明父类
    <aop:aspect>元素中使用<aop:declare-parents>元素声明父类

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

    recordUsage方法:

    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }
    
    5.5.5切面实例化模型

    xml配置只支持单例模式.

    5.5.6 Advisors

    advisors的概念来自于spring中AOP的定义, 在AspectJ中没有对等的定义. 一个advisor就相当于拥有一个通知的自包含型切面.spring通过<aop:advisor>元素来定义它, 最常见的是事务通知.

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

    可以通过order属性来定义advisor的顺序

    5.6 选择aop的定义样式

    如果在开发中确定要使用aspect, 是使用spring aop还是AspectJ, 是使用xml形式还是注解形式呢?

    5.6.1 Spring AOP 与 Full AspectJ

    如果你只是在spring的bean上执行操作, 则用Spring AOP, 如果你还要操作一些不被spring容器管理的对象, 则用AspectJ.

    5.6.2 @AspectJ 与 XML

    在选择了spring aop后, 怎么选择是使用@AspectJ还是xml.
    如果使用aop作为服务工具, 则使用xml更好(即切点表达式是否可能更改的配置的一部分), 而且使用xml配置, 可以更清楚的看到系统中哪些地方存在切面.

    但xml有两个缺点:

    • 首先,它没有将它所处理的需求的实现完全封装在一个地方。但是使用注解时, 这些都将被封装在一个单模块中.
    • XML样式在它能表达的内容上稍微有些限制:只支持“单例”方面实例化模型,并且不可能组合XML中声明的命名切入点。
    @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的缺点是不能组合这两种定义.

    5.7 混合使用

    两种模式可以混合使用, 所有的这些实现都依赖相同的底层实现机制.

    5.8 代理机制

    Spring AOP使用JDK动态代理或CGLIB为给定的目标对象创建代理, 如果要代理的目标对象实现至少一个接口,则使用JDK动态代理。如果目标对象没有实现任何接口,则创建一个CGLIB代理。

    如果想强制使用CGLIB代理, 则要考虑以下几点:

    • 不能通知final方法. 因为它不能被重写.
    • 从Spring 4.0开始,代理对象的构造函数不再被调用两次, since the CGLIB proxy instance is created through Objenesis. Only if your JVM does not allow for constructor bypassing, you might see double invocations and corresponding debug log entries from Spring’s AOP support.

    <aop:config>元素上配置proxy-target-class属性为true即可.

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

    要在使用@AspectJ自动代理支持时强制CGLIB代理,请将<aop:aspectj-autoproxy>元素的代理目标类属性设置为true.

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

    如果存在多个<aop:config>, 在运行时会将其合并成为一个<aop:config>定义, 其中最强的代理配置将被使用.即在<tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/>中任何一个上使用了proxy-target-class=true, 都将强制以上三个都使用CGLIB代理.

    5.8.1 理解AOP代理

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

    直接调用foo方法, 方法将直接在对象引用上调用:

    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();
        }
    }
    
    image.png

    当引用是一个代理时,则会发生变化:

    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();
        }
    }
    
    image.png

    此处的关键是在main方法中对foo方法的调用.对该对象引用的方法调用是对代理的调用。因此,代理可以委托给与特定方法调用相关的所有拦截器(通知)。然而,一旦调用最终到达目标对象(在本例中是SimplePojo引用),它对自身执行的任何方法调用,比如this.bar()或this.foo(),都将针对这个引用而不是代理来调用。这具有重要意义。这意味着自调用不会导致与方法调用关联的通知有机会执行。
    最好的方法是重构代码,这样就不会发生自调用。

    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>在配置中声明方面之外,还可以通过编程创建通知目标对象的代理. 在这里,我们只关注通过使用@AspectJ方面自动创建代理的能力。
    可以使用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);
    
    // add an aspect, the class must be an @AspectJ aspect
    // you can call this as many times as you need with different aspects
    factory.addAspect(SecurityManager.class);
    
    // you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
    factory.addAspect(usageTracker);
    
    // now get the proxy object...
    MyInterfaceType proxy = factory.getProxy();
    
    5.10 在spring容器中使用AspectJ

    如果您的需求超出了Spring AOP单独提供的功能,我们将研究如何使用AspectJ编译器或编织器来代替或补充Spring AOP。
    要引入spring-aspects.jar.

    相关文章

      网友评论

          本文标题:spring-core-5 AOP

          本文链接:https://www.haomeiwen.com/subject/zzyofctx.html