Spring AOP详解及其用法(一)

引言

在企业级服务中,经常面临多个业务类中需要完成一些相同的事情,如日志记录、异常处理、事物管理、安全管理等,这些多个业务类共同关注的点也叫横切关注点( cross-cutting concern )。如果在每个业务类中都加上这些横切关注点逻辑,不仅工作量会很大,而且容易产生冗余代码。这时候为解决横切关注点的面向切面编程(AOP)应运而生,AOP补充了面向对象编程(OOP)。OOP中模块化的关键单元是类,而在AOP中模块化的单元是切面。切面支持跨多个类型和对象的关注点(例如事务管理)。

​ Spring的一个关键组件就是AOP框架,虽然Spring IOC容器并不依赖Spring AOP(这意味着你在不需要的时候可以不必在项目中引入Spring AOP依赖),但是AOP补充了Spring IoC,提供了一个非常强大的中间件解决方案。
Spring提供了两种简单而强大的自定义切面的方式:

  • 基于Schema的,也就是在XML配置文件中提供AOP配置和基于注解的AOP配置
  • 基于注解的AOP配置

这两种方式都提供了丰富的通知功能和使用 AspectJ 切点表达式语言 的支持,但是在织入时仍然使用Spring AOP。

AOP在Spring框架中使用主要用于:

  • 提供声名式企业服务,最重要的服务莫过于声名式事物管理
  • 让开发者实现自定义切面,在开发过程中使用AOP补充OOP编程的不足

1. AOP概念

首先,让我们弄清楚一些AOP的核心核心概念和技术术语:

切面(Aspect):一个跨多个类关注的模块,在企业级Java应用中的事物管理(Transaction Management)就是一个横切关注点的很好例子。在Spring AOP中通过给普通POJO类在XML文件中进行AOP配置后者给普通POJO类添加@Aspect注解实现切面的定义。

连接点(Joint Point): 程序执行过程中一个点,例如方法的执行或者异常处理,在Spring AOP中,连接点始终代表方法的执行;

通知(Advice):切面在特定的连接点上发生的行为,不通类型的通知包括"Around",“Before”,“After”,"After Returning"等通知(通知类型之后再讨论)。很多AOP框架包含Spring,将通知看成拦截器和维护围绕连接点的拦截器链。

切点(Pointcut): 匹配连接点的正则表达式,通知与切点表达式紧密关联,并且运行在任意匹配切点表达式的连接点上(例如具有指定名字的方法的执行)。连接点与切入点表达式匹配的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。

引入(Introduction):代表类型声明其他方法或字段,Spring AOP允许你将新的接口(和相应的实现)引入任何被通知的对象。例如,你可以使用一个Introduction来让一个bean实现一个IsModified接口,以简化缓存。(Introduction在AspectJ社区中称内部类声明。)

目标对象(Target Object):被一个或多个切面通知的对象,也被称作通知对象。由于Spring AOP是由运行时代理实现的,因此目标对象永远是代理对象。

AOP代理:AOP框架为实现切面逻辑而创建的一个通知方法执行的对象,在Spring框架中,AOP代理是指JDK动态代理或者CGLIB代理。

织入(Weaving):将切面与其他应用程序类型或对象链接以创建通知对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。与其他纯Java AOP框架一样,Spring AOP在运行时执行织入。

Spring AOP包含以下5种通知:

  • 前置通知(Before Advice): 连接点方法执行前的通知,并不能阻止连接点方法流程的执行,除非执行过程中抛出异常;
  • 返回通知(After Returning Advice): 连接点正常执行流程之后返回时的通知(不抛出异常的情况下);
  • 异常通知(After Throwning Advice):连接点方法执行过程中抛出异常时的通知;
  • 后置通知(After Advice): 无论连接点方法是否发生异常都会执行的通知;
  • 环绕通知(Around Advice): 环绕连接点方法执行过程的通知,这是AOP 5种通知中功能最强大的通知 。环绕通知可以自定义连接点方法执行前后过程中的行为。它也能选择是执行连接点方法流程,还是通过返回连接点方法的返回值或抛出异常的方式剪切被通知方法的执行。

虽然环绕通知是5种通知中功能最强大的通知,Spring AOP也提供了各种类型的通知,但是我们还是建议你使用能实现你业务需求最弱功能的通知。例如你仅仅需要拿到一个方法的返回值去更新缓存,你最好使用后置通知。虽然使用环绕通知也能实现相同的业务,但是使用最准确的通知能够简化程序执行并尽可能地避免潜在的错误。

所有通知参数都是静态类型的,因此你可以使用确定类型的通知参数(例如一个方法执行的返回值类型),而不是对象数组。

匹配切点表达式的连接点概念是AOP中的关键,它将AOP与只提供拦截的旧技术区分开来。切入点使通知能够独立于面向对象的层次结构。例如,你可以给分布在服务层中的多个业务操作对象加上一个环绕通知以提供声名式事物管理。

2. Spring AOP的功能和目标

Spring AOP是基于纯Java对象实现的,不需要特殊的编译过程。Spring AOP不需要控制类加载器层次结构,因此适合在servlet容器或应用服务器中使用。Spring AOP目前只支持方法级别的连接点拦截,通知必须加在Spring容器管理的Bean方法上,属性级别的拦截目前还没有实现。如果你需要属性访问和更新连接点,可以考虑使用AspectJ语言。

Spring AOP的AOP方法与大多数其他AOP框架不同。其目的不是提供最完整的AOP实现(尽管Spring AOP非常强大)。相反,其目标是提供AOP实现和Spring IoC之间的紧密集成,以帮助解决企业应用程序中的常见问题。

Spring框架的AOP功能通常与Spring IoC容器一起使用,切面(Aspect)是通过使用普通的bean定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的一个重要区别。使用Spring AOP不能轻松或有效地完成某些事情,比如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ是最佳选择。然而,我们的经验是,Spring AOP为企业Java应用程序中大多数适合AOP的问题提供了一个优秀的解决方案。

Spring AOP从未试图与AspectJ竞争来提供全面的AOP解决方案。我们认为基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是竞争的。Spring将Spring AOP和IoC与AspectJ无缝集成,从而在一致的基于Spring的应用程序体系结构中支持AOP的所有使用。这种集成并不影响Spring AOP API或AOP Alliance API,Spring AOP保持向后兼容。有关Spring AOP api的讨论,请参阅下一章。

Spring框架的一个核心原则就是非侵入性,它的思想就是不强迫你将框架中指定的类或接口引入到你的业务或领域模型中。然而在某些地方,Spring框架给你引入Spring框架依赖到你代码库中的可选项。之所以提供这些选项,是因为在某些场景中,以这种方式阅读或编写某些特定功能的代码可能更容易。然而,Spring框架几乎总是为你提供这样的选择:你可以自由地做出明智的决定,即哪种选择最适合你的特定用例或场景。

3 AspectJ支持

@AspectJ指在普通Java类上加上注解使之成为切面类,@AspectJ注解是作为AspectJ项目的一部分引入AspectJ5版本的。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注解。但是AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器。

3.1 开启@AspectJ支持

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

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

3.2 使用Java配置开启@AspectJ支持

使用Java @Configurtion 配置开启@AspectJ支持,你需要在Java配置类上添加@EnableAspectJAutoProxyl注解,示例代码如下:

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=false,exposeProxy=false)
public class AspectjConfig{
    
}
3.3 使用XML配置开启@AspectJ支持

使用基于XML配置开启@AspectJ支持,在应用上下文applicationContext.xml配置文件中添加aop:aspectj-autoproxy元素标签,示例代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"    
    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/context
http://www.springframework.org/schema/context/spring-context.xsd        
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--开启aspectj自动代理支持-->
        <context: conponent-scan base-package="com.example"/>    
        <aop:aspectj-autoproxy proxy-target-class="false" exposeproxy="false"/>
        <!-- 在这里配置bean -->
</beans>

Spring AOP可以使用JDK动态代理或CGLIB动态代理,如果目标对象没有实现任何接口,Spring AOP会创建基于CGLIB的动态代理;如果目标对象实现了一个或多个接口,那么Spring AOP会创建基于JDK的动态代理。如果需要强制使用CGLIB动态代理,可以将proxy-target-class属性设置为true(如果是使用注解风格,则将@EnableAspectJAutoProxy注解的proxyTargetClass方法值改为true),这样即使目标对象实现了一个或多个接口,Spring AOP也会创建CGLIB动态代理。而expose-proxy属性设置为true时(使用@EnableAspectJAutoProxy注解时将其exposeProxy方法值改为true),则可以从ApplicationContext应用上下文中拿到动态代理对象。

4 声名切面

开启了@AspectJ支持后,任何在应用上下文中定义并具有@Aspect注解的Bean就是一个切面,它会被Spring容器自动发现并用来配置Spring AOP。下面的代码示例展示了配置一个切面最小的配置需要:

  1. 在应用上下文中配置一个常规bean定义
<bean id="myAspect" class="com.example.aspect.AspectBean">
    <!-- configure properties of the aspect here -->
</bean>
  1. com.example.aspect.AspectBean上添加org.aspectj.lang.annotation.Aspect注解
package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class AspectBean {

}

切面类可以和其他类一样具有方法和属性,也可以包含切点、通知和引用声名。

通过组件扫描自动发现切面
你可以在你的Spring XML文件中通过一个常规的bean定义,也可以通过类路径扫描自动发现注册切面,与其他Spring 管理的Bean一样。注意添加@Aspect注解对于Spring在类路径中自动发现切面还不够,还需要添加@Component注解。

5 声名切点
5.1 切点定义

切入点确定感兴趣的连接点,从而使我们能够控制何时执行通知。Spring AOP只支持Spring bean的方法执行连接点,因此可以将切入点看作是与Spring bean上的方法执行相匹配的。切入点声明有两部分:

  • 签名:由名称和任何参数组成;
  • 切点表达式:它确定我们对哪个方法执行感兴趣。在AOP的@AspectJ注释风格中,切入点签名由一个常规方法定义提供,切入点表达式通过使用@Pointcut注解来表示(作为切入点签名的方法必须是void返回类型)。

下面的简易代码示例希望能帮助读者弄清楚切点签名和和切点表达式:

@Pointcut("execution(* transfer(..))") // 起点表达式
private void anyOldTransfer() { // 切点签名
    
} 
5.2 Spring中支持的切点表达式

Spring AOP支持以下几种AspectJ 切点指示器(PCD)用于切点表达式中

5.2.1 常用的切点指示器
  • execution: 用于匹配连接点方法执行,这是使用Spring AOP时使用的主要切点指示器,也是控制粒度最小的切点指示器;
  • within: 限制匹配连接点目标对象为确定的类;
  • this: 限制匹配连接点为具有指定bean引用类型的实例;
  • target: 限制匹配连接点目标对象为指定类的实例;
  • args: 限制匹配连接点目标对象方法参数为指定类型;
  • @target: 限制匹配连接点目标对象头部有指定的注解类;
  • @args: 限制匹配连接点目标对象方法参数具有指定类型的注解;
  • @within: 限制匹配连接点目标对象具有指定类型的注解;
  • @anotation: 限制匹配连接点目标对象头上具有指定类型的注解;

Spring AOP也支持另外一个命名为bean的切点指示器,它限制匹配连接点为指定名称的bean或一系列bean集合(使用通配符时)的方法,使用示例如下:

bean(idOrNameOfBean)

idOrNameOfBean字符可以是任何Spring Bean的名字,限制通配符支持使用*字符。因此,如果你为你的Spring bean建立一些命名约定,你可以编写一个bean 切点指示器表达式来选择它们。与其他切点指示器一样,bean切点指示器也可以使用&&(and),||(or)或!(negation)等逻辑操作符。

注意:bean 切点指示器只在Spring AOP中受支持,而在原生AspectJ织入中不受支持,它是AspectJ定义的标准切点指示器的特定于spring的扩展,因此不能在@Aspect模型中声明的切面中使用bean 切点指示器。

5.2.2 联合使用切点指示器

你可以使用&&、||或!操作符联合使用多个切点表达式,也可以通过名字来引用切点表达式。下面的代码示例展示了3种切点表达式的使用:

@Pointcut("execution(public * (..))")
private void anyPublicOperation() {} //匹配任意public访问修饰符修饰的方法

@Pointcut("within(com.xyz.someapp.trading..)") //匹配包名以com.xyz.someapp.trading开头的任意类的所有方法
private void inTrading() {} 

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} //匹配以com.xyz.someapp.trading开头的任意类中任意以public访问修饰符修饰的方法

最佳实践是从较小的命名组件构建更复杂的切入点表达式,如前面所示。当通过名称引用切入点时,应用普通的Java可见性规则(你可以在同一类型中看到private修饰的切入点、层次结构中的protect修饰的切入点、任何地方的public切入点,等等)。可见性不影响切入点匹配。

5.2.3 共享切点定义

在使用企业应用程序时,开发人员通常希望从几个切面引用应用程序的模块和特定的操作集。官方文档建议定义一个“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() {}

    /*
     *匹配定义在数据访问层且目标对象在com.xyz.someapp.dao包及其子包中所有类中的任意方法 
     */
    @Pointcut("within(com.xyz.someapp.dao..)")
    public void inDataAccessLayer() {}

    /*
     * 一个业务服务是定义在服务层接口中任意方法的执行
     * 这种假定所有接口放在service包中,而实现类在其子包中
     * 如果你把所有接口按功能分组(例如服务层接口在com.xyz.someapp.abc.service包和                      * com.xyz.someapp.def.service包中,这样你可以这样使用切点表达式:
     * "execution(* com.xyz.someapp..service..(..))"
     * 同样,你可以使用bean切点指示器如"bean(Service)"书写切点表达式
     * 这假定你以同样的风格命名Spring service Bean
     */
    @Pointcut("execution( com.xyz.someapp..service..(..))")
    public void businessService() {}

    /* 
     *匹配数据库访问层中目标对象在com.xyz.someapp.dao..(..)及其子包中的任意方法
     */
    @Pointcut("execution( com.xyz.someapp.dao..(..))")
    public void dataAccessOperation() {}

}

你可以在任何需要切点表达式的地方引用在这样一个切面中定义的切点。

5.2.4 切点表达式解读与使用示例

在使用Spring AOP中,开发者最常使用execution切点指示器,execution切点表达式格式如下:

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

除了返回类型模式(前面代码段中的rt-type-pattern)、名称模式和参数模式之外,其他所有部分都是可选的

  • modifiers-pattern:方法访问修饰符模式,可选;
  • ret-type-pattern: 方法返回类型模式,必须要有,*通配符代表任意返回类型;
    declaring-type-pattern:声名类型模式,可选项,有的话使用包含包名的全限定类名;
  • name-pattern: 方法名模式,方法名,通配符代表任意方法名;
  • (param-pattern):方法参数模式,()代表没有参数,(…)代表0个或多个参数

下面的代码示例展示了一些常用的切点表达式的使用:

execution(public * *(..)) //匹配任意公共方法
    
execution(* set*(..)) //匹配任意方法名以set字符开头的方法   
    
//匹配com.xyz.service.AccountService类下任意方法
execution(* com.xyz.service.AccountService.*(..)) 
    
//匹配com.xyz.service包及其子包下任意类的所有方法        
 execution(* com.xyz.service.*.*(..))
    
within(com.xyz.service.*) //匹配com.xyz.service包下任意接口的所有方法
    
within(com.xyz.service..*) //匹配com.xyz.service包及其子包下任意类的所有方法  

//匹配实现AccountService接口的代理的任意方法:  
this(com.xyz.service.AccountService) 

 //匹配实现AccountService接口的目标对象的任意方法:   
target(com.xyz.service.AccountService)
   
args(java.io.Serializable) //匹配只带一个参数的方法,且该参数为可序列化参数
 
//匹配具有Transactional的目标对象任意方法    
@target(org.springframework.transaction.annotation.Transactional)

//匹配目标对象声名有Transactional的方法    
 @within(org.springframework.transaction.annotation.Transactional)

//匹配带有个参数的方法,且运行时参数类型绑定有Classified注解
 @args(com.xyz.security.Classified)
 
 //匹配Spring容器中id或name属性值为tradeService的bean实例的方法
 bean(tradeService)
 
 //匹配Spring容器中id或name属性值以Service结尾的bean实例的方法   
 bean(*Service)
6 声名通知

通知与切点表达式紧密相连,并在程序运行时执行与切点匹配的前置(before)、后置(After)或环绕(Around)方法。切点明确了在哪里进行代码织入,而通知则确定了何时织入增强逻辑,通知可以是一个切点名的引用,也可以是在某处声名的切点表达式。

6.1 前置通知(Before Advice)

你可以在一个切面中使用@Before注解声名前置通知,示例代码如下:

  1. 在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() {
       //切点方法执行前执行的逻辑
    }

}
  1. 在Before注解属性值中引入切点方法名
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void  pointcut() {
        
    }
    @Before("pointcut")
    public void beforeAdvice(){
        //切点方法执行前执行的逻辑
    }

}

@Before注解的源码如下:

package org.aspectj.lang.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Before {
    String value();   //起点表达式值或引用

    String argNames() default ""; //参数名
}
6.2 正常返回通知

After Returning 通知在匹配的方法正常返回时执行,你可以在切面中使用@AfterReturning注解声名After Returning通知,示例代码如下:

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属性中的名字必须与通知方法中的参数名一致,当被拦截切点方法执行返回时,返回值会作为与参数名对应的参数传递给通知方法。也可以通过retVal的类型限制匹配固定类型的返回值,上面的实例中Object类型可以匹配任意类型的返回值。

**注意:**在使用返回通知后返回一个完全不同的引用是不可能的

@AfterReturning注解源码如下:

package org.aspectj.lang.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AfterReturning {
    String value() default ""; //切点表达式引用方法名

    String pointcut() default ""; //切达表达式内容

    String returning() default ""; //切点方法返回值名

    String argNames() default ""; //切点方法参数名
}
6.3 异常通知(After Throwning Advice)

当匹配的连接点方法在程序执行发生异常时会执行异常通知。你可以在切面类中使用@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属性限制匹配和绑定异常到通知参数中,使用示例如下:

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) {
        // 抛出DataAccessException类型异常时执行的逻辑
    }

}

throwing属性中的名字必须与通知方法参数中的名字一致,异常将传递到通知方法中对应的参数值中。抛出的异常类型可以指定连接点为抛出指定异常类型的方法,本例中限定抛出DataAccessException类异常的连接点。

6.4 后置通知(After Advice)

当存在匹配的连接点方法时,后置通知总是会被执行。你可以使用@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() {
        //释放版本锁处理 
    }

}

@After注解类源码如下:

package org.aspectj.lang.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface After {
    String value(); //切点表达式值或切点方法名应用

    String argNames() default ""; //切点方法参数名
}
6.5 环绕通知(Around Advice)

最后一种通知是环绕通知,环绕通知会在匹配的连接点方法周围执行,它有机会在方法执行之前和之后执行工作,并确定何时、如何执行,甚至是否真正执行方法。如果需要以线程安全的方式(例如,启动和停止计时器)共享方法执行前后的状态,通常会使用Around通知。始终使用最不低强度的通知来满足你的需求(也就是说,如果before通知可以满足需求的话,就不要使用around通知)。

通过只用@Around注解来声名环绕通知,通知方法的第一个参数必须是一个ProceedingJoinPoint类型的参数,调用ProceedingJoinPoint对象的proceed()方法会触发底层方法的执行,proceed方法也可以传递一个对象数组,当方法执行时,数组中的值用作方法执行的参数。以下代码示例展示如何使用环绕通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}

环绕通知方法的返回值就是被拦截方法调用时的返回值,例如一个简单的缓存切面在缓存里有值时可以从缓存里返回一个值,没有的话就执行proceed()方法,注意proceed()方法在环绕通知体中可以被执行一次、多次或者零次,这些情况都是合法的。

7 获取通知中的参数

Spring AOP提供了5中通知,这意味着你可以在通知签名中声名你需要的参数(参考前面的正常返回通知和异常通知中的代码示例),而不是一直使用对象数组。我们接下来再看如何获取通知方法中的参数值和其他与上下文相关的参数值。首先,我们来看看如何编写通用的通知,从而找出通知当前通知的方法。

7.1 获取当前连接点(JoinPoint)

任意通知方法都可以声明第一个参数为org.aspectj.lang.JoinPoint类型的参数(注意,环绕通知方法需要声明的第一个参数为ProceedingJoinPoint类型,它是JoinPoint接口的子类)。JoinPoint接口提供了下面这些非常有用的方法:

  • Object[] getArgs() : 返回切点方法参数数组

  • Object getThis() : 返回切点方法代理对象

  • Object getTarget() : 返回切点方法目标对象

  • Signature getSignature() : 返回被通知方法的签名(方法完整描述)

  • String toString() : 被通知方法转字符串

7.5 给通知方法传递参数

到现在,我们已经学会了如何在通知方法中绑定切点方法的返回值和异常值(使用正常返回通知和异常通知),为了是切点方法的参数值可用,你可以使用args切点指示器绑定形式。如果在args表达式中使用参数名代替类型名,则在调用通知方法时将传递相应参数的值作为通知方法的参数值。举个例子可以说明,假设你需要通知一个携带第一个参数为Account类型参数的Dao操作,同时你需要在通知方法中访问该account参数值,你可以这样写:

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

args(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, and @args)也能以类似的风格绑定,下面的两份代码示例展示了如何使用@Auditable注解类匹配连接点方法,并获取注解类的方法值:

  1. 定义Auditable注解类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}
  1. 在通知方法的切点表达式中使用Auditable注解类
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}
7.6 通知方法与泛型

Spring AOP`一样能处理类和方法参数申明中的泛型。假设你有如下代码示例中的泛型类:

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

你可以将方法类型的拦截限制为特定的参数类型,方法是将通知方法参数指定为要拦截方法的参数类型,示例用法如下:

@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<?>并手工检查泛型中的元素类型。

7.7 通过参数的名确定参数

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

@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,ProcedingJoinPoint或JoinPoint.staticPart类型中的一个,你可以你可以将参数名从argNames属性值中移除。例如你要修改前置通知接收一个JoinPont对象,那么argNames属性可以不包含在切点表达式中,示例代码如下:

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

使用argNames属性有点笨拙,因此如果没有指定argNames属性,Spring AOP将查看该类的调试信息,并尝试从局部变量表中确定参数名。只要使用调试信息(-g:vars)编译了类,就会出现此信息。

7.8 处理参数

我们在前面提到过,我们将描述如何使用在Spring AOP和AspectJ中一致工作的参数来编写proceed()调用。解决方案是确保通知签名按顺序绑定每个方法参数,下面的例子演示了如何做到这一点:

@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});
}
8 通知顺序

如果多个通知的代码片段发生在同一个切点上将会发生什么?Spring AOP遵循与AspectJ相同的优先规则来确定通知执行的顺序。在进程进来时,优先级高的通知方法先运行,例如两个前置通知,优先级高的通知先执行;在进程从连接点方法出去时,优先级高的通知后执行,例如两个后置通知方法片段,优先级高的后执行。

当来自不同切面的两个通知逻辑需要在同一个切点上执行时,除非你指定优先级顺序,否则两个通知执行的顺序将是未知的。你可以通过指定不同切面的优先级控制两个切面中通知执行的顺序,在Spring项目中通常通过使切面类实现org.springframework.core.Ordered接口或者添加@Order注解来控制切面的优先级。

8.1 通过实现 Ordered接口定义优先级
package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered
@Aspect    
public class OrderAspect implements Ordered{
    
    private int order = 1;
    
    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }
    @Before("execution(public * com.example.service.impl.*Service.save*(..))")
    public void beforeAdviceMethod(){
        //通知逻辑实现
    }
}
8.2 通过添加@Order注解定义优先级
package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;

@Aspect
@Order(value=1)
public class OrderAspect{
    
    @Before("execution(public * com.example.service.impl.*Service.save*(..))")
    public void beforeAdviceMethod(){
        //通知逻辑实现
    }
}

当定义在同一个切面中的两个同类通知需要运行在同一处切点时,此时通知的顺序是未知的,因为没有办法通过反射来检索javac编译类的声明顺序。解决方法是考虑将这些通知方法分解为每个切面类的每个连接点上的一个通知方法,或者将这些通知片段重构为可以在切面级上排序的独立切面类。

9 引入

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

你可以通过使用@DeclareParents注解创建引用,这个注解用来声明匹配的类具有一个新的父类。例如,给定一个命名为UsageTracked的接口和实现这个接口的实现类,命名为DefaultUsageTracked。以下代码示例表明所有com.xzy.myapp.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注解声名的属性类型决定,如上例中mixin字段的类型为UsageTracked,那么UsageTracked接口就是要实现的接口。@DeclareParents注解的value属性值是一个AspectJ类型模式。任何匹配类型的bean都要实现UsageTracked接口。注意,在前面示例的before建议中,服务bean可以直接用作UsageTracked接口的实现。如果以编程方式访问一个bean,您将编写以下代码:

UsageTracked usageTracked = (UsageTracked) appContext.getBean("myService");
10 一个使用AOP的完整示例

现在我们已经知道怎么单独使用Spring AOP的部分功能了,那么现在让我们来综合使用它做些有用的事情。业务层方法有时会因为并发问题(例如获取锁失败),如果操作重试,那么可能在下一次中成功。对于存在这种并发问题的业务层服务,重试解决问题的合适方法(幂等操作,不需要返回给用户来解决冲突)。我们想通过透明地重试操作以避免客户看到乐观锁失败异常(PessimisticLockingFailureException)。这种需求明显在服务层中很切多个服务类,因此通过切面解决是一个理想的解决方案。

10.1 定义一个切面类

因为我们要进行重试操作,所以需要使用环绕通知,这样就可以多次调用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;
    }

}

注意,由于ConcurrentOperationExecutor切面实现了Ordered接口,我们可以设置通知的优先级高于事物切面的通知。最大重试次数(maxRetries属性)和order属性都是通过Spring配置的。主要的行为都发生在doConcurrentOperation环绕通知中。注意当每次在businessService()方法运行重试逻辑时,程序尝试执行proceed()方法,如果因为捕获到PessimisticLockingFailureExceptio异常导致失败就再重复执行一次,知道重试次数大于最大重试次数为止。

10.2 注册切面类到Spring容器

将ConcurrentOperationExecutor切面类作为bean注册到Spring管理的容器对应的Spring 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>
10.3 切面改进

为了改进切面,使它只重试幂等操作,我们可以定义以下幂等注解类:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.Method})
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 {
    // ...
}
总结

本文笔者主要参考了Spring官方文档中的面向切面编程部分讲解了Spring AOP的一些基本概念,以及如何在项目中开启AspectJ的支持,讲解了基于注解的风格的切面的定义、切点表达式的定义和5种通知的使用。使用的demo代码基本都是官方文档中的代码片段,在笔者的下一篇文章中将使用基于SpringBoot的项目,讲解利用Spring AOP特性实现用户登录日志记录,接口调用耗时日志记录和一些操作权限验证等功能。

参考文档链接: Spring5官方文档Spring Core部分之Aspect Oriented Programming with Spring

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

heshengfu1211

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值