Spring之AOP

一、基本概念

  • AOP,即面向切面编程,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率
  • 可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”
  • 即:不通过修改源代码的方式,在主干功能里面添加新功能
  • AOP的好处:
    • 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
    • 业务模块更简洁,只包含核心业务代码

二、底层原理

  • AOP底层使用动态代理,在有接口的情况下,使用JDK动态代理;在没有接口的情况下,使用CGLIB代理。具体的设计模式,详见代理模式

三、AOP术语

在这里插入图片描述

  • 横切关注点:从每个方法中抽取出来的同一类非核心业务。
  • 切面(Aspect):封装横切关注点信息的类,每个关注点体现为一个通知方法。
  • 通知(Advice):切面必须要完成的各个具体工作
  • 目标(Target):被通知的对象
  • 代理(Proxy):向目标对象应用通知之后创建的代理对象
  • 连接点(Joinpoint):横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。

在应用程序中可以使用横纵两个坐标来定位一个具体的连接点:
在这里插入图片描述

  • 切入点(pointcut): 定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
  • 简单来说,横切关注点就是被抽取出来的业务,切面就是封装业务方法的类,通知就是具体的方法。每个目标都有相对应的横切关注点,在程序中的具体位置就是连接点,而连接点可以选择是否切入通知,选择被切入通知的位置叫做切入点。

四、AOP依赖

  • spring框架一般都是基于AspectJ实现AOP操作
  • 相关依赖:

在这里插入图片描述

  • 或通过maven导入
 <dependencies>
<!--spring AOP的包-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<!--springIOC的包-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-expression</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
复制代码

五、基于注解

  • 在spring配置文件中,开启注解扫描、开启aop
    <?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">
    复制代码
    <!--  开启注解扫描  -->
    <context:component-scan base-package="com.du.spring5"/>
    复制代码
  • 开启生成代理对象
    <!-- 开启生成代理对象 -->
    <aop:aspectj-autoproxy/>
    复制代码
  • 通过注解@Component创建UserProxy对象,在增强类上面添加@Aspect,说明这是一个切面对象
    @Component
    @Aspect
    public class UserProxy2 { ... }
    复制代码
  • 配置不同类型的通知。在作为通知方法上面添加通知类型注解,使用切入点表达式配置。
  • AspectJ支持5种类型的通知注解:
    • @Before:前置通知,在方法执行之前执行
    • @After:后置通知,在方法执行之后执行
    • @AfterRunning:返回通知,在方法返回结果之后执行
    • @AfterThrowing:异常通知,在方法抛出异常之后执行
    • @Around:环绕通知,围绕着方法执行
  • 在切面上再添加一个@Order注解,可以设置多个切面的优先级,值越小优先级越大,越先运行
    @Component //通过注解配置
    @Aspect // 生成代理对象
    @Order(10)
    public class UserProxy {
    }
    复制代码

六、切入点表达式

  • 知道对哪个类里面的哪个方法进行增强
  • 语法结构:execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]) )

6.1 例子

  • com.du.spring5.bean.User类里面的add进行增强:execution(* com.du.spring5.bean.User.add(..))
  • com.du.spring5.bean.User类里面的所有方法进行增强:execution(* com.du.spring5.bean.User.*(..))
  • com.du.spring5.bean包内的所有类的所有方法进行增强:execution(* com.du.spring5.bean.*.*(..))
  • ..匹配任意数量、任意类型的参数

6.2 例子二

  • 在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
  • 任意类中第一个参数为int类型的add方法或sub方法:execution (* *.add(int,..)) || execution(* *.sub(int,..))

七、通知(Advice)

  • 在具体的连接点上要执行的操作
  • 一个切面可以包括一个或者多个通知。
  • 通知所使用的注解的值往往是切入点表达式。

7.1 前置通知

  • 在方法执行之前执行的通知
  • 使用@Before注解

7.2 后置通知

  • 后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的之后。可以类比成finally,无论是否正常结束,都会执行。
  • 使用@After注解

7.3 返回通知

  • 只有正常结束了,才会执行
  • 使用@AfterReturning注解
  • 若想获取方法执行完之后的返回结果,可以在参数列表中添加一个接收result的参数,示例如下:
@AfterReturning(value = "execution(* com.du.spring5.bean.*.*(..))", returning = "result")
  public void afterReturning(Object result) {
    System.out.println("方法执行结束,返回值为" + result);
  }
复制代码

returning 指定 返回值赋给哪个参数,将参数名 result 赋给它

7.4 异常通知

  • 只在连接点抛出异常时才执行异常通知
  • 使用@AfterThrowing注解
  • 若想捕获异常的信息,就可以像上面一样,在入参中添加一个接受Exception的参数,并在注解中说明
  @AfterThrowing(value = "execution(* com.du.spring5.bean.*.*(..))", throwing = "exception")
  public void afterThrowing(Exception exception) {
    System.out.println("发生了" + exception + "异常");
  }
复制代码

7.5 JoinPoint

  • 若想在通知中接收目标方法的信息,就可以在参数列表中添加一个JoinPoint类型的参数,Spring 会自动将该对象注入到方法中,无需在注解中说明。
  @Before(value = "execution(* com.du.spring5.bean.*.*(..))")
  public void before(JoinPoint joinPoint) {
    // 获取方法的所有输入参数
    Object[] args = joinPoint.getArgs();
    // 获取签名
    Signature signature = joinPoint.getSignature();
    System.out.println("正在执行[" + signature.getName() + "]方法, 参数为" + Arrays.toString(args));
  }
复制代码

更多细节可参考JoinPoint 接口

7.6 @Pointcut

  • 为了避免切入点表达式重复写,可以通过注解@Pointcut,统一配置,统一使用。
      @Pointcut("execution(* com.du.spring5.bean.*.*(..))")
      public void pointcut() {
      }
      
      @Before(value = "pointcut()")
      public void before(JoinPoint joinPoint) {
      }
    复制代码

7.7 环绕通知

  • 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
  • 很类似于反射的方法,相当于在参数中接收一个包含需要执行的方法及其参数的对象ProceedingJoinPoint,然后通过反射的方法执行该方法。而在执行方法的周围,可以通过添加一些try-catch-finally,实现与上面四种通知类型的效果。
      @Around(value="pointcut()")
      public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
          System.out.println("【环绕前置通知】");
          
          Object[] args = joinPoint.getArgs(); // 获取参数
          Object res = joinPoint.proceed(args);  // 执行方法,获取结果
          
          System.out.println("【环绕返回通知】,结果为" + res);
          return res;
        } catch (Throwable throwable) {
          System.out.println("【环绕异常通知】");
          throw new RuntimeException(throwable);
        } finally {
          System.out.println("【环绕后置通知】");
        }
      }
    复制代码

    记得将接收到的结果返回出去,否则之前注解的@AfterReturning的方法、真正调用方法的地方,没办法获取到执行的结果。

    拦截到的Exception,记得也抛出去,否则其他地方也接收不到。

7.8 执行顺序

  • 当前版本是5.2.10,执行顺序如下
  • 无异常:@Before -> 真正的方法 -> @AfterReturning -> @After
  • 存在异常:@Before -> 真正的方法 -> @AfterThrowing -> @After
  • 包含上述的环绕通知之后:环绕前置通知 -> @Before -> 真正的方法 -> @AfterReturning -> @After -> 环绕返回通知 -> 环绕后置通知

八、完全注解开发

  • 不使用xml文件,直接使用一个config类
@Configuration // 表示这是个配置类
@ComponentScan(basePackages = {"com.du.spring5"})  // 开启组件扫描
@EnableAspectJAutoProxy(proxyTargetClass = true)  // 开启aop
public class ConfigAop {
}
复制代码
  • 调用
  @Test
  public void test3() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigAop.class);  // 配置类
    User2 user2 = context.getBean("user2", User2.class);
    user2.add();
  }
复制代码

九、基于xml文件

  • 除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
  • 正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
  • 比较:较重要的配置,通过xml配置;其他配置通过注解配置。

9.1 配置案例

  • xml文件,名称空间添加aop相关内容
    <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 http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
    >
    复制代码
  • 在xml上配置aop
    • 在bean配置文件中,所有的Spring AOP配置都必须定义在<aop:config>元素内部。对于每个切面而言,都要创建一个<aop:aspect>元素来为具体的切面实现引用后端bean实例。
    • 切面bean必须有一个标识符,供<aop:aspect>元素引用。
    <aop:config>
        <!-- 设置切入点 -->
        <aop:pointcut id="user_add_pointcut" expression="execution(* com.du.spring5.bean.User.add(..))"/>
        <!--设置切面-->
        <aop:aspect ref="userProxy">
            <!-- 设置切面的位置,以及使用切面插入的方法,切入点-->
            <!-- 标签头说明了切入的位置,method为指定的方法,pointcut-ref为指定的切入点 -->
            <aop:before method="before" pointcut-ref="user_add_pointcut"/>
            <aop:after method="after" pointcut-ref="user_add_pointcut"/>
            <aop:after-returning method="afterReturn" pointcut-ref="user_add_pointcut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="user_add_pointcut"/>
            <aop:around method="around" pointcut-ref="user_add_pointcut"/>
        </aop:aspect>
    </aop:config>
    复制代码
  • 原始类
    public class User {
      private final static Logger logger = Logger.getLogger(User.class);
      public void add() {
        logger.info("add.......");
      }
    }
    复制代码
  • 代理类
    public class UserProxy {
      private final static Logger logger = Logger.getLogger(UserProxy.class);
      /**
       * 前置通知
       */
      public void before() {
        logger.info("before");
      }
      /**
       * 最终通知
       */
      public void after() {
        logger.info("after");
      }
      /**
       * 后置通知(返回通知)
       */
      public void afterReturn() {
        logger.info("afterReturn");
      }
      /**
       * 异常通知
       */
      public void afterThrowing() {
        logger.info("afterThrowing");
      }
      /**
       * 环绕通知
       * @param proceedingJoinPoint
       * @throws Throwable
       */
      public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        logger.info("around before");
        proceedingJoinPoint.proceed();
        logger.info("around ater");
      }
    }
    复制代码

9.2 配置

  • 所有的Spring AOP配置都必须定义在<aop:config>元素内部。在这个config里面,把需要的切面类在里面通过<aop:aspect>配置。

9.3 切面

  • <aop:aspect>切面里面需要指定切面类是哪个(已经被注入到IOC容器内了),通过ref 参数指定对象id;里面还可以指定优先级order

9.3 切入点

  • 切入点使用<aop:pointcut>元素声明
  • 这个标签可以放进<aop:config>里面,给所有切面使用,也可以放进<aop:aspect>只给当前切面使用。
  • 该标签需要通过expression指定切入点表达式,设置id以供别的标签调用。

9.4 声明通知

  • before通知可以通过<aop:before>标签指定,其中,method属性指定方法,pointcut-ref指定切入点。
  • 具体配置
<aop:config>
<!-- 设置切入点 -->
<aop:pointcut id="user_add_pointcut" expression="execution(* com.du.spring5.bean.User.add(..))"/>
<!--设置切面-->
<aop:aspect ref="userProxy">
    <!-- 设置切面的位置,以及使用切面插入的方法,切入点-->
    <!-- method为切入的位置,pointcut-ref为代理类的指定的方法 -->
    <aop:before method="before" pointcut-ref="user_add_pointcut"/>
    <aop:after method="after" pointcut-ref="user_add_pointcut"/>
    <aop:after-returning method="afterReturn" pointcut-ref="user_add_pointcut"/>
    <aop:after-throwing method="afterThrowing" pointcut-ref="user_add_pointcut"/>
    <aop:around method="around" pointcut-ref="user_add_pointcut"/>
</aop:aspect>
</aop:config>
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享