Spring AOP 面向切面编程

Spring AOP 面向切面编程

**概述:**AOP(Aspect Oriented Programming)面向切面编程。

**实现:**动态代理,就是面向切面编程最主流的实现。而Spring AOP 是Spring 框架的高级技术,旨在管理bean 对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。

步骤:

1、导入依赖:

在pom.xml里面导入AOP的依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、编写 aop 程序:针对于特定方法根据业务需要,进行编程
@Component  //  表名当前类是配置类 交给IOC容器进行管理
@Aspect  //  表名当前类是 切面类
@Slf4j  //日志 注解
public class TimeAspect{
    @Around("execution(* com.my.service.*.*(..))") //第一个* 返回值任意  第二个* 类名或者接口名 第三个* 代表方法名  表名针对于所有service包下的所有方法
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //proceedingJoinPoint 固定参数 调用原始方法执行
	
     Long begin = System.currentTimeMillis();
     Object object = proceedingJoinPoint.proceed(); //调用原始方法开始执行    object原始方法的返回值
     Long end = System.currentTimeMillis();
     
     log.info(proceedingJoinPoint.getSignature()+"执行耗时:{}ms",end-begin);  //proceedingJoinPoint.getSignature()得到原始方法的签名
     return object;
    }
}

**AOP场景:**记录操作日志、权限控制、事务管理…

**优势:**代码无侵入、减少重复代码、提高开发效率、维护方便…

核心概念:

  • 连接点: JoinPoint , 可以被AOP控制的方法(暗含方法执行时的相关信息)。
  • 通知: Advice , 指那些重复的逻辑,也就是共性功能,(最终体现为一个方法)。
  • 切入点: PointCut , 匹配连接点的条件,通知仅会在切入点方法执行时被调用。
  • 切面: Aspect , 描述通知与切入点的对应关系(通知 + 切入点)
  • **目标对象:**Target , 通知所应用的对象

AOP执行流程:

在方法执行时会创建一个代理对象,代理对象在运行时就会根据切点,加入通知,对方法进行增强

  1. 配置阶段
    • 依赖加载:在项目启动阶段,当添加了 Spring AOP 相关依赖(如spring - boot - starter - aop)后,Spring 容器会加载必要的 AOP 库和配置。
    • 启用 AOP:在 Spring 配置类中(如果是 Spring Boot 项目,很多配置是自动完成的),通过@EnableAspectJAutoProxy注解开启 AOP 功能。这个注解会触发 Spring 对 AspectJ 切面的自动代理机制,它告诉 Spring 去扫描带有@Aspect注解的类,并将它们注册为切面。
    • 组件扫描:Spring 容器会扫描被@Component注解标记的切面类。例如,一个自定义的切面类@Aspect @Component会被扫描并加载到容器中。
  2. 代理对象创建阶段
    • 目标对象识别:当 Spring 容器加载了目标对象(被切面增强的业务对象)后,它会根据配置决定是否需要为这个目标对象创建代理。如果目标对象的方法被切面中的切点所匹配,就会触发代理对象的创建。
    • 代理方式选择:
      • JDK 动态代理:如果目标对象实现了接口,Spring 默认会使用 JDK 动态代理。JDK 动态代理通过反射机制在运行时生成代理类,这个代理类实现了目标对象的接口。例如,假设有一个UserService接口和它的实现类UserServiceImpl,如果要对UserServiceImpl进行 AOP 增强,并且UserServiceImpl实现了UserService接口,那么就会生成一个实现UserService接口的代理类。
      • CGLIB 代理:如果目标对象没有实现接口,Spring 会使用 CGLIB 代理。CGLIB 通过字节码增强技术在运行时生成目标对象的子类作为代理对象。例如,对于一个没有实现任何接口的SomeBusinessClass,Spring 会生成一个SomeBusinessClass的子类作为代理对象。
    • 代理对象生成:无论是 JDK 动态代理还是 CGLIB 代理,生成的代理对象都包含了对目标对象的引用,并且代理对象会拦截对目标对象方法的调用。
  3. 运行时增强阶段(方法调用阶段)
    • 切点匹配:当代理对象拦截到对目标对象方法的调用时,它首先会检查当前调用的方法是否与切面中的切点表达式相匹配。例如,在一个切面中有切点表达式execution(* com.example.service..*.*(..)),如果调用的方法属于com.example.service包及其子包下的任何类的任何方法,就会匹配成功。
    • 通知执行:
      • Before 通知:如果匹配成功且切面中有@Before通知,那么在目标方法执行之前,代理对象会先执行@Before通知中定义的逻辑。例如,在一个日志记录切面中,@Before通知可能会记录方法的开始时间、输入参数等信息。
      • Around 通知:对于@Around通知,它会完全包裹目标方法的执行。代理对象会先执行@Around通知中的前置逻辑,然后决定是否调用目标方法。如果调用目标方法,在目标方法执行完成后,还会执行@Around通知中的后置逻辑。这种通知可以灵活地控制目标方法的执行,比如在事务管理中,@Around通知可以在方法执行前开启事务,根据方法执行情况决定是否提交或回滚事务。
      • After - returning 通知:如果目标方法正常返回,并且有@After - returning通知,代理对象会在目标方法返回后执行该通知中的逻辑。例如,对返回的数据进行格式转换或缓存操作。
      • After - throwing 通知:如果目标方法抛出异常,并且有@After - throwing通知,代理对象会在异常抛出后执行该通知中的逻辑。例如,记录异常信息或者进行异常处理的补偿操作。
      • After 通知:无论目标方法是正常返回还是抛出异常,@After通知中的逻辑都会在最后执行。它可以用于清理资源等操作,比如关闭数据库连接或者释放文件句柄。
    • 目标方法执行:在执行完所有符合条件的通知逻辑(根据通知类型和方法执行情况)后,代理对象会调用目标对象的原始方法。如果通知逻辑中有对目标方法执行的控制(如@Around通知),则按照通知中的逻辑执行目标方法。
    • 返回结果:目标方法执行完成后,其返回返回结果会沿着代理对象和通知的执行路径反向传递。如果有通知对返回结果进行了处理(如@After - returning通知),那么经过处理后的结果会最终返回给方法调用者。

通知类型:

  1. @Around: 环绕通知,此注解标注的通知方法在目标方法前、后都会被执行。
  2. @Before: 前置通知,此注解标注的通知方法在目标方法前被执行
  3. @After: 后置通知 , 此主机标注的通知方法在目标方法后被执行,无论是否有异常都会被执行
  4. @AfterReturning: 返回后通知 , 此注解标注的通知方法在目标方法后被执行,有异常 不会被 执行
  5. @AfterThrowing: 抛出异常后通知 , 此注解标注的通知方法发生异常后执行,没有异常不会执行
// 注意事项:
@Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他同志则不需要考虑目标方法执行
@Around 环绕通知方法的返回值,必须指定为 Object ,来接收原始方法的返回值
@Around 环绕通知调用原始方法,原始方法有异常时,环绕后就 不会 通知

@Order 执行顺序

当一个项目中有多个切面类时,我们可以使用 @Order注解 来指定 切面的执行顺序, 数字越小越先执行,例如 标注@Order(1)的切面类中的通知会在标注@Order(2)的切面类中的通知之前执行(在相同类型的通知和相同连接点的情况下)。

注意:当你不定义时,默认的执行顺序和切面类的类名有关

用 @Order(数字)  加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行;
目标方法后的通知方法,数字小的后执行;

切入点表达式

切入点表达式:描述切入点方法的一种表达式

作用:主要用来决定项目中的哪些方法需要加入通知

常见形式:

  1. 根据方法的签名来匹配: execution(…) ;
  2. 根据注解来匹配: @Annotation(…) ;
切入点表达式—execution

execution 主要根据方法得返回值,包名、类名、方法名、方法参数等信息来匹配;

//语法
execution(访问修饰符 ? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
    
其中带?的表示可以省略的部分
    访问修饰符可以省略
    包名.类名可以省略    不建议省略 !!!
    throws 异常可以省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
    
    
    @Before("execution(public void com.my.service.impl.serviceImpl.delete(java.lang.Integer))")
    public void before(JoinPoint joinPoint){}

可以使用通配符描述切入点

单个 * ,可以通配任意返回值,包名,类名,方法名,任意类型的一个参数,也可以通配包、类、方法名的一部分

execution(*  com.*.service.*.update*(*))  //update开头,后面什么无所谓

符号 … 可以通配任意层级的包、或任意属性、任意个数的参数

execution(*  com.my..service.*(..))  //匹配com.my包及其子包下service目录(包括子目录)中的所有类的所有方法,

例如:全局配置的话可以加上一个切入点表达式

@Pointcut("execution(* com.my.service.impl.serviceImpl.*(..))")
public void pointCut(){}   //定义为private的话。外部类就不可以使用了,只有当前类可以使用



通知引用时可以直接引用
例如  @Around("pointCut()")
切入点表达式—@annotation

@annotation 切入点表达式,用于匹配标识有特定注解的方法

@annotation(com.my.aop.MyAnnotation)  //作用在有MyAnnotation注解的方法上

自定义注解步骤:

  1. 创建一个annotation 接口
  2. 类上面加上 @Retention(RetentionPolicy.RUNTIME) 表明这个注解在运行时生效 , @Target(ElementType.METHOD) 表名当前注解作用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public  @interface MyAnnotation{}

连接点

在Spring 中 用JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名,方法名,方法参数等。

  • 对于@Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
  • 对于其他四种通知来说,获取连接点的信息只能使用 JoinPoint ,他是 ProceedingJoinPoint 的父类型

ProceedingJoinPoint (Around 专属连接点)

@Around("execution(*  com.my.service.impl.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    String calssName = joinPoint.getTarget().getClass().getName();  //获取目标方法 类名
    Signature  signature = joinPoint.getSignature();  //获取目标方法  签名
    String  methodName = joinPoint.getSignature().getName();  //获取目标方法名
    Object[] args = joinPoint.getArgs();  //获取目标方法运行参数
    Object res = joinPoint.proceed();  //执行原始方法,返回方法执行结果(环绕通知)
    return res;
    
}

JoinPoint (其余四种通知类型)

@Before("execution(*  com.my.service.*(..))")
public void before(JoinPoint  point){
    String className = point.getTarget().getClass().getName();  //获取目标方法 类名
    Signature  signature = point。getSignature();  //获取目标方法 签名
    String methodName = point.getSignature().getName();  //获取目标方法名
    Object[] args = point.getArgs();  //获取目标方法运行参数
}

案例:

将增、删、改相关接口的操作日志记录到数据库表中

操作日志:包含操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。

思路分析:需要对所有业务类中的增、删、改方法添加统一的功能,使用 AOP 最方便,@Around 环绕通知

步骤

准备:

  • 在案例工程中引入 AOP 的起步依赖

编码:

  1. 自定义注解 @MyLog 仅仅做标识作用
  2. 定义切面类,完成记录操作日志相关的逻辑
public class Log{
//需要记录如下日志属性
Integer operateUser;  // 操作人ID
LocalDateTime operateTime;  //操作时间
String calssName;  //操作类名
String methodName;  //操作方法名
String methodParams; //操作方法参数
String returnValue; //操作方法返回值
Long costTime; //操作耗时
}

下面在切面类中获取以上属性

@Around("@annotation(com.my.anno.MyLog)")
public Object recordLog(ProceedingJoinPoint  point) throws ThrowAble{
 
    Log log = new Log();
    //记录操作人的ID-当前登录人的id
    log.setOperateUser(当前登陆人);
    //操作时间
    log.setoperateTime(new Date());
    //操作类名
    log.setClassName(point.getTarget().getClass().getName());
    //操作方法名
    log.setMethodName(point.getSignature().getName());
    //操作方法参数
    Object[] args =  point.getArgs();
    String methodParams = Arrays.toString(args);  //将数组转化成为字符串
    log.setMethodParams(methodParams);
    //操作方法返回值
        //记录耗时  原始方法开始之前记录开始时间
        Long begin = System.currentTimeMillis();
    Object result = point.proceed();
    	Long end = System.currentTimeMillis();   //结束时间
    String resultValue = JSONObject.toJSONString(result);  //fastJSON包下的方法   将结果转成JSON字符串
    //操作耗时
    log.setCostTime(end - begin);
    
    
    mapper.insert(log);
    return resultValue;
}

之后在增删改的controller 接口方法上面加上 @MyLog 注解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值