• 浅谈 Spring AOP


    AOP(Aspect Oriented Programming):⾯向切⾯编程,它是⼀种思想,它是对某⼀类事情的集中处理。⽐如⽤户登录权限的效验,没学 AOP 之前,我们所有需要判断⽤户登录的⻚⾯(中的⽅法),都要各⾃实现或调⽤⽤户验证的⽅法,然⽽有了 AOP 之后,我们只需要在某⼀处配置⼀下,所有需要判断⽤户登录⻚⾯(中的⽅法)就全部可以实现⽤户登录验证了,不再需要每个⽅法中都写相同的⽤户登录验证了。

    一、AOP的组成

     我们先举个栗子大概理解一下

    以多个页面都要访问⽤户登录权限为例

    下面我们详细了解一下APO各个组成部分

    1.1、切⾯(Aspect)

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

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

    1.2、连接点(Join Point)

    应⽤执⾏过程中能够插⼊切⾯的⼀个点,这个点可以是⽅法调⽤时,抛出异常时,甚⾄修改字段
    时。切⾯代码可以利⽤这些点插⼊到应⽤的正常流程之中,并添加新的⾏为。

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

    1.3、切点(Pointcut))


    Pointcut 的作⽤就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述)来

    匹配 Join Point,给满⾜规则的 Join Point 添加 Advice。

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

    1.4、通知(Advice)

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

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

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

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

    二、Spring AOP 实现

    2.1、Spring AOP的实现步骤

    接下来我们使⽤ Spring AOP 来实现⼀下 AOP 的功能,完成的⽬标是拦截所有 UserController ⾥⾯的⽅法,每次调⽤ UserController 中任意⼀个⽅法时,都执⾏相应的通知事件。

    2.1.1、添加Spring AOP的依赖

    在pom.xml中:

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-aopartifactId>
    4. dependency>

    2.1.2、定义切面和切点

    切点指的是具体要处理的某⼀类问题,⽐如⽤户登录权限验证就是⼀个具体的问题,记录所有⽅法的执⾏⽇志就是⼀个具体的问题,切点定义的是某⼀类问题。
    Spring AOP 切点的定义如下,在切点中我们要定义拦截的规则,具体实现如下:

    1. @Aspect // 表明此类为⼀个切⾯
    2. @Component
    3. public class UserAspect {
    4. // 定义切点,这⾥使⽤ AspectJ 表达式语法
    5. @Pointcut("execution(* com.example.demo.controller.UserController.*
    6. (..))")
    7. public void pointcut(){ }
    8. }

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

    AspectJ ⽀持三种通配符

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

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

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

    表达式示例

    • 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
       

    2.1.3、实现通知

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

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

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

    具体实现如下:

    1. import org.aspectj.lang.ProceedingJoinPoint;
    2. import org.aspectj.lang.annotation.*;
    3. import org.springframework.stereotype.Component;
    4. @Aspect
    5. @Component
    6. public class UserAspect {
    7. // 定义切点⽅法
    8. @Pointcut("execution(* com.example.demo.controller.UserController.*
    9. (..))")
    10. public void pointcut(){ }
    11. // 前置通知
    12. @Before("pointcut()")
    13. public void doBefore(){
    14. System.out.println("执⾏ Before ⽅法");
    15. }
    16. // 后置通知
    17. @After("pointcut()")
    18. public void doAfter(){
    19. System.out.println("执⾏ After ⽅法");
    20. }
    21. // return 之前通知
    22. @AfterReturning("pointcut()")
    23. public void doAfterReturning(){
    24. System.out.println("执⾏ AfterReturning ⽅法");
    25. }
    26. // 抛出异常之前通知
    27. @AfterThrowing("pointcut()")
    28. public void doAfterThrowing(){
    29. System.out.println("执⾏ doAfterThrowing ⽅法");
    30. }
    31. // 添加环绕通知
    32. @Around("pointcut()")
    33. public Object doAround(ProceedingJoinPoint joinPoint){
    34. Object obj = null;
    35. System.out.println("Around ⽅法开始执⾏");
    36. try {
    37. // 执⾏拦截⽅法
    38. obj = joinPoint.proceed();
    39. } catch (Throwable throwable) {
    40. throwable.printStackTrace();
    41. }
    42. System.out.println("Around ⽅法结束执⾏");
    43. return obj;
    44. }
    45. }

    经过以上的代码我们就能实现 Spring AOP 了。

    三、 Spring AOP 实现原理

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

    3.1、静态代理

    静态代理角色分析

    • 抽象角色 : 一般使用接口或者抽象类来实现

    • 真实角色 : 被代理的角色

    • 代理角色 : 代理真实角色 ; 代理真实角色后 , 一般会做一些附属的操作 .

    • 客户 : 使用代理角色来进行一些操作 .

    静态代理的好处:

    • 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 .
    • 公共的业务由代理来完成 . 实现了业务的分工 ,
    • 公共业务发生扩展时变得更加集中和方便 .

    缺点 :

    • 类多了 , 多了代理类 , 工作量变大了 . 开发效率降低

    我们用代码举个栗子:

    3.2、动态代理

    • 动态代理的角色和静态代理的一样 .

    • 动态代理的代理类是动态生成的 . 静态代理的代理类是我们提前写好的

    • 动态代理分为两类 : 一类是基于接口动态代理 , 一类是基于类的动态代理

      • 基于接口的动态代理----JDK动态代理
      • 基于类的动态代理–cglib
      • 现在用的比较多的是 javasist 来生成动态代理 . 百度一下javasist
      • 我们这里使用JDK的原生代码来实现,其余的道理都是一样的!

    JDK的动态代理需要了解两个类

    核心 : InvocationHandler 和 Proxy , 可以打开JDK帮助文档看看

    【InvocationHandler:调用处理程序】

    Object invoke(Object proxy, 方法 method, Object[] args);

    //参数

    //proxy - 调用该方法的代理实例

    //method -所述方法对应于调用代理实例上的接口方法的实例。方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口。

    //args -包含的方法调用传递代理实例的参数值的对象的阵列,或null如果接口方法没有参数。原始类型的参数包含在适当的原始包装器类的实例中,例如java.lang.Integer或java.lang.Boolean 。

    【Proxy : 代理】

    //生成代理类

    public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(),this); }

    我们来使用动态代理实现代理我们后面写的UserService!

    我们也可以编写一个通用的动态代理实现的类!所有的代理对象设置为Object即可!

    1. public class ProxyInvocationHandler implements InvocationHandler {
    2. private Object target;
    3. public void setTarget(Object target) {
    4. this.target = target;
    5. }
    6. //生成代理类
    7. public Object getProxy(){
    8. return Proxy.newProxyInstance(this.getClass().getClassLoader(),
    9. target.getClass().getInterfaces(),this);
    10. }
    11. // proxy : 代理类
    12. // method : 代理类的调用处理程序的方法对象.
    13. public Object invoke(Object proxy, Method method, Object[] args) throwsThrowable {
    14. log(method.getName());
    15. Object result = method.invoke(target, args);
    16. return result;
    17. }
    18. public void log(String methodName){
    19. System.out.println("执行了"+methodName+"方法");
    20. }
    21. }
    22. //测试!
    23. public class Test {
    24. public static void main(String[] args) {
    25. //真实对象
    26. UserServiceImpl userService = new UserServiceImpl();
    27. //代理对象的调用处理程序
    28. ProxyInvocationHandler pih = new ProxyInvocationHandler();
    29. pih.setTarget(userService); //设置要代理的对象
    30. UserService proxy = (UserService)pih.getProxy(); //动态生成代理类!
    31. proxy.delete();
    32. }
    33. }

    3.3、Spring 动态代理的组成和比较


    以上就是本文分享的主要内容,对你有帮助的话,可以点个赞哦~~

  • 相关阅读:
    【Cookie和Session辨析】
    MongoDB ObjectId 详解
    java-php-python-ssm天津城建大学教室查询预约管理系统计算机毕业设计
    三、基于图像分类预训练编码及图神经网络的预测模型 【框图+源码】
    36、Java——吃货联盟订餐系统(JDBC+MySQL+Apache DBUtils)
    DML相关操作
    java-net-php-python-springtboot外卖系统计算机毕业设计程序
    架构每日一学 3:架构师六个生存法则之一:如何找到唯一且正确的架构目标?(二)
    ES6对象
    C++ 字符串格式化方法
  • 原文地址:https://blog.csdn.net/qq_48584557/article/details/132713176