2021-09-28

Spring重要模块----AOP详解(一)


前言

面向切面编程 (AOP) 通过提供另一种思考程序结构的方式来补充面向对象编程 (OOP)。OOP 中模块化的关键单位是类,而 AOP 中模块化的单位是方面。方面能够实现跨越多种类型和对象的关注点(例如事务管理)的模块化。

Spring 的关键组件之一是 AOP 框架。虽然 Spring IoC 容器不依赖于 AOP,但 AOP 补充了 Spring IoC 以提供一个非常强大的中间件解决方案。


一、AOP 概念

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

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

Advice(通知):切面在特定连接点采取的行动。不同类型的建议包括“周围”、“之前”和“之后”建议。(通知类型将在后面讨论。)许多 AOP 框架,包括 Spring,将通知建模为拦截器,并在连接点周围维护一个拦截器链。

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

目标对象:被一个或多个方面建议的对象。也称为“建议对象”。由于 Spring AOP 是使用运行时代理实现的,所以这个对象始终是一个被代理的对象。

AOP 代理:由 AOP 框架创建的对象,用于实现方面契约(建议方法执行等)。在 Spring 框架中,AOP 代理是 JDK 动态代理或 CGLIB 代理。

编织:将方面与其他应用程序类型或对象联系起来以创建建议对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。

二、增强密码-通知Advice

Spring AOP 包括以下类型的通知:

  • 前通知:在连接点之前运行的通知,但没有能力阻止执行流继续到连接点(除非它抛出异常)。
  • 返回后通知:在连接点正常完成后运行的通知(例如,如果方法返回而没有抛出异常)。
  • 抛出后通知:如果方法通过抛出异常退出,则运行建议。
  • After (finally) 通知:不管连接点退出的方式(正常或异常返回)都将运行的通知。
  • 环绕通知:环绕连接点的通知,例如方法调用。这是最有力的建议。环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续连接点还是通过返回自己的返回值或抛出异常来缩短建议的方法执行。

三、AOP 代理区别

Spring AOP 默认为 AOP 代理使用标准的 JDK 动态代理。这允许代理任何接口(或接口集)。

Spring AOP 也可以使用 CGLIB 代理。这是代理类而不是接口所必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于编程接口而不是类是一种很好的做法,因此业务类通常实现一个或多个业务接口。在需要建议未在接口上声明的方法或需要将代理对象作为具体类型传递给方法的情况下,可以 强制使用 CGLIB。

四、AOP 代理—@AspectJ 支持

1.启用@AspectJ 支持
要在 Spring 配置中使用 @AspectJ 切面,需要启用 Spring 支持以基于 @AspectJ 切面配置 Spring AOP,并根据这些切面是否建议自动代理 bean。通过自动代理,我们的意思是,如果 Spring 确定一个 bean 被一个或多个方面通知,它会自动为该 bean 生成一个代理来拦截方法调用并确保根据需要运行通知。可以使用 XML 或 Java 样式的配置启用 @AspectJ 支持。无论哪种情况,还需要确保 AspectJ 的aspectjweaver.jar库位于应用程序的类路径中(版本 1.8 或更高版本)。该库在libAspectJ 发行版的目录中或从 Maven 中央存储库中可用。

1.1使用 Java 配置启用 @AspectJ 支持
要使用 Java 启用 @AspectJ 支持@Configuration,请添加@EnableAspectJAutoProxy 注释,如以下示例所示:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

1.2使用 XML 配置启用 @AspectJ 支持
要使用基于 XML 的配置启用 @AspectJ 支持,请使用该aop:aspectj-autoproxy 元素,如以下示例所示:

<aop:aspectj-autoproxy/>

2.声明一个方面
启用@AspectJ 支持后,@AspectSpring 会自动检测到应用程序上下文中定义的任何 bean,其类是 @AspectJ 方面(具有注释)并用于配置 Spring AOP。接下来的两个示例显示了一个不太有用的方面所需的最小定义。两个示例中的第一个显示了应用程序上下文中的常规 bean 定义,该定义指向具有@Aspect注释的 bean 类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
</bean>

两个例子中的第二个展示了NotVeryUsefulAspect类定义,用org.aspectj.lang.annotation.Aspect注解来注解;

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

注意: 通过组件扫描自动检测方面
您可以在 Spring XML 配置中通过类中的@Bean方法将方面类注册为常规 bean @Configuration,或者让 Spring 通过类路径扫描自动检测它们——与任何其他 Spring 管理的 bean 相同。但是,请注意, @Aspect注释不足以在类路径中进行自动检测。为此,您需要添加一个单独的@Component注解(或者,根据 Spring 组件扫描器的规则,添加一个符合条件的自定义构造型注解)。

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

构成@Pointcut注解值的切入点表达式是一个正则 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("execution(public * *(..))")
private void anyPublicOperation() {} 

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

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} 
  • anyPublicOperation 如果方法执行连接点表示任何公共方法的执行,则匹配。
  • inTrading 如果方法执行在交易模块中,则匹配。
  • tradingOperation 如果方法执行代表交易模块中的任何公共方法,则匹配。

4.增强型通知
Advice 与切入点表达式相关联,并在与切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。

5.实例
由于并发问题(例如,死锁失败者),业务服务的执行有时会失败。如果重试该操作,下一次尝试很可能会成功。对于适合在这种情况下重试的业务服务(不需要返回给用户解决冲突的幂等操作),我们希望透明地重试操作以避免客户端看到 PessimisticLockingFailureException. 这是一个明确跨越服务层中的多个服务的需求,因此是通过方面实现的理想选择。

因为要重试操作,所以需要使用around通知,这样可以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接口,因此我们可以将方面的优先级设置为高于事务建议(我们每次重试时都需要一个新的事务)。该maxRetries和order性能都Spring配置。主要动作发生在doConcurrentOperationaround通知中。请注意,目前,我们将重试逻辑应用于每个businessService(). 我们尝试继续,如果我们失败了PessimisticLockingFailureException,我们会再试一次,除非我们已经用尽了所有的重试尝试。
对应的Spring配置如下:

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

然后我们可以使用注解来注解服务操作的实现。将切面更改为仅重试幂等操作涉及细化切入点表达式,以便仅@Idempotent操作匹配,如下所示:

@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值