Spring 采用纯注解实现 AOP 切面增强

Spring 的 Aop 切面编程的主要用途是:在不改变相关方法原有代码的情况下,实现对相关方法的功能增强,其本质就是采用动态代理技术来实现的。有关 Spring 的 Aop 底层原理所采用的动态代理技术,我将在下篇博客进行介绍。

本篇博客主要介绍 Spring 如何采用纯注解的方式,对相关方法进行 Aop 扩展增强。有关 Spring 的 Aop 的相关术语,这里不进行详细介绍,网上资料很多,限于篇幅有限,这里仅仅介绍如果进行快速搭建和使用,在本篇博客的最后会提供 Demo 的源代码。


一、搭建工程

新建一个 maven 项目,导入相关 jar 包,我导入的都是最新版本的 jar 包,内容如下:

有关具体的 jar 包地址,可以在 https://mvnrepository.com 上进行查询。

<dependencies>
    <!--Spring 的基础核心 jar 包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.17</version>
    </dependency>
    <!--第三方 Aop 切面编程 jar 包-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.8</version>
    </dependency>

    <!--
    由于本 Demo 需要使用 junit 进行单元测试,
    因此需要导入 junit 和 spring-test 这两个 jar 包,
    用于 spring 整合 junit 单元测试
    -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.17</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

打开右侧的 Maven 窗口,刷新一下,这样 Maven 会自动下载所需的 jar 包文件。

搭建好的项目工程整体目录比较简单,具体如下图所示:

image

项目工程结构简单介绍:

aop 包下存放 Aop 的方法类,这些方法用于对相关 Service 类中的方法增强

service 包下存放的是业务处理实现类

AopApp 这个是该 demo 的 main 入口所在类,使用 Spring 访问数据库,验证搭建成果

test 目录下 EmployeeAopTest 这个是测试方法类,里面编写测试 Service 接口的方法,验证 Aop 的执行情况


二、Service 业务方法的细节

本 Demo 中只有一个接口 EmployeeService 和一个实现类 EmployeeServiceImpl ,里面的内容非常简单。

需要注意的是:
Service 的业务方法类最好要实现接口,因为在实际开发场景中,Aop 切面配置的位置,最佳方案是配置在接口上,而不是配置到具体的实现类上。这样的好处在于后续如果接口有其它的实现类,Aop 切面增强功能也在接口的其它实现类上自动生效。

package com.jobs.service;

public interface EmployeeService {

    //不会报错的方法
    Integer normalTest(Integer xxx, Integer yyy);

    //会出现异常的方法
    void errorTest(Integer zzz);

    //测试方法
    String aopTest(String ppp, String qqq);
}
package com.jobs.service.impl;

import com.jobs.service.EmployeeService;
import org.springframework.stereotype.Service;

//通过 @Service 注解将该实现类的实例化对象,装载到 Spring 容器中,方便后续注入使用
@Service("empService")
public class EmployeeServiceImpl implements EmployeeService {

    //不会报错的方法,测试 Aop 方法执行
    @Override
    public Integer normalTest(Integer xxx, Integer yyy) {
        Integer result = xxx + yyy;
        System.out.println("normalTest 方法执行了,计算结果为:" + result);
        return result;
    }

    //会出现异常的方法,测试 Aop 方法执行
    @Override
    public void errorTest(Integer zzz) {
        System.out.println("errorTest 方法执行了,必然会抛出异常...");
        //这里必然会抛出异常
        Integer result = zzz / 0;
    }

    //测试方法,测试 Aop 方法执行
    @Override
    public String aopTest(String ppp, String qqq) {
        String result = ppp + "-------" + qqq;
        System.out.println("aopTest 方法执行了,结果为:" + result);
        return result;
    }
}

本 Demo 的业务方法,只有 3 个,实现内容也非常简单。

normalTest 方法用来展示 Aop 在【被增强的方法】正常不报错情况下的执行顺序。

errorTest 方法用于展示 Aop 在【被增强的方法】出错的情况下的执行顺序。

aopTest 方法用于在本 Demo 的启动类 AopApp 中,验证本 Demo 的 Aop 功能是否搭建成功。


三、Spring 纯注解配置 Aop 细节

编写 Aop 功能的方法类 AopAdvice ,该类中的方法用于在不改变 Service 中方法的情况下,强行介入到具体方法的执行过程中,实现对 Service 方法的增强。AopAdvice 类的具体内容如下:

package com.jobs.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

//使用 @Component 注解将 AopAdvice 的实例化对象,装载到 Spring 容器中
//使用 @Aspect 注解表示该类是切面类,里面提供了切面增强的方法
@Component
@Aspect
public class AopAdvice {

    //使用 @Pointcut 注解,设置要为哪些类的哪些方法进行切面方法增强
    //这里设置的是,针对 com.jobs.service 包下的所有 Service 类中的方法进行增强
    @Pointcut("execution(* com.jobs.service.*Service.*(..))")
    public void pt() {
    }

    //使用 @Before 注解,表示在【被增强的方法】之前执行下面的代码
    //该注解修饰的方法,可以有一个 JoinPoint 参数,用于获取【被增强的方法】的相关信息
    @Before("pt()")
    public void before(JoinPoint jp) {
        System.out.println("前置 before 方法执行了...");

        //获取所执行方法的签名信息
        Signature signature = jp.getSignature();
        System.out.println("\t 方法签名为:" + signature);
        //通过签名获取执行接口名称或类名
        String interfaceName = signature.getDeclaringTypeName();
        System.out.println("\t 方法所属接口或所属类名为:" + interfaceName);
        //通过签名获取执行方法名称
        String methodName = signature.getName();
        System.out.println("\t 方法名为:" + methodName);
        //获取方法的所传入的参数值
        Object[] args = jp.getArgs();
        for (Object obj : args) {
            System.out.println("\t 方法传入的参数值:" + obj);
        }
    }

    //使用 @After 注解,表示在【被增强的方法】之后执行下面的代码
    //无论【被增强的方法】在执行过程中,是否出错抛出异常,都会执行下面的代码
    //该注解修饰的方法,可以有一个 JoinPoint 参数,用于获取【被增强的方法】的相关信息
    @After("pt()")
    public void after(JoinPoint jp) {
        //后置方法,通过 jp 参数,也能够获取到所执行方法的相关信息
        System.out.println("后置 after 方法执行了...");
    }

    //使用 @AfterReturning 注解,表示在【被增强的方法】返回结果后,执行下面的代码
    //如果【被增强的方法】在执行过程中,是否出错抛出异常,将不会执行下面的代码
    //该注解修饰的方法,可以有一个 Object 参数,用于获取【被增强的方法】执行后的返回值
    @AfterReturning(pointcut = "pt()", returning = "ret")
    public void afterReturing(Object ret) {
        //通过参数,可以获取到方法执行的结果
        System.out.println("返回结果后 afterReturing 执行了,返回结果为:" + ret);
    }

    //使用 @AfterThrowing 注解,表示【被增强的方法】出错抛异常后,执行下面的代码
    //如果【被增强的方法】没有出错抛异常,将不会执行下面的代码
    //该注解修饰的方法,可以有一个 Throwable 参数,用于获取【被增强的方法】出错时的异常对象
    @AfterThrowing(pointcut = "pt()", throwing = "t")
    public void afterThrowing(Throwable t) {
        //通过参数,可以获取到异常对象,从而获取异常信息
        System.out.println("抛出异常后 afterThrowing 执行了,异常信息为:" + t.getMessage());
    }

    //使用 @Around 注解,可以在【被增强的方法】执行前后,增加相关代码
    // @Around 注解是我们以后用的最多的注解,因为其功能强大灵活。
    //该注解修饰的方法,可以有一个 ProceedingJoinPoint 参数,用于获取【被增强的方法】的相关信息
    //可以通过 ProceedingJoinPoint 参数的 proceed() 来执行【被增强的方法】
    //需要注意的是:如果这里没有对【被增强的方法】进行 try catch 包裹处理异常,
    //那么【被增强的方法】如果出现异常,将不会执行后面的代码,这样后置增强代码就不会执行了
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("环绕前 around before 执行了...");
        //在环绕方法中,可以获取到方法执行的结果
        Object ret = pjp.proceed();
        System.out.println("环绕后 around after 执行了...");
        return ret;
    }
}

下面我们创建一个 Spring 配置类 SpringConfig 并启用 Aop 功能,具体内容如下:

package com.jobs.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

// @Configuration 注解,表示该类是 Spring 的配置类
// @ComponentScan 注解,表示扫描具体包及其子包下的所有注解,将相关对象实例化并装载到 Spring 容器中
// @EnableAspectJAutoProxy 这个就是启用 Aop 功能的注解
@Configuration
@ComponentScan("com.jobs")
@EnableAspectJAutoProxy
public class SpringConfig {
}

下面我们创建一个拥有 main 方法的入口类,调用 EmployeeService 接口中的 aopTest 方法,验证搭建成果。

package com.jobs;

import com.jobs.config.SpringConfig;
import com.jobs.service.EmployeeService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopApp {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        EmployeeService empService = (EmployeeService) ctx.getBean("empService");
        empService.aopTest("侯胖胖","任肥肥");
    }
}

因为 aopTest 方法内部不会出异常,所以不会执行 Aop 的切面类 AopAdvice 下 @AfterThrowing 注解修饰的方法,其它注解的方法都会正常执行,从而实现对 aopTest 的增强。具体如下图所示:

image

本 Demo 把 Aop 的所有注解方法都用上了,目的在于演示 Aop 各种注解增强后的方法,具体的执行顺序。

从上图的执行结果可以发现:

@Around 注解修饰的方法(环绕通知),
其内部【被增强方法之前的代码】在【最前面】执行,【被增强方法之后的代码】在【最后面】执行。
(上图中的红色框输出的内容)

@Before 注解修饰的方法(前置通知),
其实际执行顺序仅次于【环绕通知】的【被增强方法之前的代码】。
(上图中的蓝色框输出的内容)

绿色框内输出的内容,是【被增强方法】执行时输出的,其实际执行顺序仅次于【前置通知】。
(上图中的绿色框输出的内容)

@AfterReturning 注解修饰的方法(返回结果通知),其执行顺序仅次于【被增强的方法】。
(上图中的紫色框输出的内容)

@After 注解修饰的方法(后置通知),在【返回结果通知】之后执行。
(上图中的黑色框输出的内容)


四、测试异常方法 Aop 执行顺序

前面的博客已经介绍过 Spring 如何整合 junit ,这里就不再详细介绍了,下面列出编写的测试类内容:

package com.jobs;

import com.jobs.config.SpringConfig;
import com.jobs.service.EmployeeService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class EmployeeAopTest {

    @Autowired
    private EmployeeService employeeService;

    @Test
    public void normalTest() {
        int result = employeeService.normalTest(120, 130);
        //Assert.assertEquals 主要用来验证【执行结果】与【所期望的结果】是否一致
        Assert.assertEquals(250, result);
    }

    @Test
    public void errorTest() {
        try {
            //此方法执行会抛出异常,从而导致 AfterThrowing 执行
            employeeService.errorTest(38);
        } catch (Exception ex) {
            System.out.println("单元测试方法获得的异常信息:" + ex.getMessage());
        }
    }

    @Test
    public void aopTest() {
        String result = employeeService.aopTest("候肥肥", "任胖胖");
        //Assert.assertEquals 主要用来验证【执行结果】与【所期望的结果】是否一致
        Assert.assertEquals("候肥肥-------任胖胖", result);
    }
}

有关 normalTest 方法测试的 Aop 执行顺序,跟 aopTest 方法的测试结果一样,因为都是没有报错的正常方法。

下面列出 errorTest 方法测试的 Aop 执行顺序,employeeService.errorTest 方法会抛异常,其执行结果如下:

image

从上图中可以发现,当【被增强的方法】在执行过程中出错抛出异常,Aop 的执行顺序为:

@Around 注解修饰的方法(环绕通知),
其内部【被增强方法之前的代码】在【最前面】执行,【被增强方法之后的代码】不会被执行。
(上图中的红色框输出的内容)

@Before 注解修饰的方法(前置通知),
其实际执行顺序仅次于【环绕通知】的【被增强方法之前的代码】。
(上图中的蓝色框输出的内容)

绿色框内输出的内容,是【被增强方法】执行时输出的,其实际执行顺序仅次于【前置通知】。
(上图中的绿色框输出的内容)

@AfterReturning 注解修饰的方法(返回结果通知),
当【被增强的方法】在执行过程中出错抛出异常时,不会被执行。

@AfterThrowing 注解修饰的方法(异常通知),
当【被增强的方法】在执行过程中出错抛出异常时,会被执行。
(上图中的紫色框输出的内容)

@After 注解修饰的方法(后置通知),
无论【被增强的方法】在执行过程中是否出错抛出异常时,都会执行。
(上图中的黑色框输出的内容)

上图中黄色框中的内容,是单元测试方法打印出来的。



到此为止,已经完成了 Spring 有关 Aop 切面编程快速搭建和使用的介绍,大家可以根据实际情况在工作中进行参考使用。限于篇幅有限,这里没有介绍具体的 Aop 概念和细节。在实际开发场景中,使用最多的是 Aop 的环绕通知,其它 Aop 切面通知很少使用。

最后提供本 Demo 的源代码:https://files.cnblogs.com/files/blogs/699532/spring_aop_junit.zip



posted @ 2022-03-22 00:18  乔京飞  阅读(9511)  评论(0编辑  收藏  举报