Spring AOP

一、AOP术语

核心概念

名称 说明
切面(Aspect) 切面是通知和切点的结合。 切面是一组定义了横切关注点的类。在 Spring AOP 中,切面通常通过使用 @Aspect 注解的类来实现。
连接点(Joinpoint) 连接点表示应用执行过程中能够插入切面的一个特定点,如方法的调用或异常的抛出。 在 Spring AOP 中,连接点总是方法的执行。
切入点(Pointcut) 定义了可以插入增强处理的连接点。 Spring AOP 允许通过正则表达式或特定的 API 来定义切入点。
通知(Advice) AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
织入(Weaving) 织入是将切面应用到目标对象并创建新的代理对象的过程。Spring AOP 在运行时动态地织入切面。
代理(Proxy) 代理是织入切面后生成的对象,它包装了目标对象,并在执行目标对象的方法前后应用通知。
目标对象(Target Object) 目标对象是被代理的对象,即包含连接点的业务逻辑组件。
引入(Introduction) 引入允许为现有的类添加新的方法、属性和接口,而不需要修改类本身。

Spring 通知分类

通知类型 说明
before(前置通知) 在目标方法执行之前执行的通知。不管目标方法是否执行,前置通知总是会被执行。
after(后置通知) 在目标方法执行之后执行的通知,无论方法执行是否成功,都会执行后置通知。
after-returning(返回后通知) 当目标方法成功执行并返回值后执行的通知。如果目标方法抛出异常,则不会执行返回通知。
after-throwing(抛出异常后通知) 当目标方法抛出异常时执行的通知。如果目标方法正常执行并返回,则不会执行异常通知。
around(环绕通知) 包围目标方法执行的通知,可以在目标方法执行前后添加逻辑,并且可以决定是否继续执行目标方法或替换返回值。

AOP 织入时期

织入时期 说明 适用场景
编译时织入 在源代码编译成字节码时进行织入。 在编译阶段,AspectJ 的编译器会处理注解,将切面织入到 Java 字节码中。这种方式需要使用 AspectJ 的特定编译器。
加载时织入 在字节码加载到 JVM 时进行织入。 在类加载到 JVM 时,通过自定义的类加载器动态地修改字节码,将切面织入。这同样需要 AspectJ 的支持
运行时织入 在应用程序运行时进行织入,是 Spring AOP 的主要工作方式。 适用于 Spring 管理的 Bean。 Spring AOP 在运行时使用代理机制来织入切面。如果目标对象实现了接口,Spring 使用 JDK 的 Proxy 类创建代理;如果没有实现接口,Spring 使用 CGLIB 库来创建代理。

注意:Spring AOP 主要支持运行时织入,而编译时织入和加载时织入通常与 AspectJ 框架一起使用,它们提供了更广泛的 AOP 功能,但不是 Spring AOP 的一部分。

二、AOP 常见应用场景

  • 日志记录
  • 事务管理
  • 权限验证
  • 性能监测
    ...

三、AOP 实现方式

AOP有两种实现方式:静态代理和动态代理。

静态代理:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。

缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。

动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。

四、Spring AOP动态代理

Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。

  1. JDK动态代理(生成的代理类实现了接口)。如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

    缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。

  2. CGLIB来动态代理(通过继承)。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。

    CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

    优点:目标类不需要实现特定的接口,更加灵活。

什么时候采用哪种动态代理?

如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
如果目标对象实现了接口,可以强制使用CGLIB实现AOP
如果目标对象没有实现了接口,必须采用CGLIB库

区别:

  1. jdk动态代理使用jdk中的类Proxy来创建代理对象,它使用反射技术来实现,不需要导入其他依赖。cglib需要引入相关依赖:asm.jar,它使用字节码增强技术来实现。

  2. 当目标类实现了接口的时候Spring Aop默认使用jdk动态代理方式来增强方法,没有实现接口的时候使用cglib动态代理方式增强方法。

五、Spring AOP编程的实现方式

  1. XML配置文件

(1)基于Spring自带的AOP(xml配置文件)
(2)基于Aspectj实现切面(普通pojo类),使用spring aop进行配置(xml配置文件)

  a. 首先,导入Aspectj相关依赖
  b. 实现切面类,实现通知方法
  c. 配置代理,配置切面,注入切面bean,定义切点、通知方法 

具体了解请点击参考链接

  1. 基于注解的实现方式 (强烈推荐)

(1)基础步骤

a. 首先,导入Aspectj相关依赖
b. 实现Aspect切面,并交给Spring容器管理。使用@Aspect、@Component,使用@Order(1)可以控制执行顺序
c. 定义切点,使用@Pointcut。
d. 定义通知,使用@Before、@After、@AfterReturning、@AfterThrowing、@Around
e. 配置启用AspectJ自动代理

(2)实现例子

<!--aop依赖1:aspectjrt -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.5</version>
        </dependency>

        <!--aop依赖2: aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>

实现日志切面类

@Aspect
@Component
@Slf4j
public class LogAspect {

    @Pointcut("execution(* com.example.controller..*(..))")
    public void pointCut(){}

    @Before("pointCut()")
    public void before(JoinPoint joinPoint){
        log.info("Before >>>");
    }

    @After("pointCut()")
    public void after(JoinPoint joinPoint){
        log.info("After >>>");
    }

    @AfterReturning("pointCut()")
    public void afterReturning(JoinPoint joinPoint){
        log.info("AfterReturning >>>");
    }

    @AfterThrowing("pointCut()")
    public void afterThrowing(JoinPoint joinPoint){
        log.info("AfterThrowing >>>");
    }

    //环绕通知
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("##########【环绕通知中的前置通知】##########");
        Object returnVale = joinPoint.proceed();
        log.info("##########【环绕通知中的后置通知】##########");
        return returnVale;
    }
}     

注意:@Pointcut的切点表达式有很多种形式,示例是匹配特定包下的所有方法,其它的比如匹配特定注解 @Pointcut("@annotation(com.example.annotation.MyCustomAnnotation)")

创建配置类,启用AspectJ自动代理

package com.example.demo.config;

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

@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}

注意:Spring项目配置启动Aspectj的自动代理的两种方式,一是在配置文件中添加配置<aop:aspectj-autoproxy/>,二是创建一个Java配置类来代替XML配置,使用@Configuration注解标记为配置类,并通过@ComponentScan注解来启用组件扫描,通过@EnableAspectJAutoProxy启用AspectJ自动代理。

无抛出异常时的输出:

2024-08-08 17:11:40.960  INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect         : ##########【环绕通知中的前置通知】##########
2024-08-08 17:11:40.961  INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect         : Before >>>
2024-08-08 17:11:40.961  INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect         : AfterReturning >>>
2024-08-08 17:11:40.961  INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect         : After >>>
2024-08-08 17:11:40.961  INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect         : ##########【环绕通知中的后置通知】##########

有抛出异常时的输出:

2024-08-08 17:11:03.058  INFO 5144 --- [nio-8080-exec-1] com.example.common.aop.LogAspect         : ##########【环绕通知中的前置通知】##########
2024-08-08 17:11:03.058  INFO 5144 --- [nio-8080-exec-1] com.example.common.aop.LogAspect         : Before >>>
2024-08-08 17:11:03.090  INFO 5144 --- [nio-8080-exec-1] com.example.common.aop.LogAspect         : AfterThrowing >>>
2024-08-08 17:11:03.090  INFO 5144 --- [nio-8080-exec-1] com.example.common.aop.LogAspect         : After >>>
2024-08-08 17:11:03.104 ERROR 5144 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: Test the AfterThrowing Advice] with root cause

java.lang.RuntimeException: Test the AfterThrowing Advice
	at com.example.controller.LogController.testAop(LogController.java:16) 
...
问题:为什么spring boot项目中实现aop,没有任何地方显性显性的启动AspectJ代理?

原因:
此示例是基于Spring Boot的框架,如果,它会自动配置 AOP。Spring Boot 应用通常会引入了spring-boot-autoconfigure依赖,这是springboot项目自动装配的基础,此依赖的spring.factories文件中有一项org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,此类有一个注解@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true),意思是在配置文件中有spring.aop.auto的配置,默认为true,它的作用与@EnableAspectJAutoProxy是类似的。

六、参考链接

  1. Spring AOP全面详解(超级详细)
  2. Spring AOP——Spring 中面向切面编程
  3. 静态代理和动态代理
  4. Spring 高手之路 19——Spring AOP 注解指南
  5. 使springAOP生效不一定要加@EnableAspectJAutoProxy注解
posted @ 2024-08-07 16:56  抒写  阅读(4)  评论(0编辑  收藏  举报