AOP-面向切面编程

发布时间 2023-07-18 16:07:54作者: primaryC

1,AOP 概念

  面向切面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。OOP中模块化的关键单位是类,而AOP中模块化的单位是切面。切面使跨越多种类型和对象的关注点(如事务管理)模块化。(这样的关注点在AOP文献中通常被称为 "交叉(crosscutting)" 关注点)。

1,术语

  • Aspect(切面): 一个跨越多个类的关注点的模块化。事务管理是企业级Java应用中横切关注点的一个很好的例子。在Spring AOP中,切面是通过使用常规类(基于 schema 的方法)或使用 @Aspect 注解的常规类(@AspectJ 风格)实现的。

  • Join point: 程序执行过程中的一个点,例如一个方法的执行或一个异常的处理。在Spring AOP中,一个连接点总是代表一个方法的执行。

  • Advice: 一个切面在一个特定的连接点采取的行动。不同类型的advice包括 "around"、"before" 和 "after" 的advice(Advice 类型将在后面讨论)。许多AOP框架,包括Spring,都将advice建模为一个拦截器,并在连接点(Join point)周围维护一个拦截器链。

  • Pointcut: 一个匹配连接点的谓词(predicate)。advice与一个切点表达式相关联,并在切点匹配的任何连接点上运行(例如,执行一个具有特定名称的方法)。由切点表达式匹配的连接点概念是AOP的核心,Spring默认使用AspectJ的切点表达式语言。

  • Introduction: 代表一个类型声明额外的方法或字段。Spring AOP允许你为任何 advice 的对象引入新的接口(以及相应的实现)。例如,你可以使用引入来使一个bean实现 IsModified 接口,以简化缓存。(介绍在AspectJ社区中被称为类型间声明)。

  • Target object: 被一个或多个切面所 advice 的对象。也被称为 "advised object"。由于Spring AOP是通过使用运行时代理来实现的,这个对象总是一个被代理的对象。

  • AOP proxy: 一个由AOP框架创建的对象,以实现切面契约(advice 方法执行等)。在Spring框架中,AOP代理是一个JDK动态代理或CGLIB代理。

  • Weaving(织入): 将aspect与其他应用程序类型或对象连接起来,以创建一个 advice 对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。Spring AOP和其他纯Java AOP框架一样,在运行时进行织入。

2,AOP 代理

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

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

3,Spring AOP 和 AspectJ 的区别

  • SpringAOP 是 spring 支持的面向切面 AOP 编程。
  • AspectJ 是一个面向切面的框架,它扩展了 Java 语言。AspectJ 定义了 AOP 语法,它有一个专门的编译器用来生成遵守 Java 字节编码规范的 Class 文件。

4,使用

1,启用 @AspectJ

  • 注解
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
  • XML 配置文件
<aop:aspectj-autoproxy/>

2,声明一个 Aspect

  • 注解
package com.xyz;

import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {
}

3,声明一个切点 PointCut

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

Spring AOP支持以下AspectJ的切点指定器(PCD),用于切点表达式中

  • 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 类型
完整的AspectJ点式语言支持Spring不支持的其他点式指定符:call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this 和 @withincode。在由Spring AOP解释的pointcut表达式中使用这些 pointcut 指定器会导致> 抛出 IllegalArgumentException。

Spring AOP支持的切点指定器集合可能会在未来的版本中扩展,以支持更多的AspectJ切点指定器。

组合切点 PointCut 表达式

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

package com.xyz;

@Aspect
public class Pointcuts {

    @Pointcut("execution(public * *(..))")
    public void publicMethod() {} (1)

    @Pointcut("within(com.xyz.trading..*)")
    public void inTrading() {} (2)

    @Pointcut("publicMethod() && inTrading()")
    public void tradingOperation() {} (3)
}

一些常见的 PointCut 表达式

  • 任何 public 方法的 execution
execution(public * *(..))
  • 任何名称以 set 开头的方法的 execution。
execution(* set*(..))
  • AccountService 接口所定义的任何方法的 execution:
execution(* com.xyz.service.AccountService.*(..))
  • 在 service 包中定义的任何方法的 execution:
execution(* com.xyz.service.*.*(..))
  • 在 service 包或其子包中定义的任何方法的 execution。
execution(* com.xyz.service..*.*(..))
  • service 包内的任何连接点(仅在Spring AOP中的method execution)。
within(com.xyz.service.*)
  • 在 service 包或其一个子包中的任何连接点(仅在Spring AOP中的method execution)。
within(com.xyz.service..*)
  • 任何代理实现了 AccountService 接口的连接点(仅在Spring AOP中的method execution)。
this(com.xyz.service.AccountService)
  • 目标对象实现了 AccountService 接口的任何连接点仅在Spring AOP中的method execution)。
target(com.xyz.service.AccountService)
  • 任何连接点(仅在Spring AOP中的method execution)都需要一个参数,并且在运行时传递的参数是 Serializable 的。
args(java.io.Serializable)
  • 任何连接点(仅在Spring AOP中的method execution),其中目标对象有 @Transactional 注解。
@target(org.springframework.transaction.annotation.Transactional)
  • 任何连接点(仅在Spring AOP中的method execution),其中目标对象的声明类型有一个 @Transactional 注解。
@within(org.springframework.transaction.annotation.Transactional)
  • 任何连接点(仅在Spring AOP中的method execution),其中执行的方法有一个 @Transactional 注解。
@annotation(org.springframework.transaction.annotation.Transactional)

*任何连接点(仅在Spring AOP中的method execution),它需要一个参数,并且所传递的参数的运行时类型具有 @Classified 注解。

@args(com.xyz.security.Classified)
  • 在一个名为 tradeService 的Spring Bean上的任何连接点(仅在Spring AOP中的method execution)。
bean(tradeService)
  • 在Spring Bean上的任何连接点(仅在Spring AOP中的method execution),其名称与通配符表达式 *Service 相匹配。
bean(*Service)

4,声明一个 Advice

  1. 内联的切点表达式
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }
}
  1. 命名的切点表达式
@Aspect
@Component
public class AspectTest {

    @Pointcut("execution(* com.spring1.service.aop.ServiceImpl.*(..))")
    public void pointCut(){}

    //这里 pointCut() 可以是其他类中的,要写全限定名
    @Before("pointCut()")
    public void before(JoinPoint point){
        System.out.println("前置通知:" + point);
    }
}

增强的几种类型

  • @Before
    方法执行前

  • @AfterReturning
    当一个匹配的方法执行正常返回

@Aspect
public class AfterReturningExample {
    
    //retVal 可以获取方法返回值
    @AfterReturning(
        pointcut="execution(* com.xyz.dao.*.*(..))",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}
  • @AfterThrowing
    当一个匹配的方法执行通过抛出异常退出时,After throwing advice 运行
@Aspect
public class AfterThrowingExample {
    // throwing 希望advice只在给定类型的异常被抛出时运行,而且你也经常需要在advice body中访问被抛出的异常
    @AfterThrowing(
        pointcut="execution(* com.xyz.dao.*.*(..))",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}
  • @After
    当一个匹配的方法执行退出时,After (finally) advice 会运行
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("execution(* com.xyz.dao.*.*(..))")
    public void doReleaseLock() {
        // ...
    }
}

  请注意,AspectJ中的 @After advice 被定义为 "after finally advice",类似于try-catch语句中的finally块。它将对任何结果、正常返回或从连接点(用户声明的目标方法)抛出的异常进行调用,这与 @AfterReturning 不同,后者只适用于成功的正常返回。

  • @Around

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

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

@Aspect
public class AroundExample {

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

始终使用符合你要求的最不强大的 advice 形式。

例如,如果 before advice 足以满足你的需要,就不要使用 around advice。

Advice 参数:

  • 访问当前的 JoinPoint

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

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

  • getArgs(): 返回方法的参数。

  • getThis(): 返回代理对象。

  • getTarget(): 返回目标对象。

  • getSignature(): 返回正在被 advice 的方法的描述。

  • toString(): 打印对所 advice 的方法的有用描述。

向 Advice 传递参数:

@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

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