✏️作者:银河罐头
系列专栏:JavaEE
“种一棵树最好的时间是十年前,其次是现在”
AOP(Aspect Oriented Programming):⾯向切⾯编程,它是⼀种思想,它是对某一类事情的集中处理。
⽐如⽤户登录权限的校验,没学 AOP 之前,我们所有需要判断⽤户登录的⻚⾯(中 的⽅法),都要各⾃实现或调⽤⽤户验证的⽅法,然⽽有了 AOP 之后,我们只需要在某⼀处配 置⼀下,所有需要判断⽤户登录⻚⾯(中的⽅法)就全部可以实现⽤户登录验证了,不再需要每 个⽅法中都写相同的⽤户登录验证了。 ⽽ AOP 是⼀种思想,⽽ Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和 IoC 与 DI 类似。
除了统⼀的⽤户登录判断之外,AOP 还可以实现:
统⼀⽇志记录
统⼀⽅法执⾏时间统计
统⼀的返回格式设置
统⼀的异常处理
事务的开启和提交等
也就是说使⽤ AOP 可以扩充多个对象的某个能⼒,所以 AOP 可以说是 OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。
切⾯(Aspect)由切点(Pointcut)和通知(Advice)组成。
切⾯指的是某一方面的具体内容。
比如用户的登陆判断就是一个"切⾯",而日志的统计记录又是另一个"切面"。
如果把切面看成是一个类,那么切点就相当于是类里的一个方法。
定义一个"拦截规则"。
以博客系统举例:不是每个页面都需要进行登录校验,比如登录页或注册页就不需要。
通知:方法的具体实现,执行 整个 AOP 的逻辑业务
前置通知使⽤ @Before:在目标方法(实际要执行的方法)调用之前执行的通知。
后置通知使⽤ @After:在目标方法调用之后执行的通知。
比如在添加文章(目标方法)之前进行登录校验就是前置通知。在添加文章之后进行登录校验就是后置通知。
环绕通知 @Around:在目标方法执行前、后都执行的通知。
环绕通知使用场景:记录方法的调用时间
异常通知@AfterThrowing:在目标方法抛出异常的时候执行的通知。
异常通知使用场景:声明式事务
返回通知@AfterReturning:在目标方法返回的时候执行的通知。
所有可能触发"切点"的点就叫做"连接点"。
以博客系统为例:
假设点击发布博客,这是一个"连接点",
此时被拦截,没有立刻跳转到编辑页(“切点”),
而是跳转到登录页进行登录校验(“通知”),
切点 + 通知 = 切面
Maven Repository: org.springframework.boot » spring-boot-starter-aop » 2.7.11 (mvnrepository.com)
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
<version>2.7.11version>
dependency>
package com.example.demo.common;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect //切面
@Component //不能省略
public class UserAOP {
}
@Aspect //切面
@Component //不能省略
public class UserAOP {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){
}
}
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
修饰符和异常可以省略
@Before("pointcut()")
public void doBefore(){
System.out.println("执行前置通知: " + LocalDateTime.now());
}
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@RequestMapping("/user/sayhi")
public String sayHi(){
System.out.println("执行了 sayHi 方法");
return "hi spring boot aop";
}
@RequestMapping("/user/login")
public String login(){
System.out.println("执行了 login 方法");
return "do user login";
}
}
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ArticleController {
@RequestMapping("/article/sayhi")
public String sayHi(){
System.out.println("执行了 Article sayHi 方法");
return "article: hi spring boot aop";
}
}
在执行 ArticleController 类的方法时没有执行前置通知。
因为之前配置了拦截规则,只拦截 UserController 里的方法。
@After(("pointcut()"))
public void doAfter(){
System.out.println("执行后置通知: " + LocalDateTime.now());
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知开始");
Object obj = joinPoint.proceed();
System.out.println("环绕通知结束");
return obj;
}
Spring AOP 是构建在动态代理基础上。
Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。
默认情况下,实现了接⼝的类,使 ⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。
举例:
如果没有代理类,做"添加文章"操作时,就要在"目标对象"内部去实现 "判断登录状态"的操作。而有了代理类之后,就可以在"代理类"内部去实现"判断用户登陆状态"的操作。相当于代理类做了一层筛选。这样一来"目标对象"要做的事就变少了。
织⼊是把切⾯应⽤到⽬标对象并创建新的代理对象的过程,切⾯在指定的连接点被织⼊到⽬标对 象中。
在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:
编译期:
类加载期:
运⾏期:
1.JDK Proxy:
要求:被代理类一定要实现 接口
底层是:反射实现动态代理。
速度快
2.CGLIB:
通过实现代理类的子类来实现动态代理。所以它不能代理被 final 修饰的类。
底层是:字节码增强技术生成子类。
在 Spring 框架中,既使用了 JDK 动态代理又使用 CGLIB,默认情况下使用的是 JDK 动态代理,但是如果目标对象没有实现接口,则会使用 CGLIB 动态代理。