14-Spring AOP

SpringAOP是面向切面编程的实现,用于将横切关注点如日志记录、事务管理等从核心业务逻辑中分离。AOP包括切面、连接点、切点、通知和织入等概念。学习SpringAOP涉及添加框架支持、定义切点和通知,通过动态代理(JDK或CGLIB)实现。织入发生在运行时,代理对象动态创建,简化代码维护。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.什么是Spring AOP?

2.为什么要用AOP?

3.Spring AOP 怎么学?

3.1.AOP组成

3.1.1.切面(Aspect)

3.1.2.连接点(Join Point)

3.1.3.切点(Pointcut)

3.1.4.通知(Advice)

3.1.5.织入(Weaving)

3.2.Spring AOP实现

3.2.1.添加AOP框架支持

3.2.2.定义切面和切点

--->切点表达式说明:

3.2.3.定义相关通知

3.3.Spring AOP实现原理

3.3.1.织入(Weaving):代理的生成时机

--->AOP实现手段:

3.3.2.AOP实现技术

--->①静态代理:

--->②动态代理:

3.3.3.动态代理

--->①JDK 动态代理

--->②CGLIB动态代理实现

--->PS:(常见面试题)JDK 和 CGLIB 实现的区别

4.总结


1.什么是Spring AOP?

AOP(Aspect-Oriented Programming):面向切面编程,它是⼀种思想,它是对某一类事情的集中处理

AOP是一种软件开发的编程范式,用于将跨越多个模块的(横切)关注点从核心业务逻辑中分离出来,使得横切关注点的定义和应用能够更加集中和重用。

在传统的面向对象编程中,程序的功能逻辑被分散在各个对象中,而横切关注点(如日志记录、事务管理、安全控制等)则分散在多个对象之间,导致代码重复、可维护性差,并且难以修改和扩展。AOP 的目标就是解决这些问题。

AOP 通过引入横切关注点,将其与核心业务逻辑分离,并以模块化的方式进行管理。它通过切面(Aspect)来描述横切关注点,切面是对横切关注点的封装。切面定义了在何处、何时和如何应用横切关注点。在 AOP 中,切面可以横跨多个对象,独立于核心业务逻辑。

AOP 是⼀种思想,⽽ Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和IoC DI 类似。

2.为什么要用AOP?

比如在做后台系统时,除了登录和注册等⼏个功能不用做⽤户登录验证之外,其他⼏乎所有⻚⾯调⽤的前端控制器 Controller 都要先验证⽤户登录的状态。

之前的处理⽅式是每个 Controller 都要写⼀遍⽤户登录验证,然⽽当功能越来越多,那么要写的登录验证也越来越多,⽽这些⽅法⼜是相同的,就会增加代码修改和维护的成本。

对于功能统一,且使用的地方较多的功能,可以考虑 AOP 来统一处理

AOP 的优点是可以将横切关注点从应用程序的核心业务逻辑中分离出来,以便更好地实现模块化和复用。

通过使用 AOP,可以将通用的功能(如日志记录、性能统计、事务管理等)封装成切面,然后在需要的地方进行重用,从而提高代码的可维护性和可重用性。

只需要在某⼀处配置⼀下,所有需要判断⽤户登录⻚⾯(中的⽅法)就全部可以实现⽤户登录验证了,不再需要每个⽅法中都写相同的⽤户登录验证了,不用在业务代码中关注此功能了。

除了统⼀的⽤户登录判断之外,AOP 还可以实现:

  • 统⼀⽇志记录
  • 统⼀⽅法执⾏时间统计
  • 统⼀返回格式设置
  • 统⼀异常处理
  • 事务的开启和提交等

也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented

Programming,面向对象编程)的补充和完善。

3.Spring AOP 怎么学?

3.1.AOP组成

3.1.1.切面(Aspect)

切面是横切关注点的模块化单元,它将通知和切点组合在一起,描述了在何处、何时和如何应用横切关注点。

相当于定义了一个俱乐部。

切面 (Aspect) 由切点 (Pointcut) 和通知 (Advice) 组成,它既包含了横切逻辑的定义,也包括了连接点的定义。

切面是包含了通知,切点和切面的,相当于AOP实现的某个功能的集合

定义AOP的业务类型。(要干嘛:验证登录?记录日志?统计方法执行时间?)

3.1.2.连接点(Join Point)

在程序执行过程中的某个特定点,例如方法调用、异常抛出等。

相当于所有想进俱乐部的用户。(分为允许进去的和不允许进去的)

应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

连接点相当于需要被增强的某个AOP功能的所有方法

AOP中的所有方法声明。

3.1.3.切点(Pointcut)

用于定义哪些连接点被切面关注,即切面要织入的具体位置。

相当于规则,规定哪些人能进入会场,哪些人进不去。

切点是匹配连接点的谓词。

切点的作用就是提供一组规则(使⽤ AspectJ pointcut expression language 来描述)来配连接点,给满⾜规则的连接点添加通知。

切点相当于保存了众多连接点的一个集合。(如果把切点看成一个表,而连接点就是表中一条一条的数据)

提供一组规则,用来匹配连接点。

3.1.4.通知(Advice)

切面在特定切点上执行的代码,包括在连接点之前、之后或周围执行的行为。

相当于底层提供的一系列的服务:唱歌?吃饭?玩剧本杀?喝酒?泡澡?(5类)

切⾯也是有⽬标的 ——它必须完成的⼯作。在 AOP 术语中,切⾯的⼯作被称之为通知。

通知:定义了切⾯是什么,何时使⽤,其描述了切⾯要完成的⼯作,解决了何时执⾏这个⼯作的问题。

具体执行的业务代码,提供AOP方法实现。

AOP 整个组成部分的概念如下图所示,以多个⻚⾯都要访问⽤户登录权限为例:

3.1.5.织入(Weaving)

将切面应用到目标对象中的过程,可以在编译时、加载时或运行时进行。

3.2.Spring AOP实现

接下来使用Spring AOP来实现一下AOP的功能,目标是拦截所有UserController里的方法,每次调用UserController中任意一个方法时,都执行相应的通知事件。

3.2.1.添加AOP框架支持

AOP属于一个官方还没有收入到Spring Boot里的插件,需要手动去maven中央仓库寻找其依赖在pom.xml中添加如下配置(最好和Spring Boot项目版本号对应一致):

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

  

Maven中央仓库https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient/3.1

3.2.2.定义切面和切点

a.定义切面:

b.定义切点:

切点指的是具体要处理的某一类问题,比如用户登录权限验证,记录所有方法的执行日志等都是具体的问题。

在切点中定义拦截的规则,具体实现如下:

其中pointcut方法为空方法,不需要有方法体,此方法名就是起到一个“标识”作用,标识下面的通知方法具体指的是哪个切点(因为切点可能有很多个)。

切点是规则提供方,通知是规则执行方。

--->切点表达式说明:

@Pointcut("切点表达式")

AspectJ ⽀持三种通配符

  • * :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)。
  • .. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
  • + :表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+,表示继承该类的所有⼦类包括本身。

切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为: execution(<修饰符><返回类型><包.类.方法(参数)><异常>)

表示拦截符合()里面的方法。

修饰符和异常可以省略,具体含义如下:

修饰符,一般省略

  • public   公共方法
  • *   任意

返回值,不能省略

  • void   返回没有值
  • String   返回值字符串
  • *   任意

包,可省略

  • com.gyf.crm   固定包
  • com.gyf.crm.*.service   crm包下面子包任意(如:com.gyf.crm.staff.service)
  • com.gyf.crm..   crm包下面的所有子包(含自己)
  • com.gyf.crm.*.service..   crm包下面任意子包,固定目录service,service目录任意包

类,可省略

  • UserServiceImpl   指定类
  • *Impl   以Impl结尾
  • User*   以User开头
  • *   任意

方法名,不能省略

  • addUser   固定方法
  • add*   以add开头
  • *Do   以Do结尾
  • *   任意

(参数)

  • ()   无参
  • (int)   一个整型
  • (int,int)   两个整型
  • (..)   参数任意

throws,可省略,一般不写

eg1:通过方法返回值类型拦截对应方法:

 

返回int类型的getNumber方法没有被拦截,没有执行前置方法。

eg2:通过参数类型拦截对应方法:

表达式示例

  • execution(* com.cad.demo.User.*(..)) :匹配 User 类⾥的所有⽅法。
  • execution(* com.cad.demo.User+.*(..)) :匹配该类的⼦类包括该类的所有⽅法。
  • execution(* com.cad.*.*(..)) :匹配 com.cad 包下的所有类的所有⽅法。
  • execution(* com.cad..*.*(..)) :匹配 com.cad 包下、⼦孙包下所有类的所有⽅法。
  • execution(* addUser(String, int)) :匹配 addUser ⽅法,且第⼀个参数类型是 String,第⼆个参数类型是 int。

3.2.3.定义相关通知

先执行前置方法再执行sayhi/sayhello方法。

通知定义的是被拦截的⽅法具体要执⾏的业务,⽐如⽤户登录权限验证⽅法就是具体要执⾏的业务。

Spring 切⾯类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:

  • 前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
  • 后置通知使⽤ @After:通知⽅法会在⽬标⽅法执行完之后(返回或者抛出异常后)调⽤。
  • 返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法即将执行完时(返回后)调⽤。 (@After通知执行在@AfterReturning之后)
  • 抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
  • 环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后⾏⾃定义的⾏为。 (固定返回值Object,将整个执行过程封装到一个对象里面,再把这个对象返回回去,Spring拿到这个对象相当于打包好的环绕通知的解决方案,当触发到了拦截规则时,就会打包执行这个方法里的事件(也就是写死的方法)和环绕通知本身。)
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @RequestMapping("/sayhi")
    public String sayHi(String name) {
        log.info("执行了 sayhi 方法");
        return "你好," + name;
    }

    @RequestMapping("/sayhello")
    public String sayHello() {
        log.info("执行了 sayhello 方法");
        return "你好,sayHello。";
    }

    @RequestMapping("/getnum")
    public int getNumber(int number) {
        log.info("执行了 getNumber 方法");
        int num = number / 0;
        return number;
    }
}
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect //当前类为一个切面
@Component
@Slf4j
public class UserAspect {
    //定义切点,这里使用AspectJ表达式语法
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") //当前方法为一个切点(定义规则:拦截UserController下的所有方法)
    public void pointcut() {}

    //1.定义前置通知(AOP拦截之后具体执行的业务)
    @Before("pointcut()")
    public void doBefore() {
        log.info("执行了前置通知方法");
    }

    //2.后置通知方法
    @After("pointcut()")
    public void doAfter() {
        log.info("执行了后置通知方法");
    }

    //3.异常抛出之后的通知
    @AfterThrowing("pointcut()")
    public void doAfterThrowing() {
        log.info("异常之后的通知");
    }

    //4.返回值通知
    @AfterReturning("pointcut()")
    public void doAfterReturning() {
        log.info("执行 return 通知");
    }

    //5.环绕通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) { //变量名 joinPoint 叫啥都行
        Object result = null;
        //环绕通知的前置方法
        long stime = System.currentTimeMillis(); //记录方法开始时间
        log.info("自定义环绕通知的前置方法");
        try {
            result = joinPoint.proceed(); //执行了拦截的方法
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        long etime = System.currentTimeMillis(); //记录方法结束时间
        log.info("自定义环绕通知的后置方法");
        log.info("方法执行花费了:" + (etime - stime));
        return result;
    }
}

 

3.3.Spring AOP实现原理

Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截(Spring AOP能够代理的最小单位为方法)。

3.3.1.织入(Weaving):代理的生成时机

织⼊是把切面应用到目标对象创建新的代理对象的过程,切⾯在指定的连接点被织⼊到⽬标对象中。

在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:

  • 编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ(上面所有的写法就是AspectJ的写法)的织⼊编译器就是以这种⽅式织⼊切⾯的。
  • 类加载期:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器(ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
  • 运行期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是在程序运行期织入代理对象/切面的。

--->AOP实现手段:

  1. Spring AOP 官方的,最初写的东西不好用,写法麻烦 -> 改良后兼容了AspectJ的语法。(好比基于安卓底层系统照IOS进行包装为MIUI)
  2. AspectJ 是Java的一个切面框架,有自己的编译器。三方的,好用,写法简单,用的人多。(好比IOS)

3.3.2.AOP实现技术

--->①静态代理:

静态代理是一种在编译时就已经确定代理关系的代理方式。

在静态代理中,代理类和被代理类都要实现同一个接口或继承同一个父类,代理类中包含了被代理类的实例,并在调用被代理类的方法前后执行相应的操作。

静态代理的优点是实现简单,易于理解和掌握,但是它的缺点是需要为每个被代理类编写一个代理类,当被代理类的数量增多时,代码量会变得很大。

--->②动态代理:

动态代理是一种在运行时动态生成代理类的代理方式。

在动态代理中,代理类不需要实现同一个接口或继承同一个父类,而是通过 Java 反射机制动态生成代理类,并在调用被代理类的方法前后执行相应的操作。

动态代理的优点是可以为多个被代理类生成同一个代理类,从而减少了代码量,但是它的缺点是实现相对复杂,需要了解 Java 反射机制和动态生成字节码的技术。

3.3.3.动态代理

动态代理是一种在运行时动态生成代理类的代理方式。

动态代理的常用实现方法有两种:JDK 动态代理和CGLIB 动态代理。

这两种⽅式的代理⽬标都是被代理类中的⽅法,在运⾏期,动态地织⼊字节码⽣成代理类。

--->①JDK 动态代理

JDK 动态代理是一种使用 Java 标准库中的 java.lang.reflect.Proxy 类来实现动态代理的技术。

在 JDK 动态代理中,被代理类必须实现一个或多个接口,并通过 InvocationHandler 接口来实现代理类的具体逻辑。

具体来说,当使用 JDK 动态代理时,需要定义一个实现 InvocationHandler (反射管理器)接口的类,并在该类中实现代理类的具体逻辑。

然后,通过 Proxy.newProxyInstance() 方法来创建代理类的实例。

该方法接受三个参数:类加载器、代理类要实现的接口列表和 InvocationHandler 对象,如下代码所示:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;

//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler {
    //⽬标对象即就是被代理对象
    private Object target;

    public PayServiceJDKInvocationHandler(Object target) {
        this.target = target;
    }

    //核心代码
    //proxy代理对象
    @Override //重写invoke(执行调用)方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //传参是固定的
        //1.安全检查
        System.out.println("安全检查");
        //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过反射调⽤被代理类的⽅法。反射:通过字符串能直接调用到对象
        Object retVal = method.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }

    //测试
    public static void main(String[] args) {
        PayService target= new AliPayService(); //目标对象
        //⽅法调⽤处理器
        InvocationHandler handler = new PayServiceJDKInvocationHandler(target);
        //创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(target.getClass().getClassLoader(), new Class[]{PayService.class}, handler);
        proxy.pay();
    }
}

JDK 动态代理的优点是实现简单,易于理解和掌握,但是它的缺点是只能代理实现了接口的类,无法代理没有实现接口的类。

--->②CGLIB动态代理实现

CGLIB 动态代理是一种使用 CGLIB 库来实现动态代理的技术。

在 CGLIB 动态代理中,代理类不需要实现接口,而是通过继承被代理类来实现代理。

具体来说,当使用 CGLIB 动态代理时,需要定义一个继承被代理类的子类,并在该子类中实现代理类的具体逻辑。

然后,通过 Enhancer.create() 方法来创建代理类的实例。

该方法接受一个类作为参数,表示要代理的类,如下代码所示:

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;

public class PayServiceCGLIBInterceptor implements MethodInterceptor {
     //被代理对象
     private Object target;

     public PayServiceCGLIBInterceptor(Object target) {
         this.target = target;
     }

     @Override
     public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
         //1.安全检查
         System.out.println("安全检查");
         //2.记录⽇志
         System.out.println("记录⽇志");
         //3.时间统计开始
         System.out.println("记录开始时间");
         //通过cglib的代理⽅法调⽤
         Object retVal = methodProxy.invoke(target, args);
         //4.时间统计结束
         System.out.println("记录结束时间");
         return retVal;
     }

     public static void main(String[] args) {
         PayService target= new AliPayService();
         PayService proxy= (PayService) Enhancer.create(target.getClass(), new PayServiceCGLIBInterceptor(target));
         proxy.pay();
     }
}

CGLIB 动态代理的优点是可以代理没有实现接口的类,但是它的缺点是实现相对复杂,需要了解 CGLIB 库的使用方法。

--->PS:(常见面试题)JDK 和 CGLIB 实现的区别

JDK 动态代理和 CGLIB 动态代理都是常见的动态代理实现技术,但它们有以下区别:

  • JDK 动态代理基于接口,要求目标对象实现接口;CGLIB 动态代理基于类,可以代理没有实现接口的目标对象。
  • JDK 动态代理使用 java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 来生成代理对象;CGLIB 动态代理使用 CGLIB 库来生成代理对象。
  • JDK 动态代理生成的代理对象是目标对象的接口实现;CGLIB 动态代理生成的代理对象是目标对象的子类。
  • JDK 动态代理性能相对较高,生成代理对象速度较快;CGLIB 动态代理性能相对较低,生成代理对象速度较慢。
  • CGLIB 动态代理无法代理 final 类和 final 方法;JDK 动态代理可以代理任意类。

PS:

在 Spring 框架中,即使用了 JDK 动态代理又使用 CGLIB,默认情况下使用的是 JDK 动态代理,但是如果目标对象没有实现接口,则会使用 CGLIB 动态代理。

小结:

简单来说,JDK 动态代理要求被代理类实现接口,而 CGLIB 要求被代理类不能是 final 修饰的最终类,在 JDK 8 以上的版本中,因为 JDK 动态代理做了专门的优化,所以它的性能要比 CGLIB 高。

  1. JDK Proxy(JDK官方的动态代理,功能、性能上中规中矩)默认情况下,实现了接⼝的类,使⽤AOP 会基于 JDK ⽣成代理类。JDK 实现,要求被代理类必须实现接⼝,之后是通过 InvocationHandler 及 Proxy,在运⾏时动态地在内存中⽣成了代理类对象,该代理对象是通过实现同样的接⼝实现(类似静态代理接⼝实现的⽅式),只是该代理类是在运⾏期时,动态地织⼊统⼀的业务逻辑字节码来完成。 (JVM运行模式有2种:①编译执行:运行字节码,直接生成01的机器码,性能最快的;②解释执行:先把Java源代码转为字节码再从字节码去执行)JDK实现使用的是编译执行的方式。
  2. CGLIB (基于字节码的动态代理,性能高。默认使用它)默认情况下,没有实现接⼝的类(普通类),会基于 CGLIB ⽣成代理类。CGLIB 实现,被代理类可以不实现接⼝,是通过继承被代理类(不能被final修饰),在运⾏时动态地⽣成代理类对象。
  • CGLIB是Java中的动态代理框架,主要作⽤就是根据⽬标类和⽅法,动态⽣成代理类。
  • Java中的动态代理框架,⼏乎都是依赖字节码框架(如 ASM,Javassist 等)实现的。
  • 字节码框架是直接操作 class 字节码的框架。可以加载已有的class字节码⽂件信息,修改部分信息,或动态⽣成⼀个 class。 

4.总结

AOP 是对某⽅⾯能⼒的统⼀实现,它是⼀种实现思想;Spring AOP 是对 AOP 的具体实现。

Spring AOP 可通过 AspectJ(注解)的⽅式来实现 AOP 的功能,Spring AOP 的实现步骤是:

  1. 添加 AOP 框架⽀持。
  2.  定义切⾯和切点。
  3.  定义通知。

Spring AOP 是通过动态代理的⽅式,在运⾏期将 AOP 代码织⼊到程序中的,它的实现⽅式有两种:JDK Proxy 和 CGLIB。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值