AOP 面向切面编程

面向切面编程(Aspect Oriented Programming)是一种编程的思维模式,已经在软件工程/设计模式章节中有所介绍,关于什么是AOP这里就不多说了。

Spring框架中,自带了SpringAOP,此外还集成了AspectJ框架实现AOP,后者因为使用非常方便,性能也较好,因此使用较多。

AOP的一些术语

通知 Advice:切面的工作被称为通知。通俗的说,就是调用目标方法前或后,执行的代码(切面)。

  • 前置通知:目标方法调用前调用通知功能
  • 后置通知:目标方法调用后调用通知功能
  • 返回通知:目标方法成功执行之后调用通知功能
  • 异常通知:目标方法抛出异常时通知功能
  • 环绕通知:目标方法调用前和调用后执行通知功能

连接点 Join Point:应用过程中,能够插入切面的点。

切点 Poincut:匹配通知所要织入的一个或多个连接点。

切面 Aspect:通知和切点的结合,即:在何时和何处完成其功能。

引入 Introduction:引入,允许我们向现有的类添加新方法或属性。

织入 Weaving:把切面应用到目标对象并创建新的代理对象的过程。

可以进行织入的阶段:

  • 编译期织入:例如AspectJ的织入编译器
  • 类加载期织入:需要特殊的类加载器,如AspectJ 5的LTW
  • 运行时织入:使用Java语言提供的动态代理实现,SpringAOP就采用这种方式

总的来说,Spring的AOP是基于动态代理实现的,被代理的Bean载入类容器时,代理类就会被创建,同时,基于动态代理也导致了Spring只支持方法级别的连接点。

引入Maven依赖

启动AOP功能需要引入spring-aop包和AspectJ框架相关依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.20</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.9.1</version>
</dependency>

切入点表达式

切点需要使用AspectJ的切入点表达式来定义,实际上由于Spring只支持方法级别的AOP,因此我们使用的只是AspectJ切入点表达式的一个子集。

有关如何编写切入点表达式,其实就是方法签名中间包含若干通配符,我们直接上一张图:

例子:

execution(public * * (..))    任意公共方法
execution(* set* (..))    任意以set开通的方法
execution(* com.gacfox.demo.MyService.* (..))    com.gacfox.demo.MyService下所有方法
execution(* com.gacfox.demo.*.* (..))    com.gacfox.demo包下所有类的方法
execution(* com.gacfox.demo..*.* (..))    com.gacfox.demo包及子包下所有类的方法

使用注解创建切面

可用的通知类型注解:

  • @After 目标方法返回或抛出异常调用
  • @AfterRunning 目标方法返回后调用
  • @AfterThrowing 目标方法抛出异常后调用
  • @Around 环绕通知
  • @Before 目标方法调用之前执行

在使用AOP注解前,applicationContext.xml中需要开启包扫描和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.gacfox.demo" />
    <aop:aspectj-autoproxy />

</beans>

下面是一个使用AOP切面的例子:

MyAspect.java

package com.gacfox.demo;

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

@Aspect
@Component
public class MyAspect {
    @Before("execution(* com.gacfox.demo.UtilA.doA(..))")
    public void before() {
        System.out.println("before");
    }

    @After("execution(* com.gacfox.demo.UtilA.doA(..))")
    public void after() {
        System.out.println("after");
    }
}

UtilA.java

package com.gacfox.demo;

import org.springframework.stereotype.Component;

@Component
public class UtilA {
    public void doA() {
        System.out.println("utilA");
    }
}

当我们调用UtilA.doA()方法时,会得到如下输出:

before
utilA
after

注意,切面类也必须使用@Component托管在类容器中,不然切面是不起作用的。

改进切面的写法 @Pointcut

上面切面中,我们写了两次同样的切入点表达式,这个实际上可以用一个@Poincut注解进行简化。

MyAspect.java

package com.gacfox.demo;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {
    @Pointcut("execution(* com.gacfox.demo.UtilA.doA(..))")
    public void doA() {
    }

    @Before("doA()")
    public void before() {
        System.out.println("before");
    }

    @After("doA()")
    public void after() {
        System.out.println("after");
    }
}

环绕通知

我们看一个环绕通知的例子:

package com.gacfox.demo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {
    @Pointcut("execution(* com.gacfox.demo.UtilA.doA(..))")
    public void doA() {
    }

    @Around("doA()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) {
        try {
            System.out.println("before");
            Object retValue = proceedingJoinPoint.proceed();
            System.out.println("after");
            return retValue;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            throw new RuntimeException();
        }
    }
}

环绕通知切面方法,有一个参数ProceedingJoinPoint,实际上proceedingJoinPoint.proceed()就是目标方法的调用。

处理目标方法的参数

上面的例子中,我们的目标方法没有参数。实际上,切面也可以对参数进行拦截:

UtilA.java

package com.gacfox.demo;

import org.springframework.stereotype.Component;

@Component
public class UtilA {
    public void doA(int i) {
        System.out.println("utilA:" + i);
    }
}

MyAspect.java

package com.gacfox.demo;

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

@Aspect
@Component
public class MyAspect
{
    @Pointcut("execution(* com.gacfox.demo.UtilA.doA(int)) && args(i)")
    public void doA(int i) {}

    @Before(value = "doA(i)", argNames = "i")
    public void before(int i)
    {
        System.out.println("aspect:" + i);
        System.out.println("before");
    }
}

输出结果:

aspect:1
before
utilA:1
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。