Spring AOP

AOP

AOP(Aspect Oriented Programming,面向切面编程) 最早是 AOP 联盟的组织提出的,指定的一套规范,spring 将 AOP 的思想引入框架之中,通过预编译方式和运行期间动态代理实现程序的统一维护的一种技术。

OOP 和 AOP 的区别:

  • 面向对象编程(OOP),是针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

  • 面向切面编程(AOP),则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

概念

  • 连接点(Jointpoint):表示需要在程序中插入横切关注点的扩展点。

    连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring 只支持方法执行连接点。在 AOP 中表示为在哪里干。

  • 切入点(Pointcut): 通知功能被应用的范围。

    Spring 支持 perl5 正则表达式和 AspectJ 切入点模式,Spring 默认使用 AspectJ 语法。在 AOP 中表示为在哪里干的集合。

  • 通知(Advice):在连接点上执行的行为,通知提供了在 AOP 中,需要在切入点所选择的连接点处进行扩展现有行为的手段。

    通知的方式有五种:

    • 前置通知(Before advice)@Before,在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。

    • 后置通知(After returning advice)@AfterReturning,在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

    • 异常通知(After throwing advice)@AfterThrowing,在方法抛出异常退出时执行的通知。

    • 最终通知(After (finally) advice)@After,当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

    • 环绕通知(Around Advice)@Around,包围一个连接点的通知,如方法调用。

      这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

    在 Spring 中通过代理模式实现 AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知。在 AOP 中表示为干什么;

  • 方面/切面(Aspect):横切关注点的模块化,比如上边提到的日志组件。可以认为是通知、引入和切入点的组合;

    在 Spring 中可以使用 Schema 和 @AspectJ 方式进行组织实现。在AOP中表示为在哪干和干什么集合。

  • 引入(inter-type declaration):也称为内部类型声明,为已有的类添加额外新的字段或方法。

    Spring 允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象)。 在 AOP 中表示为干什么(引入什么);

  • 目标对象(Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象。

    由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为对谁干;

  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。

    这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。在AOP中表示为怎么实现的;

  • AOP代理(AOP Proxy):AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。

    在 Spring 中,AOP 代理可以用 JDK 动态代理或 CGLIB 代理实现,而通过拦截器模型应用切面。在 AOP 中表示为怎么实现的一种典型方式。

它们的关系,如下图所示:

image

Spring AOP 和 AspectJ

AspectJ

AspectJ:是一个面向切面的框架,它扩展了 Java 语言。AspectJ 定义了 AOP 语法,它有一个专门的编译器用来生成遵守 Java 字节编码规范的 Class 文件。

AspectJ 是最首创的 AOP 技术,用来提供全面的 AOP 方案。

织入方式

AspectJ 使用了三种不同类型的织入方式:

  • 编译期织入(Compile-time weaving):编译器将切面和应用的源代码编译在一个字节码文件中。

  • 编译后织入(Post-compile weaving):也称为二进制织入,将已有的字节码文件与切面编制在一起。

  • 加载时织入(Load-time weaving):与编译后织入一样,只是织入时间会推迟到类加载到jvm时。

Spring AOP

Spring AOP 默认使用标准 JDK 动态代理作为 AOP 代理。这使得任何接口(或一组接口)都可以被代理。Spring AOP 还可以使用 CGLIB 代理,这对于代理类而不是接口是必要的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。

虽然,Spring AOP 使用了 AspectJ 5 的注释,不过,Spring AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

织入方式

Spring AOP 使用运行时织入(runtime weaving)。在运行时织入,是使用目标对象的代理对象织入的。

主要使用了两种技术:JDK 动态代理CGLIB 动态代理

  • 对于接口,使用的是 JDK 动态代理;

  • 对于继承,使用的是 CGLIB 动态代理。

image

对比

因为织入方式的区别,两者所支持的 Joinpoint 也是不同的。像 final 的方法和静态方法,无法通过动态代理来改变,所以 Spring AOP 无法支持。但 AspectJ 是直接在运行前织入实际的代码,所以功能会强大很多。

Joinpoint Spring AOP Supported AspectJ Supported
Method Call N Y
Method Execution Y Y
Constructor Call N Y
Constructor Execution N Y
Static initializer execution N Y
Object initialization N Y
Field reference N Y
Field assignment N Y
Handler execution N Y
Advice execution N Y

小结

Spring AOP是用纯 Java 实现的,不需要特殊的编译过程。Spring AOP 不需要控制类加载器层次结构,因此适合在 servlet 容器或应用程序服务器中使用。编译织入会比较运行时织入快很多,Spring AOP 是使用代理模式在运行时才创建对应的代理类,所以,效率没有 AspectJ 高。

Spring AOP 处理 AOP 的方法与大多数其他 AOP 框架不同。目的不是提供最完整的 AOP 实现。相反,其目的是提供 AOP 实现和 Spring IoC 之间的紧密集成,以帮助解决企业应用程序中的常见问题。

应用

Spring 中可以通过 XML 配置 AOP,也可以通过注解的方式配置 AOP,可以根据自己的喜好配置,这里,我们主要介绍通过注解的方式配置 AOP。

基于注解配置 AOP

@AspectJ 指的是一种将切面声明为带有注释的常规 Java 类的风格。@AspectJ 样式是由 AspectJ 项目作为 AspectJ 5 版本的一部分引入的。Spring 注解与 AspectJ 5 相同的注解,使用 AspectJ 提供的库进行切入点解析和匹配。不过,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

启用 @AspectJ 支持

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

这一步,也可以通过 XML 的配置启用 @AspectJ 支持,只需要在 XML 中添加如下内容即可:

<aop:aspectj-autoproxy/>

声明一个切面

任何带有 @Aspect 注解的切面类的 bean 都会被 Spring 自动检测到,并用于配置 Spring AOP。

注意,切面类必须被作为一个 Bean 被自动注入到 IoC 容器中,因此,切面类必须带有 @Bean 或者 @Component 这种能被 Spring 自动检测到并对其自动注入的注解。

这里,我们声明了一个切面类 WebLogAspect,源码如下:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Slf4j
@Component
public class WebLogAspect {

    @Pointcut("execution(public * com.athena.service.*.*.*(..))")
    public void webLog() {
    }

    @Before(value = "webLog()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("@Before: setup user profile.");
    }

    @After(value = "webLog()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("@After: setup user profile.");
    }

    @AfterReturning(value = "webLog()", returning = "ret")
    public void doAfterReturning(Object ret) {
        log.info("@AfterReturning:" + ret.toString());
    }

    @AfterThrowing(pointcut = "webLog()", throwing = "ex")
    public void doAfterThrowing(IllegalArgumentException ex){
        log.info("@AfterThrowing: there has been an exception: " + ex.toString());
    }

    @Around(value = "webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        log.info("@Around: before around...");
        log.info("parameters : " + Arrays.toString(proceedingJoinPoint.getArgs()));
        log.info("target : " + proceedingJoinPoint.getTarget());
        log.info("signature : " + proceedingJoinPoint.getSignature());
        log.info("this : " + proceedingJoinPoint.getThis());
        log.info("advised : " + proceedingJoinPoint);
        Object result = proceedingJoinPoint.proceed();
        log.info("@Around: after around...");
        return result;
    }
}

切入点支持的指示符

Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示符 (Pointcut Desinator,PCD):

  • execution:用于匹配方法执行的连接点。这是使用 Spring AOP 时使用的主要切入点指示符。

  • within:限制匹配某些类型中的连接点(使用 Spring AOP 时执行在匹配类型中声明的方法)。

  • this:限制对连接点(使用 Spring AOP 时方法的执行)的匹配,其中 bean 引用(Spring AOP 代理)是给定类型的实例。

  • target:限制对连接点(使用 Spring AOP 时方法的执行)的匹配,其中目标对象(被代理的应用程序对象)是给定类型的实例。

  • args:限制对连接点(使用 Spring AOP 时方法的执行)的匹配,其中参数是给定类型的实例。

  • @target:限制匹配连接点(使用 Spring AOP 时的方法执行),其中执行对象的类具有给定类型的注释。

  • @args:限制对连接点(使用 Spring AOP 时方法的执行)的匹配,其中传递的实际参数的运行时类型具有给定类型的注释。

  • @within:限制匹配具有给定注释的类型内的连接点(使用 Spring AOP 时,执行具有给定注释的类型中声明的方法)。

  • @annotation:限制匹配连接点,其中连接点的主题(在 Spring AOP 中运行的方法)具有给定的注释。

由于 Spring 的 AOP 框架基于代理的性质,根据定义,目标对象内的调用不会被拦截。对于JDK代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,代理上的公共和受保护方法调用将被拦截。但是,通过代理进行的常见交互应始终通过公共签名来设计。

声明一个目标对象

这里,我们用一个简单的目标对象来作为示例:

注意,目标对象的全路径限定符,必须与切点声明时,指示的路径匹配。

// com.athena.service.user.UserServiceImpl
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    public User getUserById(String userId) {
        log.info("start to query user");
        return new User(userId);
    }
}

通过 Controllor 触发该 getUserById 方法的调用时,就会产生如下打印:

2023-10-19T10:11:59.067+08:00  INFO 15976 : @Around: before around...
2023-10-19T10:11:59.067+08:00  INFO 15976 : parameters : [2]
2023-10-19T10:11:59.068+08:00  INFO 15976 : target : com.athena.service.user.UserServiceImpl@3bc60e1b
2023-10-19T10:11:59.068+08:00  INFO 15976 : signature : User com.athena.service.user.UserServiceImpl.getUserById(String)
2023-10-19T10:11:59.068+08:00  INFO 15976 : this : com.athena.service.user.UserServiceImpl@3bc60e1b
2023-10-19T10:11:59.068+08:00  INFO 15976 : advised : execution(User com.athena.service.user.UserServiceImpl.getUserById(String))
2023-10-19T10:11:59.068+08:00  INFO 15976 : @Before: setup user profile.
2023-10-19T10:11:59.068+08:00  INFO 15976 : start to query user
2023-10-19T10:11:59.069+08:00  INFO 15976 : @AfterReturning:User(userName=defaultName, userId=2)
2023-10-19T10:11:59.069+08:00  INFO 15976 : @After: setup user profile.
2023-10-19T10:11:59.069+08:00  INFO 15976 : @Around: after around...

可以看出,环绕通知最先被调用,功能也最强大。


参考:

posted @ 2023-10-19 10:16  LARRY1024  阅读(43)  评论(0编辑  收藏  举报