Spring Boot AOP - 面向切面编程
AOP,即面向切面编程,其核心思想就是把业务分为核心业务和非核心业务两大部分。例如一个论坛系统,用户登录、发帖等等这是核心功能,而日志统计等等这些就是非核心功能。
在Spring Boot AOP中,非核心业务功能被定义为切面,核心和非核心功能都开发完成之后,再将两者编织在一起,这就是AOP。
AOP的目的就是将那些与业务无关,却需要被业务所用的逻辑单独封装,以减少重复代码,减低模块之间耦合度,利于未来系统拓展和维护。
今天,我将做一个简单的打印用户信息的程序,即后端接受POST请求中的User对象将其打印这样一个逻辑,在这个上面实现AOP。
首先放上用户打印服务逻辑的方法代码:
User
类:
public class User implements Serializable {
private int id;
private String username;
private String nickname;
}
Service
层:
package com.example.springbootaop.service.impl;
import com.example.springbootaop.model.User;
import com.example.springbootaop.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class UserServiceImpl implements UserService {
/**
* 使用Logger
*/
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void printUserInfo(User user) {
logger.info("用户id:" + user.getId());
logger.info("用户名:" + user.getUsername());
logger.info("用户昵称:" + user.getNickname());
}
}
Controller
层:
package com.example.springbootaop.api;
import com.example.springbootaop.model.User;
import com.example.springbootaop.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserAPI {
@Autowired
private UserService userService;
@PostMapping("/user")
public String printUser(@RequestBody User user) {
userService.printUserInfo(user);
return "已完成打印!";
}
}
AOP到底有什么不同
这里只是实现一个简单的逻辑,打印用户信息,这就是我们今天的核心功能。
如果这个时候我们要给它加上非核心功能:在打印之前和打印之后分别执行一个方法,如果你不知道AOP,你可能会把Controller
层的方法改成如下形式:
@PostMapping("/user")
public String printUser(@RequestBody User user) {
// 执行核心业务之前
doBefore();
// 执行核心业务
userService.printUserInfo(user);
// 执行核心业务之后
doAfter();
...
return "已完成打印!";
}
如果说方法多了,业务多了,非核心业务的逻辑一变,所有Controller
的全部方法都要改动,非常麻烦,且代码冗余,耦合度高。
这时,就需要AOP来解决这个问题。
AOP只需要我们单独定义一个切面,在里面写好非核心业务的逻辑,即可将其织入核心功能中去,无需我们再改动Service
层或者Controller
层。
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
环绕通知
定义切面
新建aop
包,在里面新建类作为我们的切面类,先放出切面类代码:
package com.example.springbootaop.aop;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogAspect {
/**
* 日志打印
*/
private Logger logger = 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.*(..))")
public void log() {
}
/**
* 前置通知:在被代理方法之前调用
*/
@Before("log()")
public void doBefore() {
logger.warn("调用方法之前:");
logger.warn("接收到请求!");
}
/**
* 后置通知:在被代理方法之后调用
*/
@After("log()")
public void doAfter() {
logger.warn("调用方法之后:");
logger.warn("打印请求内容完成!");
}
/**
* 返回通知:被代理方法正常返回之后调用
*/
@AfterReturning("log()")
public void doReturning() {
logger.warn("方法正常返回之后:");
logger.warn("完成返回内容!");
}
/**
* 异常通知:被代理方法抛出异常时调用
*/
@AfterThrowing("log()")
public void doThrowing() {
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.*(..))")
public void doBefore(JoinPoint joinPoint) {
logger.warn("调用方法之前:"); logger.warn("接收到请求!");
}
但是大多数情况并不推荐这样,这种写法较为复杂。
我们发送一个请求测试一下:
通过这个,我们也可以发现各个通知的执行顺序:
Before -> AfterReturning -> After
环绕通知
环绕通知是AOP中最强大的通知,可以同时实现前置和后置通知,不过它的可控性没那么强,如果不用大量改变业务逻辑,一般不需要用到它。我们在上述切面加入下列环绕通知方法:
/**
* 环绕通知
*/
@Around("log()")
public void around(ProceedingJoinPoint joinPoint) {
logger.warn("执行环绕通知之前:");
try {
joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
logger.warn("执行环绕通知之后");
}
通知方法中有一个ProceedingJoinPoint
类型参数,通过其proceed
方法来调用原方法。需要注意的是环绕通知是会覆盖原方法逻辑的,如果上面代码不执行joinPoint.proceed();
这一句,就不会执行原被织入方法。因此环绕通知一定要调用参数的proceed
方法,这是通过反射实现对被织入方法调用。
再次测试如下:
通知方法传参
上面每个通知方法是没有参数的。其实,通知方法是可以接受被织入方法的参数的。我们上述被织入方法参数就是一个User
对象,因此通知方法也可以加上这个参数接受。我们改变前置通知方法如下:
/**
* 前置通知:在被代理方法之前调用
*/
@Before("log() && args(user)")
public void doBefore(User user) {
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) {
...
}
总结
AOP其实使用起来是个很方便的东西,大大降低了相关功能之间的耦合度,使得整个系统井井有条。
定义切面,然后定义切点,再实现切面逻辑(各个通知方法),就完成了一个简单的切面。
来源:https://juejin.cn/post/6999570632409088008