Spring Boot AOP - 面向切面编程

简介: Spring Boot AOP - 面向切面编程

AOP,即面向切面编程,其核心思想就是把业务分为核心业务非核心业务两大部分。例如一个论坛系统,用户登录、发帖等等这是核心功能,而日志统计等等这些就是非核心功能。

在Spring Boot AOP中,非核心业务功能被定义为切面,核心和非核心功能都开发完成之后,再将两者编织在一起,这就是AOP。

AOP的目的就是将那些与业务无关,却需要被业务所用的逻辑单独封装,以减少重复代码,减低模块之间耦合度,利于未来系统拓展和维护。

今天,我将做一个简单的打印用户信息的程序,即后端接受POST请求中的User对象将其打印这样一个逻辑,在这个上面实现AOP。

首先放上用户打印服务逻辑的方法代码:

Service层:

packagecom.example.springbootaop.service.impl;
importcom.example.springbootaop.model.User;
importcom.example.springbootaop.service.UserService;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.stereotype.Component;
@ComponentpublicclassUserServiceImplimplementsUserService {
/*** 使用Logger*/privateLoggerlogger=LoggerFactory.getLogger(this.getClass());
@OverridepublicvoidprintUserInfo(Useruser) {
logger.info("用户id:"+user.getId());
logger.info("用户名:"+user.getUsername());
logger.info("用户昵称:"+user.getNickname());
   }
}

Control层:

packagecom.example.springbootaop.api;
importcom.example.springbootaop.model.User;
importcom.example.springbootaop.service.UserService;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.web.bind.annotation.PostMapping;
importorg.springframework.web.bind.annotation.RequestBody;
importorg.springframework.web.bind.annotation.RestController;
@RestControllerpublicclassUserAPI {
@AutowiredprivateUserServiceuserService;
@PostMapping("/user")
publicStringprintUser(@RequestBodyUseruser) {
userService.printUserInfo(user);
return"已完成打印!";
   }
}

User是一个简单的封装类,这里就不展示了,文章末尾会给出整个示例程序代码地址。

1,AOP到底有什么不同

这里只是实现一个简单的逻辑,打印用户信息,这就是我们今天的核心功能。

如果这个时候我们要给它加上非核心功能:在打印之前和打印之后分别执行一个方法,如果你不知道AOP,你可能会把Control层的方法改成如下形式:

@PostMapping("/user")
publicStringprintUser(@RequestBodyUseruser) {
// 执行核心业务之前doBefore();
// 执行核心业务userService.printUserInfo(user);
// 执行核心业务之后doAfter();
   ...
return"已完成打印!";
}

如果说方法多了,业务多了,非核心业务的逻辑一变,所有Controller的全部方法都要改动,非常麻烦,且代码冗余,耦合度高。

这时,就需要AOP来解决这个问题。

AOP只需要我们单独定义一个切面,在里面写好非核心业务的逻辑,即可将其织入核心功能中去,无需我们再改动Service层或者Control层。

2,AOP中的编程术语和常用注解

在学习AOP之前,我们还是需要了解一下常用术语:

  • 切面:非核心业务功能就被定义为切面。比如一个系统的日志功能,它贯穿整个核心业务的逻辑,因此叫做切面
  • 切入点:在哪些类的哪些方法上面切入
  • 通知:在方法执行前/后或者执行前后做什么
  • 前置通知:在被代理方法之前执行
  • 后置通知:在被代理方法之后执行
  • 返回通知:被代理方法正常返回之后执行
  • 异常通知:被代理方法抛出异常时执行
  • 环绕通知:是AOP中强大、灵活的通知,集成前置和后置通知
  • 切面:在什么时机、什么地方做什么(切入点+通知)
  • 织入:把切面加入对象,是生成代理对象并将切面放入到流程中的过程(简而言之,就是把切面逻辑加入到核心业务逻辑的过程)

在Spring Boot中,我们使用@AspectJ注解开发AOP,首先需要在pom.xml中引入如下依赖:

<!-- Spring Boot AOP --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>

然后就可以进行AOP开发了!

这里先给出常用注解,大家联系着下面的例子看就可以了:

  • @Pointcut 定义切点
  • @Before 前置通知
  • @After 后置通知
  • @AfterReturning 返回通知
  • @AfterThrowing 异常通知
  • @Around 环绕通知

3,定义切面

新建aop包,在里面新建类作为我们的切面类,先放出切面类代码:

packagecom.example.springbootaop.aop;
importorg.aspectj.lang.annotation.*;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.stereotype.Component;
@Aspect@ComponentpublicclassLogAspect {
/*** 日志打印*/privateLoggerlogger=LoggerFactory.getLogger(this.getClass());
/*** 使用Pointcut给这个方法定义切点,即UserService中全部方法均为切点。<br>* 这里在这个log方法上面定义切点,然后就只需在下面的Before、After等等注解中填写这个切点方法"log()"即可设置好各个通知的切入位置。* 其中:* <ul>*     <li>execution:代表方法被执行时触发</li>*     <li>*:代表任意返回值的方法</li>*     <li>com.example.springbootaop.service.impl.UserServiceImpl:这个类的全限定名</li>*     <li>(..):表示任意的参数</li>* </ul>*/@Pointcut("execution(* com.example.springbootaop.service.impl.UserServiceImpl.*(..))")
publicvoidlog() {
   }
/*** 前置通知:在被代理方法之前调用*/@Before("log()")
publicvoiddoBefore() {
logger.warn("调用方法之前:");
logger.warn("接收到请求!");
   }
/*** 后置通知:在被代理方法之后调用*/@After("log()")
publicvoiddoAfter() {
logger.warn("调用方法之后:");
logger.warn("打印请求内容完成!");
   }
/*** 返回通知:被代理方法正常返回之后调用*/@AfterReturning("log()")
publicvoiddoReturning() {
logger.warn("方法正常返回之后:");
logger.warn("完成返回内容!");
   }
/*** 异常通知:被代理方法抛出异常时调用*/@AfterThrowing("log()")
publicvoiddoThrowing() {
logger.error("方法抛出异常!");
   }
}

切面类需要打上@Aspect注解表示这是一个切面类,然后不要忘了打上@Component注解。

我们逐步来看。

首先是定义切点,只需定义一个空方法,在上面使用@Pointcut注解即可,注解里面内容含义如下:

  • execution 代表方法被执行时触发
  • * 代表任意返回值的方法
  • com.example.springbootaop.service.impl.UserServiceImpl 被织入类的全限定名
  • (..) 表示任意的参数

定义完切点之后,就可以定义各个通知的方法逻辑了,这些就是我们的切面逻辑,也就是非核心业务的逻辑。

上面在doBefore方法上面,我们使用了@Before注解,这样就标明了doBefore方法是前置通知逻辑,会在被织入方法之前执行。我们把log方法定义为切入点,然后下面各个通知注解中,填写这个切入点方法名称即可。

我们也并不需要定义所有的通知,只需定义需要的即可。

其实,如果不定义上面的切入点方法log@Pointcut,你仍然可以把execution表达式直接写在各个通知的注解里面,例如:

/*** 前置通知:在被代理方法之前调用*/@Before("execution(* com.example.springbootaop.service.impl.UserServiceImpl.*(..))")
publicvoiddoBefore(JoinPointjoinPoint) {
logger.warn("调用方法之前:"); logger.warn("接收到请求!");
}

但是大多数情况并不推荐这样,这种写法较为复杂。

我们发送一个请求测试一下:

网络异常,图片无法展示
|

通过这个,我们也可以发现各个通知的执行顺序:

Before -> AfterReturning -> After

4,环绕通知

环绕通知是AOP中最强大的通知,可以同时实现前置和后置通知,不过它的可控性没那么强,如果不用大量改变业务逻辑,一般不需要用到它。我们在上述切面加入下列环绕通知方法:

/*** 环绕通知*/@Around("log()")
publicvoidaround(ProceedingJoinPointjoinPoint) {
logger.warn("执行环绕通知之前:");
try {
joinPoint.proceed();
   } catch (Throwablee) {
e.printStackTrace();
   }
logger.warn("执行环绕通知之后");
}

通知方法中有一个ProceedingJoinPoint类型参数,通过其proceed方法来调用原方法。需要注意的是环绕通知是会覆盖原方法逻辑的,如果上面代码不执行joinPoint.proceed();这一句,就不会执行原被织入方法。因此环绕通知一定要调用参数的proceed方法,这是通过反射实现对被织入方法调用。

再次测试如下:

网络异常,图片无法展示
|

5,通知方法传参

上面每个通知方法是没有参数的。其实,通知方法是可以接受被织入方法的参数的。我们上述被织入方法参数就是一个User对象,因此通知方法也可以加上这个参数接受。我们改变前置通知方法如下:

/*** 前置通知:在被代理方法之前调用*/@Before("log() && args(user)")
publicvoiddoBefore(Useruser) {
logger.warn("调用方法之前:");
logger.warn("接收到请求!");
logger.warn("得到用户id:"+user.getId());
}

测试结果:

网络异常,图片无法展示
|

可见在注解后面加一个args选项,里面写参数名即可。

需要注意的是,通知方法的参数必须和被织入方法参数一一对应例如:

/*** 被织入方法* /public void print(User user, int num) {...}/*** 通知* /@Before("log() && args(user, num)")public void doBefore(User user, int num) {...}

6,总结

AOP其实使用起来是个很方便的东西,大大降低了相关功能之间的耦合度,使得整个系统井井有条。

定义切面,然后定义切点,再实现切面逻辑(各个通知方法),就完成了一个简单的切面。

示例程序仓库地址

相关文章
|
8天前
|
运维 Java 程序员
Spring5深入浅出篇:基于注解实现的AOP
# Spring5 AOP 深入理解:注解实现 本文介绍了基于注解的AOP编程步骤,包括原始对象、额外功能、切点和组装切面。步骤1-3旨在构建切面,与传统AOP相似。示例代码展示了如何使用`@Around`定义切面和执行逻辑。配置中,通过`@Aspect`和`@Around`注解定义切点,并在Spring配置中启用AOP自动代理。 进一步讨论了切点复用,避免重复代码以提高代码维护性。通过`@Pointcut`定义通用切点表达式,然后在多个通知中引用。此外,解释了AOP底层实现的两种动态代理方式:JDK动态代理和Cglib字节码增强,默认使用JDK,可通过配置切换到Cglib
|
2天前
|
XML Java 数据格式
Spring高手之路18——从XML配置角度理解Spring AOP
本文是全面解析面向切面编程的实践指南。通过深入讲解切面、连接点、通知等关键概念,以及通过XML配置实现Spring AOP的步骤。
21 6
Spring高手之路18——从XML配置角度理解Spring AOP
|
8天前
|
XML Java 数据格式
Spring使用AOP 的其他方式
Spring使用AOP 的其他方式
16 2
|
8天前
|
XML Java 数据格式
Spring 项目如何使用AOP
Spring 项目如何使用AOP
22 2
|
12天前
|
XML 监控 安全
18:面向切面编程-Java Spring
18:面向切面编程-Java Spring
43 5
|
13天前
|
Java 开发者 Spring
Spring AOP的切点是通过使用AspectJ的切点表达式语言来定义的。
【5月更文挑战第1天】Spring AOP的切点是通过使用AspectJ的切点表达式语言来定义的。
24 5
|
13天前
|
XML Java 数据格式
Spring AOP
【5月更文挑战第1天】Spring AOP
27 5
|
14天前
|
Java 编译器 开发者
Spring的AOP理解
Spring的AOP理解
|
14天前
|
XML Java 数据格式
如何在Spring AOP中定义和应用通知?
【4月更文挑战第30天】如何在Spring AOP中定义和应用通知?
17 0
|
14天前
|
安全 Java 开发者
在Spring框架中,IoC和AOP是如何实现的?
【4月更文挑战第30天】在Spring框架中,IoC和AOP是如何实现的?
22 0