目录
1. Spring AOP 简介
2. AOP 的组成
2.1 切面(Aspect)
2.2 连接点(Join Point)
2.3 切点(Pointcut)
2.4 通知(Advice)
3. Spring AOP的实现
3.1 新建项目
3.2 添加 AOP 框架支持
3.3 定义切面、切点和通知
4. 切点表达式说明
5. 练习:使用 AOP 统计 UserController 每个方法的执行时间
AOP 是一种思想,Spring AOP 是这种思想的具体实现。
OOP:面向对象编程
AOP:面向切面编程
AOP 面向切面编程,就是对某一类事情的集中处理。
比如,我们需要在 CSDN 上进行编辑博客、发布博客、删除博客等操作,这些功能都是需要进行权限校验的,判断是否登录。
开发三阶段
对于公共方法的处理:
- (初级阶段)每个方法都去实现
- (中级阶段)把同一类功能抽取成公共方法
- (高级阶段)采用 AOP 的方式,对代码无侵入实现
除了统⼀的用户登录判断之外,AOP 还可以实现:
统一方法执行时间统计 | 项目监控:监控项目请求流量、监控接口的响应时间甚至每个方法的响应时间 |
统一的返回格式设置 | httpstatus: HTTP状态码 code: 业务状态码(后端响应成功不代表业务办理成功) msg: 业务处理失败返回的信息 data: |
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合。
应⽤执行过程中能够插入切面的⼀个点,这个点可以是方法调用时、抛出异常时,甚至修改字段 时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个 AOP 功能的所有方法。
Pointcut 是匹配 Join Point 的谓词。 Pointcut 的作用就是提供⼀组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice。
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中⼀条⼀条 的数据)。
切面也是有目标的 ——它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知。
Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本 方法进行调用:
切点相当于要增强的方法。
AOP 整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例:
既然说 AOP 是对一类事情的集中处理,那么我们就需要明确两点:
我们通过生活中的一个例子来看一下:
比如,我们乘坐高铁需要安检
那么,我们需要处理的内容就是安检;处理的范围就是需要乘坐高铁的人。
此处乘坐高铁需要安检这件事情就是切面,处理的内容安检就是通知,处理的范围乘坐高铁的人就是切点,具体有哪些人就是连接点。
切点是一个规则,事情的处理,最终作用在方法上。
在 pom.xml 中添加如下配置:
org.springframework.boot
spring-boot-starter-aop
我们先定义 UserController 类:
@RequestMapping("/user")
@RestController
public class UserController {
// 获取用户信息
@RequestMapping("/getInfo")
public String getInfo(){
return "get info...";
}
// 注册
@RequestMapping("/reg")
public String reg(){
return "reg...";
}
// Login
@RequestMapping("/login")
public String login(){
return "login...";
}
}
运行后,成功访问:
接下来,我们在 UserController 类中定义切面和切点:
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
// 获取用户信息
@RequestMapping("/getInfo")
public String getInfo(){
log.info("get info...");
return "get info...";
}
// 注册
@RequestMapping("/reg")
public String reg(){
log.info("reg...");
return "reg...";
}
// Login
@RequestMapping("/login")
public String login(){
log.info("login...");
return "login...";
}
}
在 LoginAspect 类中使用 @Before 注解(通知方法会在目标方法调用之前执行):
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){}
@Before("pointcut()")
public void doBefore(){
log.info("do berore...");
}
}
我们接着新建一个 TestController 类:
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/hi")
public String hi(){
log.info("hi~");
return "hi~";
}
}
可以看到运行的结果中,并没有在控制台打印 @Before 中的内容:
那么为什么没有执行呢?
我们再来看一下其他注解,@After(通知方法会在目标方法返回或者抛出异常后调用):
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){}
@Before("pointcut()")
public void doBefore(){
log.info("do berore...");
}
@After("pointcut()")
public void doAfter(){
log.info("do after...");
}
}
运行结果如下:
@AfterReturning(通知方法会在目标方法返回后调用):
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){}
@Before("pointcut()")
public void doBefore(){
log.info("do berore...");
}
@After("pointcut()")
public void doAfter(){
log.info("do after...");
}
@AfterReturning("pointcut()")
public void doAfterReturning(){
log.info("do after returning...");
}
}
运行以上代码后:
可以看到 :@AfterReturning 在 @After 之前被调用。
@AfterThrowing(通知方法会在目标方法抛出异常后调用):
我们首先在 UserController 类中加入异常:
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
// 获取用户信息
@RequestMapping("/getInfo")
public String getInfo(){
log.info("get info...");
return "get info...";
}
// 注册
@RequestMapping("/reg")
public String reg(){
log.info("reg...");
int a = 10/0;
return "reg...";
}
// Login
@RequestMapping("/login")
public String login(){
log.info("login...");
return "login...";
}
}
添加 @AfterThrowing 注解:
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){}
@Before("pointcut()")
public void doBefore(){
log.info("do berore...");
}
@After("pointcut()")
public void doAfter(){
log.info("do after...");
}
@AfterReturning("pointcut()")
public void doAfterReturning(){
log.info("do after returning...");
}
@AfterThrowing("pointcut()")
public void doAfterThrowing(){
log.info("do after throwing...");
}
}
运行后可以看到:
当正常返回时,执行 @AfterReturning 注解,当出现异常时,不会执行 @AfterReturning 注解;
当出现异常时,才会执行 @AfterThrowing 注解,当正常返回时,不会执行 @AfterThrowing 注解。
@Around(通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为):
添加 @Around 注解:
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){}
@Before("pointcut()")
public void doBefore(){
log.info("do berore...");
}
@After("pointcut()")
public void doAfter(){
log.info("do after...");
}
@AfterReturning("pointcut()")
public void doAfterReturning(){
log.info("do after returning...");
}
@AfterThrowing("pointcut()")
public void doAfterThrowing(){
log.info("do after throwing...");
}
@Around("pointcut()")
public void doAround(ProceedingJoinPoint joinPoint){
log.info("环绕通知执行之前...");
try {
joinPoint.proceed(); // 调用目标方法
} catch (Throwable e) {
throw new RuntimeException(e);
}
log.info("环绕通知执行之后...");
}
}
运行后界面显示如下:
可以看到此时界面中不再有返回值,因此修改代码如下:
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object oj = null;
log.info("环绕通知执行之前...");
try {
oj = joinPoint.proceed(); // 调用目标方法
} catch (Throwable e) {
throw new RuntimeException(e);
}
log.info("环绕通知执行之后...");
return oj;
}
此时可以看到成功返回并打印了值:
我们再来看一下这段代码:
AspectJ 支持三种通配符
切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){}
@Before("pointcut()")
public void doBefore(){
log.info("do berore...");
}
@After("pointcut()")
public void doAfter(){
log.info("do after...");
}
@AfterReturning("pointcut()")
public void doAfterReturning(){
log.info("do after returning...");
}
@AfterThrowing("pointcut()")
public void doAfterThrowing(){
log.info("do after throwing...");
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object oj = null;
log.info("环绕通知执行之前...");
try {
oj = joinPoint.proceed(); // 调用目标方法
} catch (Throwable e) {
throw new RuntimeException(e);
}
log.info("环绕通知执行之后...");
return oj;
}
/**
*
* @param joinPoint 使用 AOP 统计 UserController 每个方法的执行时间
* @return
*/
@Around("pointcut()")
public Object doAroundCount(ProceedingJoinPoint joinPoint){
Object oj = null;
long start = System.currentTimeMillis();
try {
oj = joinPoint.proceed(); // 调用目标方法
} catch (Throwable e) {
throw new RuntimeException(e);
}
log.info(joinPoint.getSignature().toString()+"耗时:"+(System.currentTimeMillis()-start));
return oj;
}
}
可以看到不同的方法直接在 url 中进行更改重新运行界面即可获得: