最新最全面的Spring详解(四)——面向切面编程

时间:2022-11-24 15:55:02

前言

最新最全面的Spring详解(四)——面向切面编程

本文为 【Spring】面向切面编程 相关知识,下边将对AOP概述(包含什么是AOPAOP相关术语Spring AOP通知类型),Spring AOP能力和目标AOP代理@AspectJ风格的支持(包含对于 @AspectJ的支持声明一个切面声明一个切入点声明通知引入IntroductionAdvice OrderingAOP的例子),基于schema的AOP支持AOP声明风格以编程方式创建@AspectJ代理等进行详尽介绍~

????博主主页:小新要变强 的主页
????Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
????算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
????Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)

↩️本文上接:最新最全面的Spring详解(三)——Resources,验证、数据绑定和类型转换与Spring表达式语言(SpEL)


目录

最新最全面的Spring详解(四)——面向切面编程

一、AOP 概述

1️⃣什么是AOP

  • 面向切面编程(AOP)通过提供另一种考虑程序结构的方法对面向对象编程(OOP)进行了补充。
  • OOP中模块化的关键单元是类,而AOP中模块化的关键单元是aspect(切面)。
  • Spring的关键组件之一是AOP框架。 虽然Spring IoC容器不依赖于AOP(这意味着如果您不想使用AOP就不需要),但AOP对Spring IoC进行了补充,提供了一个非常强大的企业级解决方案。
    最新最全面的Spring详解(四)——面向切面编程
    这里有几个名词需要了解一下:
  • aop alliance:是AOP联盟,该组织定义了很多针对面向切面的接口api,通常Spring等其它具备动态织入功能的框架依赖此包。
  • AspectJ:AOP虽然是方法论,但就好像OOP中的Java一样,一些先行者也开发了一套语言来支持AOP。目前用得比较火的就是AspectJ语言了,它是一种几乎和Java完全一样的语言,而且完全兼容Java。当然spring也有独立的AOP的实现。

2️⃣AOP相关术语

  • Aspect(切面): 一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。
  • Join point(连接点 ): 在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。
  • Advice(通知): 在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括“around”、“before”和“after”等通知。通知的类型将在后面部分进行讨论。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链。
  • Pointcut(切入点 ): 匹配连接点(Joinpoint)的断言。通知和一个【切入点表】达式关联,并在满足这个切入点的连接点上运行。【切入点表达式如何和连接点匹配】是AOP的核心:Spring缺省使用AspectJ切入点语法。
  • Introduction(引入): Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。
  • Target object(目标对象): 被一个或者多个切面(aspect)所通知(advise)的对象。也有人把它叫做被通知(advised) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。
  • AOP代理 AOP proxy: 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
  • Weaving(织入): 把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象,这个过程叫织入。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。 Spring和其他纯Java AOP框架一样,在运行时完成织入。

3️⃣Spring AOP通知类型

  • Before advice : 在连接点之前运行的通知,但不能阻止执行流继续执行到连接点(除非它抛出异常)。
  • After returning advice : 在连接点正常完成后运行的通知(例如,如果方法返回而不引发异常)。
  • After throwing advice: 在方法通过抛出异常退出时运行的通知。
  • After (finally) advice: 不管连接点以何种方式退出(正常或异常返回),都要运行的通知。
  • Around advice: 围绕连接点(如方法调用)的通知。 这是最有力的建议。 Around通知可以在方法调用前后执行自定义行为。它还负责选择是继续到连接点,还是通过返回自己的返回值或抛出异常来简化被通知的方法执行。

二、Spring AOP能力和目标

  • Spring AOP是用纯Java实现的。 不需要特殊的编译过程。
  • Spring AOP目前只支持【方法执行连接点】(在Spring bean上的方法上执行通知)。
    如果需要通知字段访问和更新连接点,可以考虑使用AspectJ之类的语言。
  • Spring AOP的AOP方法不同于大多数其他AOP框架。 目的不是提供最完整的AOP实现(尽管Spring AOP很有能力)。
    相反,其目的是提供AOP实现和Spring IoC之间的紧密集成,以帮助解决企业应用程序中的常见问题。

Spring和AspectJ

  • Spring框架的AOP功能通常与Spring IoC容器一起使用。 切面是通过使用普通beanDifination语法配置的。
    使用Spring AOP不能轻松或有效地完成一些事情,比如通知非常细粒度的对象(通常是域对象)。 AspectJ是这种情况下的最佳选择。然而,我们的经验是,Spring AOP为企业Java应用程序中的大多数问题提供了一个很好的解决方案。
  • Spring AOP从不与AspectJ竞争,以提供全面的AOP解决方案。 我们相信基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是相互竞争的。 Spring无缝地将Spring AOP和IoC与AspectJ集成在一起,以支持在一致的基于Spring的应用程序体系结构中使用AOP。 这种集成不会影响Spring AOP API或AOP Alliance API, Spring AOP保持向后兼容。

三、AOP代理

  • Spring AOP默认为AOP代理使用标准的JDK动态代理, 这允许代理任何接口(或接口集)。
  • ​Spring AOP也可以使用CGLIB代理。 缺省情况下,如果业务对象没有实现接口,则使用CGLIB。
    由于编写接口是很好的实践,因此业务类通常实现一个或多个业务接口是可能的。

四、@AspectJ风格的支持

@AspectJ是将【切面】声明为带有注解的常规Java类的一种风格。 @AspectJ风格是由AspectJ项目作为AspectJ 5发行版的一部分引入的。 Spring与AspectJ 5有相同的注解, 但是,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。

1️⃣对于 @AspectJ的支持

要在Spring配置中使用@AspectJ注解,您需要启用Spring支持,以便基于@AspectJ注解配置Spring AOP,如果Spring确定一个bean被一个或多个切面通知,它将自动为该bean生成一个代理,以拦截方法调用,并确保通知在需要时运行。

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

????使用Java配置启用@AspectJ支持

要使用Java的【@Configuration】启用@AspectJ支持,请添加【@EnableAspectJAutoProxy】注解,如下面的示例所示:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

????使用XML配置启用@AspectJ支持,请使用<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: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">

    <!-- bean definitions here -->
	<aop:aspectj-autoproxy/>
</beans>

当然,我们需要引入aop的命名空间。

2️⃣声明一个切面

启用@AspectJ支持后,在应用程序上下文中定义的任何带有@AspectJ注解类的bean都会被Spring自动检测并用于配置Spring AOP。

两个示例中的第一个展示了应用程序上下文中的常规beanDifination,它指向一个具有“@Aspect”注解的bean类:

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

两个示例中的第二个展示了’ NotVeryUsefulAspect ‘类定义,它是用’ org.aspectj.lang.annotation '标注的。 方面的注解;

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

@Aspect
public class NotVeryUsefulAspect {

}

用“@Aspect”标注的类可以有方法和字段,与任何其他类一样。 它们还可以包含切入点、通知和引入(类型间)声明。

通过组件扫描自动检测切面你可以在Spring XML配置中通过“@Configuration”类中的“@Bean”方法将切面类注册为常规bean,或者让Spring通过类路径扫描自动检测它们——就像任何其他Spring管理的bean一样。 但是,请注意,“@Aspect”注解不足以实现类路径中的自动检测。 为了达到这个目的,您需要添加一个单独的【@Component】注解。

在Spring AOP中,切面本身不能成为来自其他通知的目标。 类上的“@Aspect”注解将其标记为一个切面类,因此会将其排除在自动代理之外。

3️⃣声明一个切入点

【切入点确定感兴趣的连接点】,从而使我们能够控制通知何时运行。

切入点声明由两部分组成:包含【名称和方法签名】,以及确定我们感兴趣的方法执行的【切入点表达式】。

怎么确定一个方法:public void com.ydlclass.service.impl.*(…)

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

????支持切入点指示器

Spring AOP支持以下在切入点表达式中使用的AspectJ切入点指示器(PCD):

  • execution:(常用)用于匹配方法执行的连接点,这是在使用Spring AOP时使用的主要切入点指示符。(匹配方法)
    最新最全面的Spring详解(四)——面向切面编程
  • within: 用于匹配指定类型内的方法执行。(匹配整个类)
    最新最全面的Spring详解(四)——面向切面编程
  • this: 用于匹配当前【AOP代理对象】类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能【包括引入接口】也进行类型匹配。(配置整个类)
    最新最全面的Spring详解(四)——面向切面编程
  • target: 用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就【不包括引入接口】也进行类型匹配。(配置整个类)

最新最全面的Spring详解(四)——面向切面编程

  • args: 限制匹配连接点(使用Spring AOP时的方法执行),其中参数是给定类型的实例。 (参数类型匹配)
    最新最全面的Spring详解(四)——面向切面编程
  • @target: 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解 。(类上的注解)
    最新最全面的Spring详解(四)——面向切面编程
  • @args: 用于匹配当前执行的方法传入的参数持有指定注解的执行。(参数上的注解)
    最新最全面的Spring详解(四)——面向切面编程
  • @within: 用于匹配所有持有指定注解类型内的方法。(类上的注解)
    最新最全面的Spring详解(四)——面向切面编程
  • @annotation: (常用)于匹配当前执行方法持有指定注解的方法。(方法上的注解)
    最新最全面的Spring详解(四)——面向切面编程
  • bean:使用“bean(Bean id或名字通配符)”匹配特定名称的Bean对象的执行方法;Spring ASP扩展的,在AspectJ中无相应概念。
    最新最全面的Spring详解(四)——面向切面编程

????切入点表达式运算

可以使用’ &&’ || ‘和’ ! '组合切入点表达式。 您还可以通过名称引用切入点表达式。 下面的例子展示了三个切入点表达式:

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

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

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

????共享公共切入点定义

在使用企业应用程序时,开发人员经常希望从几个切面引用应用程序的模块和特定的操作集。 我们建议定义一个【CommonPointcut】切面来捕获通用的切入点表达式。 这样一个方面典型地类似于以下示例:

package com.xyz.myapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class CommonPointcuts {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.myapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.myapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.service..*)")
    public void inServiceLayer() {}

    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.myapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
     * the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
    public void businessService() {}

    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

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

<aop:config>
    <aop:advisor
        pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

4️⃣声明通知

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

????(1)(Before advice)前置通知

你可以使用【@Before】注解在方面中声明before通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.CommonPointcuts.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() {
        // ...
    }
}

????(2)(After returning advice)返回通知

当匹配的方法执行正常返回时,返回通知运行。 你可以通过使用【@AfterReturning】注解声明它:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }
}

有时,您需要在通知主体中访问返回的实际值。 你可以使用’ @afterreturn '的形式绑定返回值以获得访问,如下例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}

????(3)(After throwing advice)抛出异常后通知

抛出通知后,当匹配的方法执行通过抛出异常退出时运行。 你可以通过使用【 @AfterThrowing】注解来声明它,如下面的例子所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }
}

通常,您如果希望通知仅在【抛出给定类型】的异常时运行,而且您还经常需要访问通知主体中抛出的异常。 你可以使用’ thrown ‘属性来限制匹配(如果需要,则使用’ Throwable '作为异常类型),并将抛出的异常绑定到一个advice参数。 下面的例子展示了如何做到这一点:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }
}

【throwing】属性中使用的【名称必须与通知方法中的参数名称】相对应。 当一个方法执行通过抛出异常而退出时,异常将作为相应的参数值传递给advice方法。

????(4)After (Finally) 最终通知

After (finally)通知在匹配的方法执行退出时运行。 它是通过使用【@After 】注解声明的。 After advice必须准备好处理正常和异常返回条件,它通常用于释放资源以及类似的目的。 下面的例子展示了如何使用after finally通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }
}

更多值得注意的地方,AspectJ中的’ @After '通知被定义为【after finally】,类似于try-catch语句中的finally块。 它将对任何结果,其中包括【正常返回】或【从连接点抛出异常】都会进行调用,而【 @ afterreturn】只适用于成功的正常返回。

????(5)Around通知

【Around advice】环绕匹配的方法执行。 它有机会在方法运行之前和之后进行工作,并确定方法何时、如何运行,甚至是否真正运行。 如果您需要在方法执行之前和之后以线程安全的方式共享状态(例如,启动和停止计时器),经常使用Around通知。 我们推荐,总是使用最弱的通知形式,以满足你的要求(也就是说,不要使用环绕通知,如果前置通知也可以完成需求)。

Around通知是通过使用【@Around】注解声明的。 advice方法的第一个参数必须是【ProceedingJoinPoint】类型。 在通知体中,在【ProceedingJoinPoint】上调用【proceed()】会导致底层方法运行。 【proceed】方法也可以传入【Object[] 】。 当方法执行时,数组中的值被用作方法执行的参数。

下面的例子展示了如何使用around advice:

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.CommonPointcuts.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // 我们可以在前边做一些工作,比如启动计时器
        
        // 这里是真正的方法调用的地方
        Object retVal = pjp.proceed();
        // 我们可以在后边做一些工作,比如停止计时器,搜集方法的执行时间
        return retVal;
    }
}

注意: around通知返回的值是【方法调用者看到的返回值】。 例如,一个简单的缓存切面可以从缓存返回一个值(如果它有一个值),如果没有,则调用’ proceed() '。 注意,【proceed方法】你可以只调用一次,也可以调用多次,也可以根本不去调用, 这都是可以的。

????(6)通知的参数

Spring提供了完整类型的通知,这意味着您可以在【通知签名】中声明【所需的参数】(就像我们前面在返回和抛出示例中看到的那样)。

访问当前 JoinPoint

任何通知方法都可以声明一个类型为【org.aspectj.lang.JoinPoint】的参数作为它的【第一个参数】(注意,around通知需要声明类型为’ ProceedingJoinPoint )的第一个参数,它是【oinPoint】的一个子类。 【JoinPoint】接口提供了许多有用的方法:

  • getArgs(): 返回方法参数。
  • getThis(): 返回代理对象。
  • getTarget(): 返回目标对象。
  • getSignature(): 返回被通知的方法的签名。
  • toString(): 打印被建议的方法的有用描述。
@Before("beforePointcut()")
private void beforeAdvice(JoinPoint jp) throws InvocationTargetException, IllegalAccessException {
    MethodSignature signature = (MethodSignature)jp.getSignature();
    // 能拿到方法,能不能拿到方法的注解
    Method method = signature.getMethod();
    // 调用方法的过程
    method.invoke(jp.getTarget(), jp.getArgs());

    System.out.println("this is before advice");
}

最新最全面的Spring详解(四)——面向切面编程
将参数传递给Advice

我们已经看到了如何绑定【返回值或异常值】。 要使参数值对通知主体可用,可以使用【args 】的绑定形式。 如果在args表达式中使用【参数名】代替类型名,则在【调用通知】时将传递相应值作为参数值。

举个例子应该能更清楚地说明这一点:

@Override
public String order(Integer money) {
    try {
        logger.info("这是order的方法");
        return "inner try";
    } finally {
        logger.info("finally");
        //return "finally";
    }
}

@Before("execution(* com.ydlclass.service.impl.OrderService.*(..)) && args(money,..)")
public void validateAccount(Integer money) {
    System.out.println("before--------" + money);
}

切入点表达式的’ args(account,…) '部分有两个目的

  • 首先,它限制只匹配哪些方法执行,其中方法接受至少一个参数,并且传递给该参数的参数是’ Account '的一个实例。
  • 其次,它通过’ Account ‘参数使通知可以使用实际的’ Account '对象。

另一种方式是【编写方法】声明一个切入点,该切入点在匹配连接点时“提供”‘Account’对象值,然后从通知中引用指定的切入点。 这看起来如下:

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

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

5️⃣引入Introduction

引入使切面能够声明被通知的对象【实现给定的接口】,也就是让代理对象实现新的接口。

@DeclareParents(value="com.ydlclass.service.impl.OrderService",defaultImpl= ActivityService.class)
public static IActivityService activityService;

要实现的接口由注解字段的类型决定。 @DeclareParents注解的【value】属性是一个AspectJ类型类。 任何与之匹配的类型的bean都将实现【UsageTracked】接口。 注意,在前面示例的before通知中,服务bean可以直接用作【UsageTracked】接口的实现。 如果以编程方式访问bean,您将编写以下代码:

IActivityService bean = ctx.getBean(IActivityService.class);
bean.sendGif();

搞过debug看到了,生成的代理实现了两个接口:
最新最全面的Spring详解(四)——面向切面编程

6️⃣Advice Ordering

  • 当多个通知都想在同一个连接点上运行时,Spring AOP遵循与AspectJ相同的优先规则来确定通知执行的顺序。优先级最高的通知在【进入时】首先运行【因此,给定两个before通知,优先级最高的将首先运行】。从连接点【退出】时,优先级最高的通知最后运行【因此,给定两个after通知,优先级最高的通知将第二运行】。
  • 当在不同切面定义的两个通知都需要在同一个连接点上运行时,除非另行指定,否则执行顺序是未定义的。 您可以通过指定优先级来控制执行顺序。在切面类中使用【Ordered】接口,或者用【@Order】注释它。 对于两个切面,从’Ordered.getOrder() '返回较低值的切面(或注释值)具有较高的优先级。

7️⃣AOP 的例子

业务代码的执行有时会由于【并发性问题】而失败(例如,死锁而导致的失败)。 如果再次尝试该操作,很可能在下一次尝试时成功。 对于适合在这种条件下重试的业务服务,我们希望进行透明地重试操作。 这是一个明显跨越service层中的多个服务的需求,因此是通过切面实现的理想需求。

因为我们想要重试操作,所以我们需要使用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配置注入的。

对应的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>

五、基于schema的AOP支持

如果您喜欢基于xml的格式,Spring还提供了使用【aop命名空间】标记定义切面的支持。 它支持与使用@AspectJ样式时完全相同的切入点表达式和通知类型。

要使用本节中描述的aop命名空间标记,您需要导入’ spring-aop '模块。

在Spring配置中,所有【切面和通知】元素都必须放在一个<aop:config> 元素中(在应用程序上下文配置中可以有多个<aop:config> 元素)。 一个<aop:config> 元素可以包含切入点、通知和切面元素(注意这些元素必须按照这个顺序声明)。

????配置切面,切点表达式,通知的方法如下

<?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: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">

</beans>
<?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: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">

<!--    <aop:aspectj-autoproxy/>-->

    <aop:config>
        <aop:aspect ref="aop">
            <aop:pointcut id="point" expression="execution(* com.ydlclass..*(..))"/>
            <aop:before method="beforeAdvice" pointcut="execution(* com.ydlclass..*(..)) and args(money,..)"/>
            <aop:after method="afterAdvice" pointcut-ref="point"/>
            <aop:after-returning method="afterReturningAdvice" pointcut-ref="point"/>
            <aop:after-throwing throwing="ex" method="afterThrowing" pointcut-ref="point"/>

        </aop:aspect>
    </aop:config>

    <bean id="aop" class="com.ydlclass.aspecj.MyAop"/>
    <bean id="orderService" class="com.ydlclass.service.impl.OrderService"/>
    <bean id="userService" class="com.ydlclass.service.impl.UserService"/>

</beans>

????Introduction

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

????AOP示例

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

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

对应的Spring配置如下:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>
        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>
    </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

六、AOP声明风格

​ 一旦您确定使用aop是实现给定需求的最佳方法,您如何决定是使用Spring AOP还是Aspect?是使用@AspectJ注解风格还是Spring XML风格?

​ 如果您选择使用Spring AOP,那么您可以选择【@AspectJ或XML】样式。

​ XML样式可能是现有Spring用户最熟悉的,并且它是由真正的【pojo支持】(侵入性很低)的。 当使用AOP作为配置企业服务的工具时,XML可能是一个很好的选择(一个很好的理由是您【是否认为切入点表达式】 是需要【独立更改】的一部分配置)。使用XML样式,可以从配置中更清楚地看出系统中存在哪些切面。

​ XML样式有两个缺点。 首先,它没有将它所处理的需求的实现完全封装在一个地方。 其次,与@AspectJ风格相比,XML风格在它能表达的内容上稍微受到一些限制,不可能在XML中声明的命名切入点进行组合。 例如,在@AspectJ风格中,你可以写如下内容:

@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方法的缺点是不能通过组合这些定义来定义“accountPropertyAccess”切入点。

@AspectJ还有一个优点,即@AspectJ切面可以被Spring AOP和AspectJ理解(从而被使用)。 因此,如果您以后决定需要AspectJ的功能来实现额外的需求,您可以轻松地迁移到经典的AspectJ当中。

总的来说,Spring团队更喜欢自定义切面的@AspectJ风格,而不是简单的企业服务配置。

七、以编程方式创建@AspectJ代理

除了通过使用<aop:config> <aop:aspectj-autoproxy>在配置中声明方面之外,还可以通过编程方式创建通知目标对象的代理。

代码如下,这只是一个小例子,用来看一下spring是怎么封装代理的:

public static void main(String[] args) {
    AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new OrderService());
    aspectJProxyFactory.addAspect(MyAspect.class);
    IOrderService proxy = (IOrderService)aspectJProxyFactory.getProxy();
    proxy.order(111);

}

后记

最新最全面的Spring详解(四)——面向切面编程

????Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
????算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~