Spring AOP (1)

在软件开发中,散布于应用中多处的功能被称为横切关注点(crosscutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。

Craig WallsSpring 实战(第4版)

AOP 概述

AOP,Aspect Oriented Programming,面向切面编程

  • AOP 是一种新的方法论,是对传统 OOP 的补充。
  • AOP 操作的主要对象是切面(aspect),切面是模块化后的横切关注点。
  • 在应用 AOP 编程时,仍然需要定义公共功能,但可以明确地定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。
  • AOP 的好处:
    • 关注点集中,代码不分散,便于维护和升级
    • 业务模块更简洁,只包含核心业务代码,降低耦合
  • 常见应用场景:日志、声明式事务、安全和缓存等

AOP 术语

AOP术语

通知 Advice

切面的工作称为通知,它定义了切面是什么(what)以及何时(when)使用。

Spring AOP 中的五种通知:

  • 前置通知(Before):在目标方法被调用之前调用通知。
  • 后置通知(After):在目标方法完成之后调用通知。无论是正常返回还是抛出异常,后置通知都会执行。
  • 返回通知(After-Returning):在目标方法成功执行之后调用通知。
  • 异常通知(After-Throwing):在目标方法抛出异常后调用通知。
  • 环绕通知(Around):在目标方法调用之前和调用之后执行自定义的行为。

连接点 Join point

可以应用切面的点称为连接点,可以是调用某个方法时、某个方法异常时等等。

例如在上面的图片中,所有圆点都是连接点。

切点 Pointcut

真正应用切面的点称为切点,切点定义了在何处(where)应用切面。

并不需要在每个连接点都应用切面,只需要根据实际需求选择连接点进行切入,这些连接点就是切点。

切面 Aspect

切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

织入 Weaving

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

可以在目标对象生命周期的不同阶段进行织入:

  • 编译期:切面在目标类编译时被织入。
  • 类加载期:切面在目标类加载到 JVM 时被织入。
  • 运行期:切面在应用运行的某个时刻被织入,AOP 容器为目标容器动态地创建一个代理对象。Spring AOP 采用的就是这种方式

Spring AOP 运行的大致流程

底层原理

Spring AOP 构建在动态代理基础之上,因此 Spring 对 AOP 的支持局限于方法拦截

Spring AOP 底层使用动态代理。有两种情况:

  • 有接口,使用 JDK 动态代理

    创建接口实现类代理对象,增强类的方法

    JDK动态代理

  • 没有接口,使用 CGLIB 动态代理

    创建子类的代理对象,增强类的方法

    CGLIB动态代理

JDK 动态代理

使用 JDK 动态代理,使用 java.lang.reflect.Proxy 类里面的方法创建代理对象

newProxyInstance方法

newProxyInstance方法参数

代码演示:

  1. 创建接口,定义方法

    1
    2
    3
    4
    5
    public interface UserDao {
    int add(int a, int b);

    String update(String id);
    }
  2. 创建接口实现类,实现方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class UserDaoImpl implements UserDao {
    @Override
    public int add(int a, int b) {
    System.out.println("add 方法执行");
    return a + b;
    }

    @Override
    public String update(String id) {
    System.out.println("update 方法执行");
    return id;
    }
    }
  3. 使用 java.lang.reflect.Proxy 类创建接口代理对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class JdkProxy {
    public static void main(String[] args) {
    // 创建接口实现类代理对象
    Class[] interfaces = {UserDao.class};
    UserDao userDao = new UserDaoImpl(); // 被代理对象
    UserDao proxyInstance =
    (UserDao) Proxy.newProxyInstance(JdkProxy.class.getClassLoader(),
    interfaces, new UserDaoProxy(userDao));
    System.out.println(proxyInstance.add(2, 3));
    System.out.println(proxyInstance.update("abc"));
    }
    }

    // 创建代理对象代码
    class UserDaoProxy implements InvocationHandler {
    private Object obj;

    // 把被代理对象传递过来
    public UserDaoProxy(Object obj) {
    this.obj = obj;
    }

    // 增强的逻辑
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 方法之前
    System.out.println(method.getName() + " 方法之前执行,参数:" + Arrays.toString(args));
    // 被增强的方法执行
    Object returnValue = method.invoke(obj, args);
    // 方法之后
    System.out.println(method.getName() + " 方法之后执行");
    return returnValue;
    }
    }
  4. 输出:

    1
    2
    3
    4
    5
    6
    7
    8
    add 方法之前执行,参数:[2, 3]
    add 方法执行
    add 方法之后执行
    5
    update 方法之前执行,参数:[abc]
    update 方法执行
    update 方法之后执行
    abc

上面代码中的 java.lang.reflect.InvocationHandler 接口及其 invoke 方法:

InvocationHandler接口

InvocationHandler.invoke方法.png

大致流程

Spring AOP 运行示意图

通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。如上图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标 bean。当代理拦截到方法调用时,在调用目标 bean 方法之前,会执行切面逻辑。


参考资料:

Craig Walls - Spring 实战(第 4 版)

曾梦想仗剑走天涯,后来没钱就没去