目录
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 每个方法的执行时间
1. Spring AOP 简介
AOP 是一种思想,Spring AOP 是这种思想的具体实现。
OOP:面向对象编程
AOP:面向切面编程
AOP 面向切面编程,就是对某一类事情的集中处理。
比如,我们需要在 CSDN 上进行编辑博客、发布博客、删除博客等操作,这些功能都是需要进行权限校验的,判断是否登录。
开发三阶段
对于公共方法的处理:
- (初级阶段)每个方法都去实现
- (中级阶段)把同一类功能抽取成公共方法
- (高级阶段)采用 AOP 的方式,对代码无侵入实现
除了统⼀的用户登录判断之外,AOP 还可以实现:
- 统⼀日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事务的开启和提交等
统一方法执行时间统计 | 项目监控:监控项目请求流量、监控接口的响应时间甚至每个方法的响应时间 |
统一的返回格式设置 | httpstatus: HTTP状态码 code: 业务状态码(后端响应成功不代表业务办理成功) msg: 业务处理失败返回的信息 data: |
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。
2. AOP 的组成
2.1 切面(Aspect)
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合。
2.2 连接点(Join Point)
应⽤执行过程中能够插入切面的⼀个点,这个点可以是方法调用时、抛出异常时,甚至修改字段 时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个 AOP 功能的所有方法。
2.3 切点(Pointcut)
Pointcut 是匹配 Join Point 的谓词。 Pointcut 的作用就是提供⼀组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice。
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中⼀条⼀条 的数据)。
2.4 通知(Advice)
切面也是有目标的 ——它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知。
Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本 方法进行调用:
- 前置通知使用 @Before:通知方法会在目标方法调用之前执行。
- 后置通知使用 @After:通知方法会在目标方法返回或者抛出异常后调用。
- 返回之后通知使用 @AfterReturning:通知方法会在目标方法返回后调用。
- 抛异常后通知使用 @AfterThrowing:通知方法会在目标方法抛出异常后调用。
- 环绕通知使用户 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
切点相当于要增强的方法。
AOP 整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例:
既然说 AOP 是对一类事情的集中处理,那么我们就需要明确两点:
- 一类事情:处理对象的一个范围
- 集中处理:处理的内容是什么
我们通过生活中的一个例子来看一下:
比如,我们乘坐高铁需要安检
那么,我们需要处理的内容就是安检;处理的范围就是需要乘坐高铁的人。
此处乘坐高铁需要安检这件事情就是切面,处理的内容安检就是通知,处理的范围乘坐高铁的人就是切点,具体有哪些人就是连接点。
切点是一个规则,事情的处理,最终作用在方法上。
3. Spring AOP的实现
3.1 新建项目
3.2 添加 AOP 框架支持
在 pom.xml 中添加如下配置:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.3 定义切面、切点和通知
我们先定义 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;
}
此时可以看到成功返回并打印了值:
我们再来看一下这段代码:
4. 切点表达式说明
AspectJ 支持三种通配符
- * :匹配任意字符,只匹配一个元素(包,类,或方法,方法参数)
- .. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。
- + :表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的 所有子类包括本身
切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
5. 练习:使用 AOP 统计 UserController 每个方法的执行时间
@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 中进行更改重新运行界面即可获得: