一.AOP的概述
什么是⾯向切⾯编程呢? 切⾯就是指某⼀类特定问题, 所以AOP也可以理解为⾯向特定⽅法编程. 什么是⾯向特定⽅法编程呢? ⽐如上个章节学习的"登录校验", 就是⼀类特定问题. 登录校验拦截器, 就是对"登录校验"这类问题的统⼀处理. 所以, 拦截器也是AOP的⼀种应⽤. AOP是⼀种思想, 拦截器是AOP思想的⼀种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接⼝.同样的, 统⼀数据返回格式和统⼀异常处理, 也是AOP思想的⼀种实现.简单来说: AOP是⼀种思想, 是对某⼀类事情的集中处理.
二.AOP入门程序
AOP就可以做到在不改动这些原始⽅法的基础上, 针对特定的⽅法进⾏功能的增强.
代码:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.在建包(aspect),创建一个TimeRecordAspect.类
作用范围是controller包下所有类的方法。
代码:
package com.example.demo.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component//交给Spring管理
@Aspect
public class TimeRecordAspect {
/*记录耗时*/
@Around("execution(* com.example.demo.controller.*.*(..))")//作用范围
public Object timeRecord(ProceedingJoinPoint joinPoint) throws Throwable {
//记录开始时间
long start = System.currentTimeMillis();
//执行目标方法
Object proceed = joinPoint.proceed();
//记录结束时间
long end = System.currentTimeMillis();
log.info("时间耗时:"+(end - start)+"ms");
return proceed;
}
}
- @Aspect: 标识这是⼀个切⾯类
- @Around: 环绕通知, 在⽬标⽅法的前后都会被执⾏. 后⾯的表达式表⽰对哪些⽅法进⾏增强.
- ProceedingJoinPoint.proceed() 让原始方法执行
3.入侵 controller包的三个方法
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("t1")
public void sayHi(){
// userService.doService();
System.out.println("do controller...");
}
public int test1(){
int count = 0;
for (int i = 0; i < 10000; i++) {
count += i;
}
return count;
}
public int test2(){
int count = 0;
for (int i = 0; i < 10000000; i++) {
count += i;
}
return count;
}
}
4.运行程序,观察日志
-
模块化:通过将横切关注点从主要业务逻辑中分离出来,实现了更好的模块化。在上面的例子中,服务团队负责处理非烹饪业务,使得厨师们可以专注于烹饪。同样,在软件开发中,AOP使得代码更加模块化,易于理解和维护。
-
重用性:AOP允许开发者将横切关注点封装为可重用的模块(切面),并在需要时将其应用于不同的模块中。例如,日志记录功能可以在多个服务或组件中被重用,而无需在每个地方都编写相同的代码。
-
解耦合:AOP将横切关注点与主要业务逻辑分离开来,降低了它们之间的耦合度。这使得代码更加灵活,易于修改和扩展。在上面的例子中,如果餐厅需要改变支付方式,服务团队可以独立地进行调整,而不需要厨师们参与。
-
提高开发效率:通过减少重复代码和降低耦合度,AOP提高了开发效率。开发者可以更快地编写和维护代码,同时减少了出错的可能性。
简而言之:
- 代码⽆侵⼊: 不修改原始的业务⽅法, 就可以对原始的业务⽅法进⾏了功能的增强或者是功能的改变
- 减少了重复代码
- 提⾼开发效率
- 维护⽅便
三.Spring AOP 详解
3.1Spring AOP中涉及的核⼼概念
3.1.1切点(Pointcut)
上⾯的表达式 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式.
3.1.2 连接点(Join Point)
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("Test")
public class UserController {
// private UserService userService;
// @Autowired
// private UserService userService;
// //通过构造方法注入
// public UserController( ) {
//
// }
// @Autowired
// public UserController(UserService userService) {
// this.userService = userService;
// }
//通过Setter方法注入
// @Autowired
//
// public void setUserService(UserService userService) {
// this.userService = userService;
// }
@RequestMapping("t1")
public void sayHi(){
// userService.doService();
System.out.println("do controller...");
}
public int test1(){
int count = 0;
for (int i = 0; i < 10000; i++) {
count += i;
}
return count;
}
public int test2(){
int count = 0;
for (int i = 0; i < 10000000; i++) {
count += i;
}
return count;
}
}
切点和连接点的关系连接点是满⾜切点表达式的元素. 切点可以看做是保存了众多连接点的⼀个集合
3.1.3 通知(Advice)
3.1.4 切⾯(Aspect)
切⾯所在的类, 我们⼀般称为切⾯类 .
3.2Spring AOP通知类型
- @Around: 环绕通知, 此注解标注的通知⽅法在⽬标⽅法前, 后都被执⾏
- @Before: 前置通知, 此注解标注的通知⽅法在⽬标⽅法前被执⾏
- @After: 后置通知, 此注解标注的通知⽅法在⽬标⽅法后被执⾏, ⽆论是否有异常都会执⾏
- @AfterReturning: 返回后通知, 此注解标注的通知⽅法在⽬标⽅法后被执⾏, 有异常不会执⾏
- @AfterThrowing: 异常后通知, 此注解标注的通知⽅法发⽣异常后执⾏
为⽅便学习, 我们可以新建⼀个项⽬
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo {
//前置通知
@Before("execution(* com.example.demo.controller.*.*(..))")
public void doBefore() {
log.info("执⾏ Before ⽅法");
}
//后置通知
@After("execution(* com.example.demo.controller.*.*(..))")
public void doAfter() {
log.info("执⾏ After ⽅法");
}
//返回后通知
@AfterReturning("execution(* com.example.demo.controller.*.*(..))")
public void doAfterReturning() {
log.info("执⾏ AfterReturning ⽅法");
}
//抛出异常后通知
@AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
public void doAfterThrowing() {
log.info("执⾏ doAfterThrowing ⽅法");
}
//添加环绕通知
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around ⽅法开始执⾏");
Object result = joinPoint.proceed();
log.info("Around ⽅法结束执⾏");
return result;
}
}
写⼀些测试程序:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
int a = 10 / 0;
return true;
}
}
运⾏程序, 观察⽇志:
1. 正常运⾏的情况
观察日志:
3.3@PointCut
@Slf4j
@Aspect
@Component
public class AspectDemo {
//定义切点(公共的切点表达式)
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
//...代码省略
}
//后置通知
@After("pt()")
public void doAfter() {
//...代码省略
}
//返回后通知
@AfterReturning("pt()")
public void doAfterReturning() {
//...代码省略
}
//抛出异常后通知
@AfterThrowing("pt()")
public void doAfterThrowing() {
//...代码省略
}
//添加环绕通知
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
//...代码省略
}
}
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
//前置通知
@Before("com.example.demo.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执⾏ AspectDemo2 -> Before ⽅法");
}
}