AOP -面向切面编程

AOP -面向切面编程

aop是oop(面向对象编程)的补充和完善。oop是一种纵向开发,然而当面对需要一些横向的功能如日志,就会导致大量重复的代码。

aop利用横切把一些重复的非核心的代码插入到核心代码周围,不需要重复写很多遍。

应用场景:

  • 日志记录,在方法的执行前后插入日志功能

  • 事务处理,在方法开始前开启事务,方法结束后提交事务或回滚

  • 安全控制,在系统中包含某些需要安全控制的操作,进行权限判断等

  • 性能监控,在方法执行前记录事件戳,方法执行完后计算方法执行的时间

  • 异常处理,处理方法过程中的异常,可以记录日志、发送邮件等

  • 缓存控制,在方法执行前查询缓存中是否有数据

  • 动态代理,AOP的实现方式之一就是动态代理

 

1.AOP中的名词

  • 横切关注点:业务处理的主要流程是核心关注点,其他的一些非核心的业务如权限认证、日志、事务等就是横切关注点,他们发生在核心关注点的多处

  • 通知(通知):提取出来的重复的非核心代码

    • 前置通知:在被代理的目标方法前执行

    • 返回通知:在被代理的目标方法成功结束后执行

    • 异常通知:在被代理的目标方法异常结束后执行

    • 后置通知:在被代理的目标方法最终结束后执行

    • 环绕通知:使用 try...catch...finally 结构环绕整个被代理的目标方法,包括上面四种 通知对应的所有位置

  • 连接点: 能够被插入通知的 方法

  • 切入点:被选中的连接点,也就是真正被插入通知的方法

  • 切面:切入点和通知的结合,是一个类

  • 目标: 被代理的目标对象,也就是向哪个对象中插入代码

  • 代理:向目标对象应用通知后创建的对象

  • 织入:把通知应用到目标对象上这个动作

 

2.AOP的实现

2.1 aop的底层技术

img

在底层是通过代理技术来实现的,主要分为两种情况:jdk动态代理和cglib

  • jdk动态代理是原生的实现方式,需要被代理的目标类必须实现接口。然后通过接口创建一个 代理类,所以代理对象和目标对象实现了同一个接口。

  • cglib:代理类继承了目标类,不需要目标类有接口

在实现层的上一层还有 AspectJ 注解层,AspectJ是早期的AOP实现框架,SpringAOP借助了这个框架来实现

所以在导入依赖的时候需要导入 spring的aop依赖和 aspectj的依赖,以及这两个框架整合的依赖。

 

2.2 初步使用aop

使用aop的前提是有横向切入的需求,这里我们通过 给计算类添加日志的功能来使用aop。

  1. 导入依赖

    由上面可以得知,需要三个依赖:aop、aspectj、spring-aspectj

    aop依赖可以通过spring-context 进行传递, aspectj 可以通过spring-aspectj依赖进行传递

    所以导入 spring-context 和 spring-aspectj即可

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>6.0.6</version>
    </dependency>
  2. 正常写计算类的核心业务代码

    • 准备接口(有接口的话,底层的实现技术就是 jdk 动态代理)

      package com.ztone.service;
      ​
      public interface Calculate {
          int add(int i,int j);
      ​
          int sub(int i,int j);
      ​
          int mul(int i,int j);
      ​
          int div(int i,int j);
      }
    • 实现计算接口

      package com.ztone.impl;
      ​
      import com.ztone.service.Calculate;
      import org.springframework.stereotype.Component;
      ​
      @Component
      public class CalculateImpl implements Calculate {
          @Override
          public int add(int i, int j) {
              int result = i + j;
              return result;
          }
      ​
          @Override
          public int sub(int i, int j) {
              int result = i - j;
              return result;
          }
      ​
          @Override
          public int mul(int i, int j) {
              int result = i * j;
              return result;
          }
      ​
          @Override
          public int div(int i, int j) {
              int result = i / j;
              return result;
          }
      }
      ​

      不要忘了把这个类放入ioc容器,因为aop功能只针对aop容器内的对象

       

  3. 编写通知类,并且定义通知方法

    现在我们想在 CalculateImpl 类中的四个计算方法的前后都加上日志,那么就需要一个类来写这个重复的代码,这个类就是通知类或者叫增强类,这个类中的方法就是增强方法,里面存放增强代码

    具体的要把这些方法插入到哪个位置,用注解来指定

    • 前置通知 @Before

    • 后置通知 @AfterReturning

    • 异常通知 @AfterThrowing

    • 最后通知 @After

    • 环绕通知 @Around

    知道了哪个增强方法放在核心代码的前或后,还需要配置切点表达式,就是要把这些增强方法插入到哪个类的哪个方法,切点表达式是就是这些注解的 参数 例如 @Before("execution(* com.ztone.impl.* .* (..))")

    execution(* com.ztone.impl.* .* (..))

    这个表达式 execution是固定写法,括号中 第一个星号表示不考虑方法的返回值和修饰符,com.ztone.impl 表示哪个包,第二个星号表示这个包下的哪个类,第三个星号表示这个类下的哪个方法,后面括号中两个点表示不考虑这个方法的返回值

    package com.ztone.advice;
    ​
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    ​
    @Component
    @Aspect
    public class LogAdvice {
        @Before("execution(* com.ztone.impl.*.*(..))")
        public void start(){
            System.out.println("方法开始了");
        }
    ​
        @AfterReturning("execution(* com.ztone.impl.*.*(..))")
        public void after(){
            System.out.println("方法结束了");
        }
    ​
        @AfterThrowing("execution(* com.ztone.impl.*.*(..))")
        public void error(){
            System.out.println("方法出错了");
        }
    }

    增强类也要放入ioc容器 用 @Component

    还要使用 @Aspect 注解 表示该类是一个切面

     

  4. 在配置类中开启aspect注解的支持

    使用 @EnableAspectJAutoProxy 注解

    package com.ztone.config;
    ​
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    ​
    @Configuration
    @ComponentScan("com.ztone")
    @EnableAspectJAutoProxy
    public class JavaConfig {
    }
  5. 测试

    @SpringJUnitConfig(value = JavaConfig.class)
    public class CalculateTest {
    ​
        @Autowired
        private Calculate calculate;
    ​
        @Test
        public void test(){
            int add = calculate.add(1, 1);
            System.out.println(add);
        }
    }

    这里用的是spring 的junit测试,不用自己创建aop容器

    还需要注意的一点是:在声明 CalculateImpl 类时,用的是 接口接值,原因是有了接口底层实现就是用的jdk动态代理,然后根据这个接口创建一个代理类,如果用 目标类去接值的话,对象的类型就不同了会报错

    ioc容器中存储的 也是 创建的代理类,而不是 目标类

    屏幕截图 2024-07-28 120010

     

2.3 在增强方法中获取目标方法信息

  • 获取目标方法的信息(方法名、参数、访问修饰符、所属类的信息),可以在所有增强方法中获取

    需要在要获取信息的增强方法中添加一个参数 JoinPoint

    @Component
    @Aspect
    public class MyAdvice {
        @Before("execution(* com.ztone.impl.*.*(..))")
        public void before(JoinPoint joinPoint){
            //获取类的信息
            String simpleName = joinPoint.getTarget().getClass().getSimpleName();
            //获取目标方法名和修饰符
            String name = joinPoint.getSignature().getName();
            joinPoint.getSignature().getModifiers();
            //获取参数列表
            Object[] args = joinPoint.getArgs();
            
        }
    }
  • 获取目标方法的返回值,只能在后置增强中获取 @AfterReturning

    • 在后置增强方法中添加一个Object 类型的参数

    • 在@AfterReturning 注解中添加 returning 属性,值就是后置增强方法的形参

    @AfterReturning(value = "execution(* com.ztone.impl.*.*(..))",returning = "returning")
    public void afterReturning(Object returning){
        System.out.println(returning);
    }

    返回值就是 后置增强方法的形参

  • 获取目标方法的异常,只能在异常增强中获取 @AfterThrowing

    • 在异常增强方法中添加一个Throwable类型的参数

    • 在 @AfterThrowing 注解中添加 throwing 属性,值是 异常增强方法的形参

    @AfterThrowing(value = "execution(* com.ztone.impl.*.*(..))",throwing = "throwable")
    public void afterThrowing(Throwable throwable){
    ​
    }

 

 

2.4 切面表达式

固定语法:execution(1 2 3.4.5(6))

  1. 访问修饰符

    四个访问修饰符 public、private、default、protect

  2. 方法返回值

    String、int 、void 等

    访问修饰符和返回值 绑定在一起,如果两者都不考虑可以用 * 代替,但是不能只考虑一个而另一个不考虑

  3. 包的位置

    com.ztone.service.impl

    单层模糊:com.ztone.service.* 表示 service包下的所有包

    多层模糊:com..impl 表示com 和 impl 包之间可以有任意层

    但是.. 不能开头,开头只能是包名或 *

  4. 类的名称

    CalculateImpl

    模糊:用 * 代替表示所有类

    部分模糊: *Impl 表示以Impl结尾的类

  5. 方法名称

    和类相同

  6. 形参列表

    没有参数:()

    有具体参数:(String,int)

    模糊参数:(..) 所有参数都行

    部分模糊:(String ..) 第一个参数是String

 

切点表达式的提取和复用:

如果在多个增强方法中的切点表达式相同,那么就可以把切点表达式提取出来,后期修改直接修改一个即可。

使用到的注解是 @Pointcut

在一个方法上使用这个注解,注解的值就是提取的切点表达式,然后再增强方法的注解上调用这个方法即可。

package com.ztone.pointcut;
​
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
​
@Component
public class MyPointcut {
    @Pointcut("execution(* com.ztone.impl.*.*(..))")
    public void myPc(){}
}

这里是在一个类中提取出来,以保证在其他类中也能调用到该方法

 

@Component
@Aspect
public class LogAdvice {
    @Before("com.ztone.pointcut.MyPointcut.myPc()")
    public void start(){
        System.out.println("方法开始了");
    }
​
    @AfterReturning("com.ztone.pointcut.MyPointcut.myPc()")
    public void after(){
        System.out.println("方法结束了");
    }
​
    @AfterThrowing("com.ztone.pointcut.MyPointcut.myPc()")
    public void error(){
        System.out.println("方法出错了");
    }
}

使用的时候就是用类的全限定符.方法名

 

2.5 环绕通知

环绕通知就是将 前置、后置、异常等通知结合起来。

比如给一个目标方法添加事务,可以在前置通知中开启事务,在后置通知中结束事务,在异常通知中进行事务回滚

需要写三个通知方法

如果使用环绕通知,用一个方法就够了,使用的注解是 @Around

该方法接收一个参数 ProceeedingJoinPoint ,这个参数可以获取到目标方法的信息并且可以执行目标方法,使用proceed()

环绕通知方法必须返回一个Object 类型的返回值,这个值就是执行目标方法返回的值

package com.ztone.advice;
​
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
​
@Component
@Aspect
public class TxAdvice {
​
    @Around("com.ztone.pointcut.MyPointcut.myPc()")
    public Object txAd(ProceedingJoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        Object result = null;
​
        try {
            System.out.println("事务开启");
            result = joinPoint.proceed(args);
            System.out.println("事务结束");
        } catch (Throwable e) {
            System.out.println("事务回滚");
            throw new RuntimeException(e);
        }
        return result;
    }
}

joinPoint.proceed(args); 就是执行目标方法

 

2.6 通知的优先级

如果有多个通知,想要某个通知先执行可以用 @Order 注解来设置通知的优先级,注意@Order的值越小优先级越高

优先级高的通知,前置通知会先执行,后置通知最后执行,相当于把优先级低的通知包裹起来

 

 

3.xml方式实现aop

  1. 首先准备一个增强类

    @Component
    public class LogAdvice {
     
        public void start(){
            System.out.println("方法开始了");
        }
      
        public void after(){
            System.out.println("方法结束了");
        }
    ​
        public void error(){
            System.out.println("方法出错了");
        }
    }

    一定要使用@Component 加入到aop容器中

  2. 在xml标签中配置

    • 在最外层用 <aop:config > 标签包裹所有的aop配置

    • 声明切点标签,使用 <aop:pointcut > 相当于 @PointCut 注解

      • id:切点的标识符

      • expression:切点表达式

    • 配置切面 使用 <aop:aspect > 相当于 @Aspect 注解

      • ref :引用的增强类

      • order:切面的优先级

      • 在标签里面使用

        • <aop:before > 表示前置增强

          • method:增强类中的前置增强方法

          • pointcut-ref:引用的切点,或者可以使用 pointcut直接指定切点表达式

        • <aop:after-running > 前两个同理,returning属性指定后置方法的返回值

        • <aop:after-throwing > 前两个同理,throwing属性指定后置方法的返回值

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
    <context:component-scan base-package="com.ztone"/>
    
    <aop:config>
        <aop:pointcut id="myPc" expression="execution(* com.ztone.service.impl.*.*(..))"/>
​
        <aop:aspect ref="txAdvice" order="5">
            <aop:before method="before" pointcut-ref="myPc"/>
            <aop:after-returning method="afterReturning" pointcut-ref="myPc" returning="returning"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="myPc" throwing="t"/>
        </aop:aspect>
    </aop:config>
​
</beans>
posted @ 2024-08-09 08:44  GrowthRoad  阅读(4)  评论(0编辑  收藏  举报