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 @   乔京飞  阅读(10629)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示