Spring注解开发05--------注解实现aop

什么是aop?

AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。
从《Spring实战(第4版)》图书中扒了一张图:

aop原理图

从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

总而言之:AOP是指在程序的运行期间动态地将某段代码切入到指定方法、指定位置进行运行的编程方式。AOP的底层是通过动态代理实现的。

案例演示

一、导入相关依赖

<!-- Spring核心依赖 -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>5.3.5</version>
</dependency>

<!-- aspect相关依赖 -->
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.9.7</version>
</dependency>

 <!-- junit,单元测试类 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

在这里有三点说明:

  1. Spring5.0之后,许多核心核心依赖都包含在spring-webmvc当中,所以导入spring-webmvc这个依赖,Spring的大部分功能就可以使用了。
  2. 要想使用Spring-Aop的功能,我们必须aspectjweaver这个依赖!
  3. Junit我们应该都很熟悉了,是我们的单元测试类依赖。

二、我们自定义一个业务逻辑类(MathCalculate)。

这里我们就简单的定义一个除法的业务逻辑。

package com.xdw.aop;

public class MathCalculate {
    public int div(int i, int j) {
        System.out.println("MathCalculate...div...");
        return i/j;
    }
}

下面我们的要求是在业务逻辑进行的时候进行日志的输出(方法之前、方法之后以及方法抛出异常。。。)

三、定义我们的切面类

在编写我们通知类之前,有几个aop的注解我们需要了解以下:

  1. @Before前置通知,在目标方法执行之前运行
  2. @After后置通知,在目标方法执行之后运行,不管方法是正常结束还是异常结束都会执行
  3. @AfterReturning返回通知,在目标方法正常返回之后执行
  4. @AfterThrowing异常通知,在目标方法运行出现异常时执行
  5. @Around环绕通知,动态代理,我们可以直接手动推进目标方法运行(joinPoint.procced()
  6. @Aspect放在切面类上,告诉Spring这是一个切面类。

下面我不会一下就把完整切面类代码贴出来,会一步一步对我们的代码进行完善!

试想一下,一开始你的代码是不是这样:

package com.xdw.aop;

import org.aspectj.lang.annotation.*;

// @Aspect 告诉Spring这是一个切面类
@Aspect
public class LogAspect {

    @Before("execution(* com.xdw.aop..*.*(..))")
    public void logStart() {
        System.out.println("方法执行之前, 执行参数{}");
    }


    @After("execution(* com.xdw.aop..*.*(..))")
    public void logEnd() {
        System.out.println("方法执行结束");
    }

    @AfterReturning("execution(* com.xdw.aop..*.*(..))")
    public void logRetur() {
        System.out.println("方法执行结束,返回值是{}");
    }

    @AfterThrowing("execution(* com.xdw.aop..*.*(..))")
    public void logException() {
        System.out.println("方法执行异常,错误信息为{}");
    }
}

execution(* com.xdw.aop..*.*(..))"是一个切入点表达式,在上述代码中反复出现,十分麻烦。作为程序员,偷懒是我们的必备技能,我们可以将上述代码稍稍优化一下:

  1. 第一步,我们新建一个空方法,方法名随意指定,使用@Pointcut注解来修饰,告诉Spring这是一个切点

    @Pointcut("execution(* com.xdw.aop..*.*(..))")
    public void pointCut() {}
    
  2. 在我们的通知标签上引入该方法

    // 本类引用,直接使用方法名即可
    @Before("pointCut()")
    public void logStart() {
        System.out.println("方法执行之前, 执行参数{}");
    }
    //  外部类引用,必须使用全类名 + 方法名
    @After("com.xdw.aop.LogAspect.pointCut()")
    public void logEnd() {
        System.out.println("方法执行结束");
    }
    

这里有两点需要说明: a.本类引用,直接使用方法名即可 b.外部类引用,必须使用全类名 + 方法名

最终,我们修改之后的方法如下:

package com.xdw.aop;

import org.aspectj.lang.annotation.*;

// @Aspect 告诉Spring这是一个切面类
@Aspect
public class LogAspect {

    @Pointcut("execution(* com.xdw.aop..*.*(..))")
    public void pointCut() {}


    @Before("pointCut()")
    public void logStart() {
        System.out.println("方法执行之前, 执行参数{}");
    }


    @After("com.xdw.aop.LogAspect.pointCut()")
    public void logEnd() {
        System.out.println("方法执行结束");
    }

    @AfterReturning("pointCut()")
    public void logRetur() {
        System.out.println("方法执行结束,返回值是{}");
    }

    @AfterThrowing("pointCut()")
    public void logException() {
        System.out.println("方法执行异常,错误信息为{}");
    }

}

四、将我们的业务逻辑类与切面类注入IOC容器中

package com.xdw.config;

import com.xdw.aop.LogAspect;
import com.xdw.aop.MathCalculate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;


@EnableAspectJAutoProxy   // 开启注解自动注入
@Configuration
public class MainConfigOfAop {

    @Bean
    public MathCalculate mathCalculate() {
        return new MathCalculate();
    }

    @Bean
    public LogAspect logAspect() {
        return new LogAspect();
    }
}

在这里,我们一定要在我们的配置类上添加@EnableAspectJAutoProxy注解,开启基于注解的aop模式,否则aop不会生效。在未来,我们会看到很多的@Enablexxxx注解,它们的作用都是开启某一项功能,来替换我们以前的那些繁琐的配置文件。

五、测试

编写测试方法测试。

@Test
public void test01() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfAop.class);
    MathCalculate mathCalculate = applicationContext.getBean(MathCalculate.class);
    mathCalculate.div(3,1);
}

运行结果如下:
运行结果

从运行结果中,我们可以看到,我们的执行参数并没有被打印出来,因为在我们的代码中,根本没有获取参数!我们应该如何在代码中获取参数信息呢?

六、完善切面类,获取方法名及参数

  1. 我们可以在切面方法中添加JoinPoint类型的参数,来获取方法名称、参数等信息

    @Before("pointCut()")
    public void logStart(JoinPoint joinPoint) {
        System.out.println(joinPoint.getSignature().getName() + "方法执行之前, 执行参数{"+ Arrays.asList(joinPoint.getArgs()) + "}");
    }
    

    joinPoint.getSignature().getName()获取方法名称,joinPoint.getArgs()获取方法参数!

  2. 我们可以在@AfterReturing注解中,设置returning属性,来指向我们自定义的接收结果的参数

    @AfterReturning(value="pointCut()", returning = "result")
    public void logReturn(JoinPoint joinPoint, Object result) {
        System.out.println(joinPoint.getSignature().getName() + "方法执行结束,返回值是{"+ result +"}");
    }
    
  3. 我们可以在@AfterThrowing注解中,设置throwing属性,来指定我们自定义的接收结果的参数

    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void logException(JoinPoint  joinPoint, Exception e) {
        System.out.println(joinPoint.getSignature().getName() + "方法执行异常,错误信息为{" + e.getMessage() + "}");
    }
    

修改之后的切面类如下:

package com.xdw.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;

import java.util.Arrays;

// @Aspect 告诉Spring这是一个切面类
@Aspect
public class LogAspect {

    @Pointcut("execution(* com.xdw.aop..*.*(..))")
    public void pointCut() {}


    @Before("pointCut()")
    public void logStart(JoinPoint joinPoint) {
        System.out.println(joinPoint.getSignature().getName() + "方法执行之前, 执行参数{"+ Arrays.asList(joinPoint.getArgs()) + "}");
    }


    @After("com.xdw.aop.LogAspect.pointCut()")
    public void logEnd(JoinPoint joinPoint) {
        System.out.println(joinPoint.getSignature().getName() + "方法执行结束");
    }

    @AfterReturning(value="pointCut()", returning = "result")
    public void logReturn(JoinPoint joinPoint, Object result) {
        System.out.println(joinPoint.getSignature().getName() + "方法执行结束,返回值是{"+ result +"}");
    }

    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void logException(JoinPoint  joinPoint, Exception e) {
        System.out.println(joinPoint.getSignature().getName() + "方法执行异常,错误信息为{" + e.getMessage() + "}");
    }

}

JoinPoint参数一定要放在参数列表的第一位,否则Spring是无法识别的,就会报错。

七、再次测试

我们先正常测试:
正常测试

我们再将除数设为0:
执行失败

发现参数、返回值、错误信息都能正常打印出来了。

至此AOP测试搭建成功!

小结

上述测试环境在搭建时,虽然繁琐,但是归纳总结起来,其实只有三步:

  1. 将业务逻辑类与切面类都注入至容器中,并告诉Spring哪个是切面类(@Aspect
  2. 在切面类上的每个切面方法上标注通知注解,告诉Spring该方法何时何地运行(切入点表达式)
  3. 开启基于注解的Aop模式 @EnableAspectJAutoProxy

切入点(execution)表达式说明:
我们以我们代买中的表达式execution(* com.xdw.aop..*.*(..)) 为例,整个表达式可以分为五个部分:

  1. execution(): 表达式主体。
  2. 第一个*号:表示返回类型,*号表示所有的类型。
  3. 包名:表示需要拦截的包名,后面的两个句点..表示当前包和当前包下的所有子包。
  4. 第二个*号:表示类名,*号表示所有的类。
  5. *(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点..表示任何参数。
posted @ 2022-02-14 16:44  卧龙戏公瑾  阅读(149)  评论(0编辑  收藏  举报