理解 AOP

AOP(Aspect Orient Programming),即面向切面编程。AOP 是一种编程思想,是 OOP(Object Oriented Programming)面向对象编程的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

以代码举例:

public interface Azh3ngInterface { void save(); }

public class Azh3ngService implements Azh3ngInterface {
    @Override
    public void doSave() {
        // do save...
    }
}

定义了一个接口和一个实现类,其中有一个方法 doSave() 目的是持久化一些数据。

现在期望在 doSave() 方法之前和之后打印日志,可以直接在代码里添加:

public class Azh3ngService implements Azh3ngInterface {
    @Override
    public void doSave() {
        System.out.println("doSave method starts in: " + new Date());
        // do save...
        System.out.println("doSave method ended in: " + new Date());
    }
}

当新增了一个 doUpdate() 方法,也需要在方法开始之前和之后记录日志,如果再在代码中添加类似的两行打印代码,明显代码就变得非常分散和冗余;
假设有多个方法都已经添加了这类代码,当需要修改打印格式时,需要到所有地方都修改。
由此可以看出,对于一些较为通用的操作,如果直接在代码中添加,会使代码耦合度变高,可维护性和可扩展性变低。

尝试进一步思考,对于打印日志、事务管理、安全检查、权限校验等通用操作,需要作用在一段业务代码上时,如果只需要关注在哪里做什么,代码执行时会自动在业务逻辑代码之前和之后执行通用操作,会给开发和维护带来极大的便利。 比如,希望在 Azh3ngServicedoSave()doUpdate() 方法之前和之后打印日志和开启/提交事务,此时:

  • 在哪里Azh3ngServicedoSave()doUpdate() 方法之前和之后
  • 做什么:打印日志

如果只需要在某个地方指定【Azh3ngServicedoSave()doUpdate() 方法】,然后编写【方法之前和之后打印日志】的代码,当被指定的方法调用时,会自动打印日志的话,便可以算作面向切面编程了,例如:

@Aspect
@Component
public class Azh3ngAspect {
    // **在哪里**:在执行 Azh3ngService 的 方法名以“do”开头 的所有方法前后执行:
    @Around("execution(public * com.azh3ng.service.Azh3ngService.do*(..))")
    public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
        // **做什么**:打印日志
        System.out.println("[Around] start " + pjp.getSignature());
        Object retVal = pjp.proceed();
        System.out.println("[Around] done " + pjp.getSignature());
        return retVal;
    }
}
public class TestAspect {
    public static void main(String[] args) {
        Azh3ngInterface azh3ngServiceProxy = BeanFactory.getBean(Azh3ngService.class);
        azh3ngServiceProxy.doSave(); // 自动打印日志
    }
}

由于目前两个类没有直接的关系,所以需要一些技术完成这个功能。
在了解技术之前,先了解一下相关术语。

相关概念

参考:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-introduction-defn

Aspect

表示切面,对应示例中的 Azh3ngAspect 类(被 @Aspect 注解的类)就是切面
切面中可以定义 Pointcut、Advice 等等

Join point

表示连接点,比如一个方法的执行、或者一个异常的处理,如 doSave()doUpdate() 方法;
Spring AOP 中,一个连接点通常表示被代理的目标方法。

Advice

直译为通知,或者可以理解为增强,表示在一个特定连接点上所采取的动作,对应示例中的 doLogging() 方法,【在方法前后打印日志】就是一种 Advice;
Advice 分为不同的类型, 包括 “around”, “before” 和 “after” 等。
Advice 可以简单理解为:只是一段执行逻辑
由于做什么可以不止做一件事,可能有多个逻辑需要执行,因此在很多 AOP 框架中,包括 Spring,会用拦截器(Interceptor)来实现 Advice,并且在连接点周围维护一个 Interceptor 链,顺序执行定义的多个逻辑

Pointcut

直译为切点,用来匹配一个或多个连接点,对应示例中的 "execution(public * com.azh3ng.service.Azh3ngService.do*(..))" 表达式 Advice 与切点表达式是关联在一起的,Advice 将会执行在和切点表达式所匹配的连接点上

Target object

目标对象,指被切面指定的对象。对应示例中的 Azh3ngService 类 在 Spring AOP 中根据被指定的对象生成代理对象。

AOP proxy

AOP 代理,表示被各类 AOP 框架生成的、用来实现 AOP 的类,对应示例中的 azh3ngServiceProxy 对象 在 Spring 中,使用 JDK 动态代理和 CGLIB 代理技术生成代理类

Weaving

直译为织入,表示创建代理对象的动作,对应示例中的 BeanFactory.getBean(Azh3ngService.class)

  • AspectJ 的织入动作发生在编译时期
  • Spring AOP 的织入动作发生在运行时

Introduction

给某个类型声明额外的方法或字段。例如在 Spring AOP 中可以使用 @DeclareParents 注解给指定的类添加接口,并指定一个默认实现,实际开发中使用较少。

相关实现技术

在 Java 中场景的实现技术有 AspectJ 和 Spring AOP。

AspectJ

AspectJ 是在编译时对字节码进行修改,直接对类的字节码进行增强,可以理解为在编译时就解析 @Around 等注解,得到代理逻辑后对被代理的类中的字节码进行增强,所以如果想用 AspectJ 技术生成代理对象,需要用单独的 AspectJ 编译器的,实际项目中很少这么用。通常开发者使用 Spring 框架,在 Spring 启动时解析处理 @Around 等注解,然后利用动态代理机制生成代理对象。

AspectJ注解

AspectJ 定义了一些 Pointcut 相关的注解:

  • Pointcut
  • Around
  • Before
  • After
  • AfterReturning
  • AfterThrowing

这些注解在 Spring 中也有使用

Spring AOP

Spring AOP