您现在的位置是:首页 >学无止境 >Spring AOP网站首页学无止境

Spring AOP

学习自省 2024-06-19 13:56:23
简介Spring AOP

目录

1、AOP理解

2、使用场景

3、AOP组成

3.1、切面(类)

3.2、切点(方法)

3.3、通知(方法具体实现代码)

3.4、连接点

4、实现AOP

4.1、添加Spring Boot AOP框架

4.2、创建切面和切点

4.3、创建通知

4.3.1、前置通知方法

4.3.2、后置通知方法

4.3.3、环绕通知方法

4.4、创建连接点

5、Spring AOP实现原理

5.1、JDK动态实现

5.2、CGLIB动态实现

5.3、JDK和CGLIB实现的区别

5.3.1、JDK

5.3.2、CGLIB

6、AOP和IoC的区别


1、AOP理解

AOP:是面向切面编程,是一种思想,对某一类事情的集中处理

举个例子例子来说明:例如你访问一个网站(可以登录的那种),登录后服务器这边是判定你登录状态的,进入每个页面也都会判定一下,如果没有被判定为登录,就会跳转到登录页面进行登录,那每个也页面都需要写一个判定登录,这不就很麻烦了嘛(代码冗余)

使用AOP思想:我们只需要在某一处配置一下,所有需要的判断用户登录页面,全部可以实现用户验证,不再需要每个页面都写一遍验证代码

2、使用场景

前面举例登录验证就是一个比较常用的场景,处理登录注册都会使用到AOP,当然也不仅止于登操作,对于这种功能统一,且使用的地方较多的功能,就可以考虑AOP来统一处理

实现场景:
<1>统一日志记录

<2>统一方法执行时间统计格式

<3>统一的返回格式设置

<4>统一的异常处理

<5>事务的开启和提交等

也就是说使用AOP可以扩充多个对象的某个能力,针对原来的OOP(面向对象)算是一个升级版的

3、AOP组成

3.1、切面(类)

切面是指某一方面的具体内容就是一个切面的,判定用户登录就是一个切面,时间格式统一处理也是一个切面,就是我们要通过AOP执行的一个具体内容

3.2、切点(方法)

在执行后,但尚未执行完,进行拦截验证,我们需要判定的内容

制作一个规则作为拦截处理(定义拦截规则)

3.3、通知(方法具体实现代码)

切点有了,操作是拦截下来,总要干点什么吧

执行AOP逻辑业务(以下会通过代码给友友们演示其作用)

<1>前置通知:在目标方法(真正要执行的方法)调用之前执行的通知(涉及注解@Before)

<2>后置通知:在目标方法调用之后执行的通知(涉及注解@After)

<3>环绕通知:在目标方法调用前、后都会执行的通知(涉及注解@Around)

<4>异常通知:在目标方法抛出异常的时候执行的通知(涉及注解@AfterThrowing)

<5>返回通知:在目标方法返回的时候执行通知(涉及注解@AfterReturning)

3.4、连接点

所有可能触发切点的点叫做“连接点”

举个例子:

 4、实现AOP

4.1、添加Spring Boot AOP框架

我们先创建一个Spring boot项目,以下我们直接跳转到选择依赖的部分和版本,因为我这里使用的是JDK8所以使用2版本的,如果友友使用的是JDK17以上包括JDK17的这里使用3版本的( 友友们不是很了解前面的创建步骤的,可以看看这篇博客

 这里会有友友们问为啥这里不添加spring AOP的依赖,其实这里是搜不到的,所以我们需要自己去引入,引入之前先做好一系列前置操作

 我们去maven仓库,引入一下依赖(直接在浏览地址栏中输入mvn既可):

 将依赖放入pom.xml文件中:我这里也直接给出依赖(不加版本的依赖,Spring boot会自己帮你配置对应的版本)

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 4.2、创建切面和切点

这些操作都算是工具或者组件,就把他们放在common层或者config层就行

我们这里就拿common层为例:

@Aspect    //注解就是一个切面那个单词
@Component    //不能省略 为啥嘞??我们得随着项目一起启动,要么要我们干啥
public class UserAOP {

    //切点  (配置拦截规则) AspectJ的语法
    @Pointcut("execution(* com.example.demo.controller.Usercontroller).*(..)")
    public void pointcut(){
        
    }
}

注:pointcut方法就是一个空方法,它不需要有方法体,此方法名就是起到一个“标识”的作用,标识下面的通知方法具体指的是哪个切点(切点可能会是多个)

这里@Pointcut注解就是切点的注解,参数内容是什么意思,先接收一点基础内容:

AspectJ支持三种通配符

*:匹配任意字符,只匹配一个元素(包,类,方法和方法参数)

..:匹配任意字符,可以匹配的多个元素,在表示类时,必须和*联合使用

+:表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的
所有⼦类包括本身

execution()是最常用的切点函数,用来匹配方法,语法为:

execution(<修饰符><返回类型><包。类.方法(参数)><异常>)

就拿我们写的这个来解释一下

这里为什么没有看见 修饰符和异常呢,修饰符和异常可以写也可以不写,修饰符不写的时候,我们想写什么修饰的就写什么修饰的,也就不用注意了(修饰符是啥:public ,private这些就是)

简单的看几个案例来理解execution():

execution(* com.example.demo.User.*(..)):配置User类中的所有方法

execution(* com.example.demo.User+*(..)):匹配User类以及User子类的所有方法

execution(* com.example.demo.*+*(..)):匹配 com.example.demo包下的所有类的所有方法

execution(*addUser(String,int)):匹配addUser方法,且第一个参数类型是String,第二个参数类型int

4.3、创建通知

通知就是以上提及的5种,切点拦截了,该通过“通知”来写逻辑业务了(这里我们就写3种通知方法作为演示)

4.3.1、前置通知方法

这里注解@Before中参数就是我们写切点时的标识方法

    @Before("pointcut")
    public void doBefore(){
        System.out.println("执行了前置通知:"+ LocalDateTime.now());
    }

4.3.2、后置通知方法

只有注解不同

    @After("pointcut")
    public void doAfter(){
        System.out.println("执行了后置通知"+LocalDateTime.now());
    }

4.3.3、环绕通知方法

环绕通知是什么意思:就是既执行前置方法,也执行后置方法,但是它的前置方法和后置方法与@Before注解和@After注解不是同一个(一会创建连接点了给友友们展示结果)

    //环绕通知
    @Around("pointcut")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("前置执行环绕通知");
        Object obj=joinPoint.proceed();
        System.out.println("后置执行环绕通知了");
        return obj;
    }

这里需要注意的就是两个点,一个是ProceedingJoinPoint类,它就是一个连接点的操作类,通过它定义的参数进行控制,如果你只想执行“前置执行环绕通知”那执行就是,不需要执行ProceedingJoinPoint类定义的参数操作,这里joinPoint.proceed()就是允许继续执行,它就是这里所有通知老大,它决定是否还继续执行

4.4、创建连接点

创建连接点,就是创建我们要操作涉及AOP的方法(这里主要时为了演示,没有实际意义):

@RestController
public class UseController {
    @RequestMapping("/user/sayhi")
    public String sayhi(){
        System.out.println("执行了 sayhi 方法");
        return " spring boot  aop ";
    }
}

 运行结果及分析:

 5、Spring AOP实现原理

Spring AOP是以代理的基础上实现的;说到代理就被划分为动态代理和静态代理

静态代理:程序运行前就已经定义好代理类,写固定了,需要为每一个被代理对象创建一个代理类,不灵活

动态代理:通过反射机制动态地生成代理类,代理类与被代理类实现相同的接口或者继承相同的父类,并在方法调用前后进行额外的操作,在实现的技术⼿段上,都是在 class 代码运⾏期,动
态的织⼊字节码

Spring AOP基于动态代理,Spring对AOP的支持局限于方法的拦截

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

Spring AOP代理

<1>CGLIB的动态代理框架,主要作用就是根据目标类和方法,动态生成代理类

<2>Java中的动态代理框架,几乎都是依赖字节码框架(ASM,Javassist等)实现的

注:字节码框架直接操作class字节码的框架,可以加载已有的class字节码文件信息,修改部分信息,或动态生成一个class

这个代理是怎么进行处理的:

调用者想要调用目标对象,前面说了AOP是干什么的了,拦截嘛,所以代理会帮我们先进行调用过滤掉一些不需要的,再将目标对象交给调度者

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

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

织入时机:

<1>编译期:切面在目标类编译时期被织入,例如:lombok在编译时期就生成的代码,编译过后就会生成字节码

<2>类加载期:切面在目标类加载的JVM时被织入,例如JVM有一个垃圾回收的类,JVM启动时这个垃圾回收的线程就会启动也就是类加载时期进行织入的

<3>运行期:切面在应用执行时刻被织入,例如:AOP容器会为目标对象动态一个代理对象,Spring AOP的这个代理在运行时才会织入

5.1、JDK动态实现

JDK实现,先通过实现InvocationHandler接口创建方法调用处理器,再通过Proxy来创建代理类

下面简单了解一下:

import com.example.demo.service.UserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JDKDynamicProxy implements InvocationHandler {
    //⽬标对象即就是被代理对象
    private Object target;
    public  JDKDynamicProxy( Object target) {
        this.target = target;
    }
    //proxy代理对象
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //<1>.安全检查
        System.out.println("安全检查");
        //<2>.记录⽇志
        System.out.println("记录⽇志");
        //<3>.时间统计开始
        System.out.println("记录开始时间");
        //5、通过反射调⽤被代理类的⽅法 也就是接口过来重写的方法
        Object retVal = method.invoke(target, args);
        //<4>.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
    public static void main(String[] args) {
        //1、 实例 ,重写了接口的方法,但是这里不会执行内部重写的方法
        UserService target= new UserService() {
            @Override
            public void sayhi() {
                System.out.println("hello world");;
            }
        };
        //2、⽅法调⽤处理器
        InvocationHandler handler =
                new JDKDynamicProxy(target);
        //3、创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
        UserService proxy = (UserService) Proxy.newProxyInstance(
                //第一个参数是代理类,我们开始创建的
                target.getClass().getClassLoader(),
                //第二参数是被代理实现的接口
                new Class[]{UserService.class},
                //方法嗲用处理器
                handler
        );
        //4、这里的 proxy 代理 在调用目标方法时就会 调用 到 invoke 最后再调用目标方法
        proxy.sayhi();
        //System.out.println(proxy.sayhi());
    }
}

注:这里的 UserService是我们自己写的接口,友友们如果是想复制看看效果一定要自己写一个接口放上去

运行结果:

 5.2、CGLIB动态实现

CGLIB不需要通过接口来实现动态代理,而是通过被代理类的子类作为代理

我们创建的类需要实现MethodInterceptor接口重写intercept方法,也就是我们现在写的

import com.example.demo.service.ArticleService;
import com.example.demo.service.UserService;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLIBDynamicProxy implements MethodInterceptor {

    //被代理对象
    private Object target;
    public CGLIBDynamicProxy(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("记录开始时间");
        //3、通过CGLIB的代理⽅法调⽤
        Object retVal = methodProxy.invoke(target,args);
        //<4>.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
    public static void main(String[] args) {
        //1、实例 该接口一个子类作为动态代理类
        UserService target=new ArticleService();
        //1、开始创建CGLIB动态代理: 第一参数被代理类的子类 第二个参数 实例一个将被代理类的子类
        UserService proxy= (UserService) Enhancer.create(target.getClass(),new CGLIBDynamicProxy(target));
        //2、代理执行
        proxy.sayhi();
    }
}

注:这里UserService是接口,ArticleService一个类继承了UserService接口 

5.3、JDK和CGLIB实现的区别

5.3.1、JDK

<1>JDK 动态代理是基于接口实现的,它要求被代理对象实现一个接口,通过 InvocationHandler 及 Proxy在运⾏时动态的在内存中⽣成了代理类对象,该代理类是在运⾏期时,动态的织⼊统⼀的业务逻辑字节码来完成

性能:JDK动态代理性能相对较高,生成代理对象速度较快

代理条件:JDK动态代理无法代理fina类和final方法

5.3.2、CGLIB

 <1>CGLIB 动态代理则是通过继承被代理对象来实现的,它不需要被代理对象实现接口,而是直接生成一个被代理对象的子类作为代理对象

性能:CGLIB动态代理性能相对较低,生成代理对象较慢

代理条件:CGLIB动态代理可以代理任意类的方法,但是CGLIB被代理类不能是final修饰的最终类

注:JDK8以后包括JDK8版本,JDK动态代理优化后相比CGLIB性能好

6、AOP和IoC的区别

相似之处:Spring AOP就提供了AOP思想的框架,它俩的关系类似于:IOC是思想和DI是IoC的实现

不同之处:

AOP通过动态代理机型方法级别的拦截,调用者调用目标方法会先通过动态代理,在再拿到目标方法(总的来说其实还是自己去调用的方法)

 相比IoC容器,IoC是将对象都实例好存储Spring中,调用的时候也通过Spring拿的(通过Spring获取对象后进行调用,Spring中的IoC

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。