【SpringBoot笔记8】Spring AOP 面向切面编程(应用篇)

原文链接: https://docs.spring.io/spring/docs/5.1.3.RELEASE/spring-framework-reference/core.html#aop

参考:官方文档

本文使用的是SpringBoot框架!!!

Spring2.0版本开始引入AOP(面向切面编程)。

AOPSpring Framework中的作用是:

  • 提供声明式的企业服务,Spring提供的声明式事务管理就是其中最重要的一个服务。
  • 让用户能够实现自定义的切面,应用AOP丰富他们的OOP应用。

SpringBoot中,为了使用AOP功能,需要引入spring-boot-starter-aop依赖:

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-aopartifactId>
dependency>

1 AOP中的概念

首先,学习AOP中重要的几个概念:

  • Aspect(切面):切面指的是跨越多个类的关注点。例如,在Spring事务管理中,事务管理就是跨越了多个类的关注点,它是一种公共的需求,可以模块化的。在Spring AOP中,切面通常可以通过XML配置或者使用@Aspect注解来声明。
  • Join Point(连接点):连接点指的是程序执行过程中的一个点。比如,程序中执行的一个方法或者处理的一个异常。在Spring AOP中,连接点通常指的是一个方法的执行。
  • Advice(通知):通知指的是在特定的连接点上被切面执行的一个动作。通知通常包括前置通知、后置通知、环绕通知这几种类型。很多AOP框架,包括Spring,通过拦截器来实现通知,并在连接点附近维持一个拦截器链。
  • Pointcut(切入点):切入点指的是匹配连接点的谓词,通常使用切入点表达式来描述切入点。通知绑定切入点,并在切入点匹配的那些连接点上被执行。Spring默认使用AspectJ的切入点表达式来描述切入点。
  • Target Object(目标对象):目标对象指的是被加上通知的对象。因为Spring AOP是使用的代理模式实现的,因此目标对象指的是代理对象。
  • AOP Proxy(AOP代理)Spring框架实现AOP是使用的JDK动态代理或者CGLIB代理。

2 通知的类型

Spring AOP提供了以下几种通知类型:

  • Before Advice(前置通知):在连接点之前执行的通知,但是不能阻止连接点的执行(除非跑出异常)。

  • After Returning Advice(正常返回通知):连接点正常返回后执行的通知。

  • After Throwing Advice(异常返回通知):在连接点抛出异常,并返回后执行的通知。

  • After (Finally) Advice(返回通知):无论是连接点正常返回还是抛出异常,都会执行的通知。

  • Around Advice(环绕通知):环绕通知是围绕在连接点前后的通知,这是最为强大的通知,能够在连接点前后自定义一些操作。环绕通知还需要负责决定是继续执行连接点(调用ProceedingJoinPoint的proceed方法)还是中断执行,并返回自己的返回值或者抛出异常。

建议根据需要选择使用能够满足需求的同时且功能最简单的通知。不要一上来就选择功能最强大的环绕通知。

3 支持@AspectJ

@AspectJ 指的是采用在Java 类上通过注解的方式来声明一个切面的方式。@AspectJ风格是AspectJ 5发行版引入的新风格。Spring使用AspectJ提供的库来解析和匹配切入点。但是AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或织如。

3.1 开启@AspectJ支持

为了使用@AspectJ支持的功能,首先需要在配置类上使用@EnableAspectJAutoProxy注解,开始AOP功能:

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {

}

这相当于基于xml配置中使用的

3.2 声明一个Aspect(切面)

当开启了AOP功能,任何在应用上下文中被@Aspect注解注释的类都将被Spring AOP框架自动的发现并加载为一个Aspect(切面)。

下面的例子声明了一个叫做CustomAspect的切面:

package com.tao.aop.aspect;

import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;

@Component
@Aspect
public class CustomAspect {

}

注意:一个@Aspect 注解并不能在Spring上下文中注册这个bean,需要添加@Component注解来注册bean,不然这个切面不会生效。

@Aspect注释的类就是一个切面类。这个切面类可以有成员变量和方法,也可以包含切入点、通知。

3.3 声明一个Pointcut(切入点)

切入点是用来匹配那些执行切面的连接点的。因为Spring AOP只支持方法执行类的连接点,所以可以将切入点看作是用来匹配那些方法的表达式。

定义一个切入点涉及两个要素:

  1. 切入点签名:通常是一个返回值为void的函数。
  2. 切入点表达式:通常会用来匹配执行切面的方法。

通常,使用@Pointcut注解注释一个返回值为void的函数,就定义了一个切入点:

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

3.3.1 Spring AOP支持的切入点标志符

Spring AOP在切入点表达式中支持以下几种AspectJ切入点标志符(PCD):

  • execution:用于匹配方法执行的连切入点。
  • within:用来匹配指定类中的全部方法或指定包中的某些类中的全部方法。
  • this:当代理对象bean 是指定类的实例时匹配。
  • target:当被代理的应用程序对象是指定类的实例时匹配。
  • args:当执行的方法的参数是指定类型时匹配。
  • @target:当被代理对象上有指定类型的注解时匹配。
  • @args:当方法参数上有指定类型的注解时匹配。
  • @within:当类上有指定注解时匹配,和@target 有点类似。
  • @annotation:当匹配的方法上有指定类型的注解时匹配。

Spring AOP还支持一个叫做 **bean **的切入点标志符,当被代理的应用程序对象是Spring容器中bean的名称时匹配,在bean切入点标志符中可以使用&&||!来组合表达式。

3.3.2 切入点表达式的组合

可以通过使用&&||!来组合切入点表达式,用来表示更加复杂的匹配规则。

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

第三个切入点表达式就是由第一个和第二个切入点表达式通过&&组合而成的。

3.3.3 共享通用的切入点表达式

在应用中,我们可以通过定义一个通用的类,并在其中定义一些通用的切入点表达式来达到共享和代码重用的作用。

package com.xyz.someapp.aop.aspect;

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

    /**
     * 匹配com.xyz.someapp包及其子包下的service包下的任意类的返回值为任意值的参数任意的任意方法。
     */
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}

    /**
     * 匹配com.xyz.someapp.dao包下的任意类的返回值为任意值的参数任意的任意方法。
     */
    @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

使用的时候,可以把它看作工具类一样来使用:

@Aspect
public class BeforeExample {

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

3.3.4 例子

Spring AOP中用的最多的切入点标志符就是execution,用来匹配方法执行,下面是它的声明格式:

execution(修饰符? 返回值类型 类类型?方法名(参数) 异常?)

除了返回值类型、方法名、参数这3个是必须的,其他的都是可选的。

下面是切入点的一些例子。

  • 匹配所有public方法

    execution(public * *(..))
    
  • 匹配所有以set开头的方法

    execution(* set*(..))
    
  • 匹配AccountService类中所有方法

    execution(* com.xyz.service.AccountService.*(..))
    
  • 匹配service包下的所有类中的所有方法

    execution(* com.xyz.service.*.*(..))
    
  • 匹配service包及其子包下的所有类中的所有方法

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

其他的一些例子:

  • service 包下的任何连接点(在Spring AOP中指的是方法)

    within(com.xyz.service.*)
    
  • service 包及其子包下的任何连接点(在Spring AOP中指的是方法)

    within(com.xyz.service..*)
    
  • 当代理对象实现了AccountService接口时匹配

    this(com.xyz.service.AccountService)
    
  • 当被代理对象实现了AccountService接口时匹配

    target(com.xyz.service.AccountService)
    
  • 当连接点(在Spring AOP中指的是方法)的参数只有一个并且实现了Serializable 时匹配

    args(java.io.Serializable)
    
  • 当被代理对象有@Transactional注解时匹配

    @target(org.springframework.transaction.annotation.Transactional)
    
  • 当连接点所在的类上有@Transactional注解时匹配,和@target很像

    @within(org.springframework.transaction.annotation.Transactional)
    
  • 当方法上有@Transactional 注解时匹配

    @annotation(org.springframework.transaction.annotation.Transactional)
    
  • 当方法只有一个参数并且参数上有@Classified注解时匹配

    @args(com.xyz.security.Classified)
    
  • 匹配Spring上下文中叫做tradeServicebean

    bean(tradeService)
    
  • 匹配Spring上下文中名称以Service结尾的bean

    bean(*Service)
    

3.4 声明Advice(通知)

通知又分为前置通知、后置通知、环绕通知三大类。

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

}

也可以在@Before中直接书写切入点表达式:

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

}

3.4.2 后置通知

后置通知又分为3种:

  • 正常返回通知
  • 异常返回通知
  • 返回通知

3.4.2.1 正常返回通知

可以使用@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() {
        // ...
    }

}

有时候,希望在正常返回通知中获得返回的值,可以通过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属性中指定的名称和函数参数的变量名要保持一致。

3.4.2.2 异常返回通知

可以使用@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(Throwable ex) {
        // ...
    }

}

注意:throwing属性中指定的名称和函数参数的变量名要保持一致。

3.4.2.3 返回通知

可以使用@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() {
        // ...
    }

}

3.4.3 环绕通知

可以使用@Around注解来声明以个环绕通知。

环绕通知方法的第一个参数必须是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 {
        // 开始时间
        long beginTime = System.currentTimeMillis();
        
        // 连接点方法的执行
        Object retVal = pjp.proceed();
        
        // 执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        logger.info("请求处理响应时间:" + time + " 毫秒");
        
        return retVal;
    }

}

3.4.4 通知的参数

可以向通知传递一些参数。

3.4.4.1 传递JoinPoint

任何通知都可以传递一个类型为org.aspectj.lang.JoinPoint的参数作为第一个参数(注意,环绕通知的第一个参数必须是ProceedingJoinPoint类型,它是JoinPoint的子类,他俩都是接口)。

JoinPoint提供了一些有用的方法:

// 返回目标方法的参数
getArgs();

// 返回代理对象
getThis();

// 返回目标对象
getTarget();

// 返回目标方法的签名,包含了方法的描述信息
getSignature();

// 打印目标方法的有用信息
toString();

可以从javadoc中了解更多详细信息。

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

}

3.4.4.2 传递参数给通知

如果需要将被执行函数的参数传入通知里面,可以通过args来绑定。

下面的例子描述了一个前置通知,通过args将被执行函数的类型为Account的参数account绑定并传入通知里面:

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

也可以换一种写法,先声明一个切入点,绑定参数:

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

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

同理,thistarget@within@target@annotation@args也可以以相同的方式绑定参数。

3.4.4.3 参数名

因为在Java反射中参数名是无法获得的,所以Spring AOP使用以下策略来确定参数名:

  • 用户可以通过argNames属性显示的指定参数名。如果显示的指定了,那么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
    }
    

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

    如果只传入JoinPointProceedingJoinPointJoinPoint.StaticPart类型的参数,你可以直接省略掉argNames属性:

    @Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
    public void audit(JoinPoint jp) {
        // ... use jp
    }
    
  • 如果没有指定argNames,那么Spring AOP将会从该类的调用信息中查找并确定参数名称。该类在编译的时候至少要有调试信息-g:vars

  • 如果在编译代码时没有必要的调试信息,Spring AOP就会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,而通知方法只接受一个参数,那么这种配对是明显的)。如果给定可用信息,变量绑定是不明确的,则抛出一个AmbiguousBindingException异常。

  • 如果以上策略都失败了,那么会抛出IllegalArgumentException异常。

3.4.4.4 在通知中传递参数给被调函数

在下面的环绕通知中,传递参数newPattern给被调用函数:

@Around("execution(List 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});
}

3.4.5 通知的顺序

如果当多个通知都在同一个连接点上运行时,它们的顺序又将是如何的呢?

Spring AOP遵循AspectJ的优先级顺序策略:

  • 在前置通知中,优先级高的先执行。
  • 在后置通知中,优先级高的后执行。
  • 如果当两个通知定义在不同的两个切面类中并且作用于同一个连接点,除非显示指定它们的优先级,否则优先级将是未定义的。可以通过让切面类实现org.springframework.core.Ordered接口或者在类上面使用@Order注解来指定优先级,值越小,优先级越高。
  • 如果两个通知定义在同一个切面类中并且作用于同一个连接点呢?这种情况下通过Java反射是无法确定优先级的,最好的办法就是将这两个通知放入不同的切面类中,然后通过@Order指定优先级。

3.4.6 为被代理对象引入新方法

参考Spring-AOP通过注解@DeclareParents引入新的方法

3.5 基于xml配置的切面

详细见https://docs.spring.io/spring/docs/5.1.3.RELEASE/spring-framework-reference/core.html#aop-schema

你可能感兴趣的:(SpringBoot)