文章目录
- 一、什么是 Spring AOP?
- 二、为什要⽤ AOP?
- 三、AOP 的组成
- 四、Spring AOP 的实现
- 五、Spring AOP 实现原理
一、什么是 Spring AOP?
AOP(Aspect Oriented Programming):⾯向切⾯编程,它是⼀种思想,它是对某⼀类事情的
集中处理。⽐如⽤户登录权限的效验,没学 AOP 之前,我们所有需要判断⽤户登录的⻚⾯(中的⽅法),都要各⾃实现或调⽤⽤户验证的⽅法,然⽽有了 AOP 之后,我们只需要在某⼀处配置⼀下,所有需要判断⽤户登录⻚⾯(中的⽅法)就全部可以实现⽤户登录验证了,不再需要每个⽅法中都写相同的⽤户登录验证了
AOP 是⼀种思想,⽽ Spring AOP 是⼀个框架,是对 AOP 思想的实现,它们的关系和 IoC与 DI 类似 !
二、为什要⽤ AOP?
想象⼀个场景,我们在做后台系统时,除了登录和注册等⼏个功能不需要做⽤户登录验证之外,其他⼏乎所有⻚⾯调⽤的前端控制器( Controller)都需要先验证⽤户登录的状态,那这个时候我们要怎么处理呢?
我们之前的处理⽅式是每个 Controller 都要写⼀遍⽤户登录验证,然⽽当你的功能越来越多,那么你要写的登录验证也越来越多,⽽这些⽅法⼜是相同的,这么多的⽅法就会增加代码修改和维护的成本。那有没有简单的处理⽅案呢?
答案是肯定的,对于这种功能统⼀,且使⽤的地⽅较多的功能,就可以考虑 AOP 来统⼀处理了
除了统⼀的⽤户登录判断之外,AOP 还可以实现:
- 统⼀⽇志记录
- 统⼀⽅法执⾏时间统计
- 统⼀的返回格式设置
- 统⼀的异常处理
- 事务的开启和提交等
也就是说使⽤ AOP 可以扩充多个对象的某个能⼒,所以 AOP 可以说是 OOP(Object Oriented Programming,⾯向对象编程)的补充和完善
三、AOP 的组成
- 切⾯(Aspect)
切⾯(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义
切⾯是包含了通知、切点和织入的类,相当于 AOP 实现的某个功能的集合。切面定义了 AOP是针对哪一个统一的功能,比如用户登录效验功能就是一个切面
- 连接点(Join Point)
应⽤执⾏过程中能够插⼊切⾯的⼀个点,这个点可以是⽅法调⽤时,抛出异常时,甚⾄修改字段时。切⾯代码可以利⽤这些点插⼊到应⽤的正常流程之中,并添加新的⾏为
连接点是需要被增强某个 AOP 功能的所有⽅法,所有满足拦截规则的方法就称之为连接点
-
切点(Pointcut)
Pointcut 的作⽤就是定义⼀组 AOP 拦截规则(使⽤ AspectJ pointcut expression language 来描述)来匹配 Join Point,给满⾜规则的 Join Point 添加 Advice -
通知(Advice)
1.切⾯也是有⽬标的 ,也有必须完成的⼯作。在 AOP 术语中,切⾯的⼯作被称之为通知 ,即具体要实现的 AOP 功能!
2.通知:定义了切⾯是什么,何时使⽤,其描述了切⾯要完成的⼯作,还解决何时执⾏这个⼯作的问题
Spring 切⾯类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本
⽅法进⾏调⽤:
- 前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏
- 后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤
- 返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤
- 抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤
- 环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为
后面我们会讲解如何使用 !!
四、Spring AOP 的实现
接下来我们使⽤ Spring AOP 来实现⼀下 AOP 的功能,完成的⽬标是拦截所有 UserController ⾥⾯的⽅法,每次调⽤ UserController 中任意⼀个⽅法时,都要执⾏相应的通知事件,即为该方法增强某个 AOP 功能 !!
Spring AOP 的实现步骤如下:
- 添加 Spring AOP 框架⽀持
- 定义切⾯和切点
- 定义通知
添加 Spring AOP 框架⽀持:
在 Maven 仓库中,找到 AOP 框架依赖并在 pom.xml 中手动引入:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/springboot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义切⾯和切点:
切面指的是具体要处理的某⼀类问题,⽐如⽤户登录权限验证就是⼀个具体的问题,记录所有⽅法的执⾏⽇志就是⼀个具体的问题,切面定义的是某⼀类问题
代码实现:
@Aspect // 定义一个切面,表明此类为一个切面
@Component
public class UserAspect {
//切点: 定义切点(设置拦截规则),这⾥使⽤ AspectJ 表达式语法
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut() {
}
注意:
- 在类上添加注解@Aspect ,表明此类为一个切面
- 其中 pointcut ⽅法为空⽅法,它不需要有⽅法体,此⽅法名就是起到⼀个“标识”的作⽤,标识下⾯的通知⽅法具体指的是哪个切点(因为切点可能有很多个)
- 定义切点(设置拦截规则),这⾥使⽤ AspectJ 表达式语法 !上述表示的是当调用 controller.UserController 中的所有方法时,就会触发拦截规则
切点表达式说明(AspectJ 表达式语法):
AspectJ ⽀持三种通配符
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>) //修饰符和异常可以省略
如下对应关系:
表达式示例:
execution(* com.cad.demo.User.(…)) :匹配 User 类⾥的所有⽅法。
execution( com.cad.demo.User+.(…)) :匹配该类的⼦类包括该类的所有⽅法。
execution( com.cad..(…)) :匹配 com.cad 包下的所有类的所有⽅法。
execution(* com.cad….(…)) :匹配 com.cad 包下、⼦孙包下所有类的所有⽅法。
execution(* addUser(String, int)) :匹配 addUser ⽅法,且第⼀个参数类型是 String,第⼆个
参数类型是 int
定义通知(补充前面各通知示例):
//前置通知
@Before("pointcut()")
public void doBefore() {
System.out.println("前置通知:被执行了 !");
}
// 后置通知
@After("pointcut()")
public void doAfter() {
System.out.println("执⾏后置通知 After ⽅法");
}
// return 之后通知
@AfterReturning("pointcut()")
public void doAfterReturning() {
System.out.println("执⾏ AfterReturning ⽅法");
}
// 抛出异常之后通知
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
System.out.println("执⾏ doAfterThrowing ⽅法");
}
// 添加环绕通知:将整个目标方法包裹起来了
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
System.out.println("Around ⽅法开始执⾏");
try {
// 执⾏拦截⽅法,以及拦截方法所对应的相应通知
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around ⽅法结束执⾏");
return obj;
}
Controller 中的实现:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/sayhi")
public String sayhi(){
System.out.println("执行了 目标方法 !");
return "HI WORLD !";
}
}
url 请求验证并观察控制台结果:
分析可知,环绕通知将整个目标方法包裹起来了,所以最先和最后执行的都是环绕通知。其次在目标方法执行之前执行了前置通知,目标方法返回后执行了返回通知,返回结束后才执行的后置通知!而目标代码中没有抛出异常,所以并没有执行抛出异常后通知 !!
我们制造一个异常,来观察 抛出异常后通知
修改controller代码,增加除数异常:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/sayhi")
public String sayhi(){
System.out.println("执行了 目标方法 !");
int num = 10/0;
return "HI WORLD !";
}
}
观察控制台结果:
当出现异常后,才会执行上述异常通知 !!
五、Spring AOP 实现原理
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的⽀持局限于⽅法级别的拦截
Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。默认情况下,实现了接⼝的类,使⽤AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类
1、织⼊(Weaving):代理的⽣成时机
织⼊是把切⾯应⽤到⽬标对象并创建新的代理对象的过程
在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:
- 编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就
是以这种⽅式织⼊切⾯的。 - 类加载器:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器
(ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载
时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。 - 运⾏期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为目标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的
2、动态代理
此种实现在设计模式上称为动态代理模式,在实现的技术⼿段上,都是在 class 代码运⾏期,动
态的织⼊字节码。
我们学习 Spring 框架中的AOP,主要基于两种⽅式:JDK 及 CGLIB 的⽅式。这两种⽅式的代理
⽬标都是被代理类中的⽅法,在运⾏期,动态的织⼊字节码⽣成代理类
- CGLIB是Java中的动态代理框架,主要作⽤就是根据⽬标类和⽅法,动态⽣成代理类。
- Java中的动态代理框架,⼏乎都是依赖字节码框架(如 ASM,Javassist 等)实现的。
- 字节码框架是直接操作 class 字节码的框架。可以加载已有的class字节码⽂件信息,修改部
分信息,或动态⽣成⼀个 class
2.1、JDK 动态代理实现
JDK 实现时,先通过实现 InvocationHandler 接⼝创建⽅法调⽤处理器,再通过 Proxy 来创建代
理类,以下为代码实现:
//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被
代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler {
//⽬标对象即就是被代理对象
private Object target;
public PayServiceJDKInvocationHandler(Object target) {
this.target = target;
}
//proxy代理对象
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过反射调⽤被代理类的⽅法
Object retVal = method.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target = new AliPayService();
//⽅法调⽤处理器
InvocationHandler handler =
new PayServiceJDKInvocationHandler(target);
//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{PayService.class},
handler
);
proxy.pay();
}
}
2.2、CGLIB 动态代理实现
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
//被代理对象
private Object target;
public PayServiceCGLIBInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过cglib的代理⽅法调⽤
Object retVal = methodProxy.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target = new AliPayService();
PayService proxy = (PayService)
Enhancer.create(target.getClass(), new
PayServiceCGLIBInterceptor(target));
proxy.pay();
}
}
2.3、JDK 和 CGLIB 实现的区别
- JDK 实现,要求被代理类必须实现接⼝,之后是通过 InvocationHandler 及 Proxy,在运⾏时
动态的在内存中⽣成了代理类对象,该代理对象是通过实现同样的接⼝实现(类似静态代理
接⼝实现的⽅式),只是该代理类是在运⾏期时,动态的织⼊统⼀业务逻辑字节码来完成。 - CGLIB 实现,被代理类可以不实现接⼝,是通过继承被代理类,在运⾏时动态的⽣成代理类
对象。默认情况下 Spring AOP 都会采用CGLIB 来实现动态代理,但是由于它是通过继承被代理类来实现的,所以它不能代理最终类(被 final修饰的类,不能被继承)