Spring AOP

目录

一、什么是AOP

1.1 AOP的定义

1.2 为什么要有AOP

二、Spring AOP 应该学习哪些知识?

三、Spring AOP的组成

3.1 切面(Aspect,可以理解为"类")

3.2 切点(Pointcut,可以理解为"方法")

3.3 通知(Advice, 可以理解为方法的具体实现)

3.4 连接点(Join Point)

3.5 AOP概念图

 四、Spring AOP 实现

4.1 添加Spring AOP 框架支持

4.2 定义切面,切点,通知

4.3 定义连接点

4.4 关于环绕通知的扩展

 4.4.1 环绕通知与前后置通知的执行顺序

4.4.2 环绕通知和CGLIB的关系

五、Spring AOP 实现原理

5.1 AOP实现技术(静态代理,动态代理)

5.2 SpringAOP为什么仅限于对方法级别的拦截

5.3 JDK动态代理和CGLIB动态代理的区别

六、总结


一、什么是AOP

1.1 AOP的定义

在介绍Spring AOP之前,我们先来看看AOP是什么。

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

AOP是一种思想,SpringAOP则是一个框架,其提供了一种对AOP思想的实现,它们的关系和IoC与DI类似。

1.2 为什么要有AOP

比如用户登录权限的校验:如果没有AOP的存在,我们就需要在一个网站的某些场景下(只有用户登陆后才能访问的场景),再次对用户是否登录进行判断,这不仅增加了代码修改和维护的成本,而且不利于实现低耦合,无法让开发者把业务逻辑给区分开。

因此,对于以上场景,需要对这种使用次数多,功能又是相同的,就需要考虑AOP来进行统一处理。

当然,除了统一的用户登录判断之外,AOP还可以实现:

  • 统一日志记录
  • 统一方法执行时间统计
  • 统一的返回格式设置
  • 统一的异常处理
  • 事务的开启和提交等

也就是说,AOP可以扩充多个对象的某个能力,所以AOP可以说是OOP(Object Oriented
Programming,⾯向对象编程)的补充和完善。

二、Spring AOP 应该学习哪些知识?

  1. 学习AOP是如何组成的,也就是学习AOP组成的相关概念。
  2. 学习Spring AOP的使用
  3. 学习Spring AOP实现原理

三、Spring AOP的组成

3.1 切面(Aspect,可以理解为"类")

切面是由切点和通知组成的,它既包含了横切逻辑的定义,也包含了连接点的定义。

通俗来说,某一方面的具体内容就是一个切面,比如用户登录判断就是一个切面,而日志的统计记录也是一个切面。

3.2 切点(Pointcut,可以理解为"方法")

定义具体的拦截规则

3.3 通知(Advice, 可以理解为方法的具体实现)

定义了切面是什么,描述了切面要完成的工作,简单来说,就是 AOP的执行逻辑。

Spring切面类中,可以在方法上使用以下注释,会设置方法为通知方法,在满足条件后会通知该方法进行调用:

  • 前置通知:使用@Before,通知方法会在目标调用之前执行。
  • 后置通知:通知方法会在目标返回或者抛出异常后调用。
  • 返回后通知:使用@AfterReturning,通知方法会在目标方法返回后调用。
  • 抛异常后通知:通知方法会在目标方法抛出异常后调用。
  • 环绕通知:使用@Around,通知包裹了被通知的方法,在被通知方法之前和之后执行自定义的行为。

3.4 连接点(Join Point)

所有可能触发的点就叫做连接点。 

3.5 AOP概念图

AOP的整个组成概念如下图所示,以多个页面都要访问用户登录权限为例:

 四、Spring AOP 实现

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

  1. 添加Spring AOP 框架支持
  2. 定义切面和切点
  3. 定义通知

4.1 添加Spring AOP 框架支持

去Maven仓库中找到Spring AOP的依赖(注意,寻找的其实是用于SpringBoot项目的SpringAOP依赖,如果是用于Spring项目的,Spirng自带的库中是已经添加过该依赖的)

在porm.xml 中添加如下配置:

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

4.2 定义切面,切点,通知

@Aspect // 切面
@Component  // 不能忽略
public class UserAOP {
    // 切点 (配置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
    public void pointcut() {
    }

    // 前置通知 @Before里面的参数取决于 切点的方法名
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知: " + LocalDateTime.now());
    }
}

4.3 定义连接点

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;



@Controller
public class UserController {

    @RequestMapping("user/sayhi")
    public String sayHi() {
        System.out.println("执行了 sayHi 方法");
        return "hi, Spring boot aop";
    }
    @RequestMapping("/user/login")
    public String login() {
        System.out.println("执行了 login 方法");
        return "do user login";
    }
}

访问:localhost:8080/user/sayhi  后:控制台信息如下:

4.4 关于环绕通知的扩展

 4.4.1 环绕通知与前后置通知的执行顺序

 我们先来看一组代码:

package com.example.demo.common;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Aspect // 切面
@Component  // 不能忽略
public class UserAOP {
    // 切点 (配置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
    public void pointcut() {
    }

    // 前置通知 @Before里面的参数取决于 切点的方法名
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知: " + LocalDateTime.now());
    }
    @After("pointcut()")
    public void doAfter() {
        System.out.println("执行了后置通知: " + LocalDateTime.now());
    }

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知开始了");
        Object obj = joinPoint.proceed();
        System.out.println("环绕通知结束了");
        return obj;
    }
}
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class UserController {

    @RequestMapping("user/sayhi")
    public String sayHi() {
        System.out.println("执行了 sayHi 方法");
        return "hi, Spring boot aop";
    }
    @RequestMapping("/user/login")
    public String login() {
        System.out.println("执行了 login 方法");
        return "do user login";
    }
}

执行结果如下:

我们发现,环绕通知始终是与最外侧的,这是因为joinPoint.proceed执行的是login方法本身,相应的,只有执行到方法本身时候,才会调用相对应的前置通知和后置通知。

因此也就有了这里环绕通知始终在最外侧的体现。

4.4.2 环绕通知和CGLIB的关系

使用以上程序代码,在 Object obj = joinPoint.proceed();处打一个断点,再次访问localhost:8080/user/login 就会发现,其内部是使用了CGLIB代理,通过这个代理调用了方法本身,也就是login方法。

如何利用环绕通知来统计方法的执行时间?

package com.example.demo.common;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Aspect // 切面
@Component  // 不能忽略
public class UserAOP {
    // 切点 (配置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
    public void pointcut() {
    }

    // 前置通知 @Before里面的参数取决于 切点的方法名
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知: " + LocalDateTime.now());
    }
    @After("pointcut()")
    public void doAfter() {
        System.out.println("执行了后置通知: " + LocalDateTime.now());
    }

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知开始了");
        long startTime = System.currentTimeMillis();
        Object obj = joinPoint.proceed();
        String methodName = joinPoint.getSignature().getName();
        System.out.println("环绕通知结束了");
        long endTime = System.currentTimeMillis();
        long executionTime = endTime-startTime;
        System.out.println(methodName+"执行时间为: "+executionTime);
        return obj;
    }

}

运行结果: 

五、Spring AOP 实现原理

Spring AOP 是构建在动态代理基础上,因此Spring 对 AOP 的支持仅限于方法级别的拦截。

Spring AOP 支持 JDK Proxy 和CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用AOP会基于JDK生成代理类,没有实现接口的类,会基于CGLIB生成代理类。

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

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

织入的过程可以在编译时,类加载时或运行时进行。根据织入的时间不同,有以下几种织入方式:

  • 编译时织入:在编译源代码成字节码的过程中,将切面代码织入到目标类中。例如 lombok就属于编译时织入。
  • 类加载时织入:在类加载过程中,通过类加载器将切面逻辑织入到目标类中。例如JVM在垃圾回收时有个类就属于类加载织入,其是在类加载过程中就织入到JVM中的。
  • 运行时织入:在目标对象的方法执行过程中,动态的将切面代码织入到方法的不同执行点。

SpringAOP就是属于运行时织入切面的。

5.1 AOP实现技术(静态代理,动态代理)

静态代理

静态代理是一种编译时就已经确认代理关系的代理方式。在静态代理中,代理类和被代理类都要实现同一个接口或继承同一个父类,代理类中包含了被代理类的实例,并在调用被代理的方法前后执行相应的操作。

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

动态代理

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

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

5.2 SpringAOP为什么仅限于对方法级别的拦截

  • JDK动态代理的限制: JDK动态代理只能代理实现了接口的类。在Spring AOP中,如果目标对象实现了接口,Spring将使用JDK动态代理来创建代理对象。由于JDK动态代理是基于接口实现的,它只能代理目标对象接口中声明的方法,无法代理目标对象内部的其他方法。
  • CGLIB动态代理的限制:CGLIB动态代理可以代理没有实现接口的类,在SpringAOP中,如果目标对象没有实现接口,Spring将使用CGLIB动态代理来创建代理对象,尽管CGLIB动态代理可以代理目标对象的所有方法,但是也存在限制,比如无法代理被标记为final的方法。

总结:

虽然 Spring AOP 的支持仅限于方法级别,但是这足以满足大部分应用中的拦截和增强需求。对于更复杂的场景,如果需要更精细的控制和更广泛的横切逻辑,可以考虑使用 AspectJ 等更强大的 AOP 框架。

5.3 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动态代理可以代理任意类。

总结:

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

六、总结

AOP是对某方面能力的统一实现,它是一种实现思想,Spring AOP是对AOP的具体实现,Spring AOP可通过AspectJ(注解)的方式来实现AOP的功能,SpringAOP的实现步骤是:

  1. 添加AOP框架支持
  2. 定义切面和切点
  3. 定义通知

SpringAOP是通过动态代理的方式,在运行期将AOP织入到程序中,它的实现方式有两种:JDK Proxy 和CGLIB。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值