【JavaEE进阶】Spring AOP

2023-10-16 15:11:28 浏览数 (2)

一. 初识SpringAOP

1.1 AOP是什么?

AOP(Aspect Oriented Programming):⾯向切面编程,它是⼀种思想,它是对某⼀类事情的 集中处理。比如在我们之前我的博客系统中所学习的用户登录权限的效验,没学 AOP 之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法.但是有了AOP之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就可以全部实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了.

AOP是一种思想,Spring AOP是一个框架.SpringAOP是对AOP思想的实现.类似于IoC和DI的关系.

1.2 为什么要使用AOP?

AOP的优势在于可以提高代码的可重用性和可维护性,减少了代码的重复性和耦合度.它能够将通用功能从核心业务逻辑中解耦出来,使得系统更加模块化和灵活。

比如说我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面调用的前端控制器都需要先验证用户登录的状态,此时我们应该如何处理呢?

我们之前的处理方式是每个 Contraller 都需要写一遍用户登录验证,但是当功能越来越多时,要写的登录验证也越来越多,这些方法又是相同的.这么多的方法就会有代码修改和维护的成本.此时我们对于这种功能统一,且使用的地方较多的功能,就可以考虑AOP来统一处理.

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

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

1.3 AOP组成

  1. 切面(Aspect)(类):指的是某一方面的具体内容就是一个切面,比如用户的登录判断就是一个“切面”,而日志的统计记录它有是一个“切面”。 切面是包含了通知,切点和切面的,相当于AOP实现的某个功能的集合.
  2. 切点(Pointcut)(方法):定义(一个)拦截规则。
  3. 通知(Advice)(方法具体实现代码):执行AOP具体逻辑业务.或者称为增强.
  4. 连接点(Joinpoint):所有可能触发切点的点就叫做连接点. 连接点相当于需要被增强的某个 AOP 功能的所有方法。 4.1 前置通知:在目标方法(实际要执行的方法)调用之前执行的通知; 4.2 后置通知:在目标方法调用之后执行的通知; 4.3 环绕通知:在目标方法调用前、后都执行的通知; 4.4 异常通知:在目标方法抛出异常的时候执行的通知; 4.5 返回通知:在目标方法返回的时候执行通知。

二. Spring AOP的实现

1. 添加Spring AOP依赖

在创建好的Spring Boot项目的pom.xml中添加Spring AOP的依赖,我们可以从中央仓库中下载.

代码语言:javascript复制
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后点击刷新,触发下载。

2. 定义切面和切点

这里使用注解@Aspect表示定义切面,即UserAspect类为切面,使用@Component注解表示让切面随着框架的启动而启动,这样切面中的切点定义的拦截规则才能生效。

代码语言:javascript复制
package com.example.demo.common;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect //定义切面
@Component //让切面随着框架的启动而启动
public class UserAspect {
    
    //定义切点,此处使用 AspectJ 表达式语法
    @Pointcut("execution(* com.exaple.demo.controller.UserController.*(..))")
    public void pointcut(){
    }
}

上述代码中,pointcut方法为空方法,它不需要有方法体,此方法名就是起到一个"标识"的作用,标识下面的通知方法具体指的是那个切点。因为一个切面中有很多切点。

上述pointcut方法上添加的@Pointcut注解的参数中使用切点表达式定义了具体的拦截规则。

代码语言:javascript复制
execution(* com.example.demo.controller.UserController.*(..))

切点表达的意思是:拦截UserController类中的所有方法其参数为任意参数并且返回值是任意类型的返回值.

  • execution:表示的意思为执行,执行的是后面跟的()中的规则。
  • *:表示的多个部分组成的,有修饰符和返回值类型。
  • com.example.demo.controller.UserController表示要拦截com.example.demo.controller包中的UserController类
  • 类后面跟的*表示UserController类中的所有方法。
  • ..表示的不定式传参

切点表达式由切点函数组成,其中execution()是最常见的切点函数用来匹配方法,语法为:

代码语言:javascript复制
execution(<修饰符><返回值类型><包.类.方法(参数)><异常>)

常见表达式示例:

  1. execution(* com.example.demo.User.*(..)):匹配User类中的所有方法。
  2. execution(* com.example.demo.User .*(..)):匹配该类的子类包括该类的所有方法
  3. execution(* com.example.*.*(..)):匹配com.example包下的所有类的所有方法
  4. execution(* com.example..*.*(..)):匹配com.example包下,子孙包下所有类的所有方法
  5. execution(* addUser(String,int)):匹配addUser方法,其第一个参数类型是String,第二个参数类型是int。

创建UserController类,这个类中的方法哪一个要被执行(目标方法)哪一个就是连接点.

代码语言:javascript复制
package com.example.demo.controller;

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

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/getuser")
    public String getUser(){
        System.out.println("do getUser");
        return "getUser";
    }

    @RequestMapping("/deluser")
    public String delUser(){
        System.out.println("do delUser");
        return "delUser";
    }
}

3. 定义相关通知

通知定义的是被拦截的方法具体要执行的业务。比如用户登录权限验证方法就是具体要执行的业务。 Spring AOP中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:

  • 前置通知使用@Before:通知方法会在目标方法(连接点)调用之前执行
  • 后置通知使用@After:通知方法会在目标方法(连接点)返回或者抛出异常后调用
  • 返回之后通知使用@AfterReturning:通知方法会在目标方法(连接点)返回后调用
  • 抛异常后通知使用@AfterThrowing:通知方法会在目标方法(连接点)抛出异常后调用
  • 环绕通知使用@Around:通知包裹了被通知的方法,在被通知的方法之前和调用之后执行自定义的行为。
  1. 前置通知和后置通知的实现:
代码语言:javascript复制
package com.example.demo.common;
 
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
 
@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
    //定义切点,@Pointcut注解的参数中定义了具体的拦截规则
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void pointcut(){}
 
    //定义前置通知
    @Before("pointcut()")//表示这个通知是针对pointcut方法的
    public void doBefore(){
        System.out.println("执行了前置通知");
    }
 
    //定义后置通知
    @After("pointcut()")
    public void doAfter(){
        System.out.println("执行了后置通知");
    }
}

当我们在前端页面中访问UserController类的方法时,后端程序的控制台上每次出现的结果是先执行前置通知,在执行目标方法(连接点),然后执行后置通知。

2. 环绕通知的具体实现 环绕通知方法是具有Object类型的返回值,需要把方法执行结果返回给框架,框架拿到对象继续执行。

代码语言:javascript复制
package com.example.demo.common;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
 
@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
    //定义切点,@Pointcut注解的参数中定义了具体的拦截规则
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void pointcut(){}
 
    //定义前置通知
    @Before("pointcut()")//表示这个通知是针对pointcut方法的
    public void doBefore(){
        System.out.println("执行了前置通知");
    }
 
    //定义后置通知
    @After("pointcut()")
    public void doAfter(){
        System.out.println("执行了后置通知");
    }
 
    //定义环绕通知
    @Around("pointcut()")
    //环绕通知方法的参数为要执行的连接点,也就是我们在前端访问的目标方法
    public Object doAround(ProceedingJoinPoint joinPoint){
        System.out.println("环绕通知之前");
        Object result = null;
        try {
            //执行目标方法,它的目标方法就是我们在前端访问的方法
            result = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("环绕通知之后");
        return result;
    }
}

从执行结果中可以看到环绕通知的执行范围,可以环绕执行通知是最先执行的,然后是执行前置通知,然后再执行目标方法,然后执行后置通知,最后所有的方法执行完成了,环绕通知方法才会执行完成。

三. AOP的实现原理

动态代理是一种设计模式,它允许你在运行时创建代理对象并将方法调用转发给真实对象。在这种模式中,代理对象和真实对象实现了相同的接口,使得代理对象可以用来代替真实对象,并在需要时执行额外的逻辑。

动态代理可以用于许多场景,如拦截方法调用、记录日志、实现事务管理等。通过使用动态代理,你可以在不修改原始对象的情况下,增加或改变其行为。

AOP的实现原理可以通过动态代理来实现。在Java中,有两种常见的动态代理方式:基于接口的代理(JDK动态代理)和基于类的代理(CGLIB动态代理)。

JDK动态代理: JDK动态代理是基于接口的代理方式。当目标对象实现了接口时,JDK动态代理通过创建一个实现了同样接口的代理类,并将方法调用委托给目标对象。在AOP中,切面被表示为一个包含通知逻辑的代理类,而目标对象则是实现了接口的原始类。 JDK动态代理通过Java的反射机制来调用目标对象的方法,并提供了InvocationHandler接口让开发人员定义通知逻辑的执行方式。在运行时,当方法被调用时,JDK动态代理会拦截方法调用,并在必要的时候执行切面中定义的通知逻辑。 JDK动态代理通常使用Java.lang.reflect.Proxy类和Java.lang.reflect.InvocationHandler接口来实现。

CGLIB动态代理: CGLIB动态代理是基于类的代理方式。当目标对象没有实现接口时,CGLIB动态代理通过创建目标对象的子类,并重写父类中的方法来实现代理功能。在AOP中,切面被表示为一个继承了目标对象的子类,切面的通知逻辑被插入到子类相应的方法中。 CGLIB动态代理使用了字节码生成库来创建目标对象的子类,并通过继承和方法的重写来实现方法拦截和通知逻辑的执行。

不论是JDK动态代理还是CGLIB动态代理,它们都能够在运行时生成代理类并将切面逻辑织入到目标对象中。具体来说,AOP框架(如Spring)在启动时会扫描切面和目标对象,并根据配置信息动态生成代理类。当调用目标对象的方法时,代理类会拦截方法调用,在合适的位置执行切面中的通知逻辑。这样就实现了将横切关注点与核心业务逻辑分离的效果。

需要注意的是,JDK动态代理只能代理实现了接口的类,而CGLIB动态代理可以代理没有实现接口的类。在选择使用哪种代理方式时,可以根据目标对象是否实现了接口进行判断。如果目标对象实现了接口,则可以使用JDK动态代理;如果目标对象没有实现接口,则需要使用CGLIB动态代理。

0 人点赞