什么是AOP
AOP(Aspect Oriented Programming):面向切面编程,它是一种思想,它是对某一类事情的集中处理。
为什要用 AOP?
想象⼀个场景,在做后台系统时,除了登录和注册等功能不需要做用户登录验证之外,其他几乎所有页面调用的前端控制器( Controller)都需要先验证用户登录的状态,那这个时候要怎么处理呢?之前的处理方式是每个 Controller 都要写⼀遍用户登录验证,然而当你的功能越来越多,那么你要写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会由代码修改和维护的成本。需要一个简单的处理方案。对于这种功能统⼀且使用的地方较多的功能,就可以考虑 AOP来统⼀处理。
除了统⼀的用户登录判断之外,AOP 还可以实现:
- 统⼀日志记录
- 统⼀方法执行时间统计(监控接口的响应时间)
- 统⼀的返回格式设置
- 统⼀的异常处理
- 事务的开启和提交等
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object OrientedProgramming,面向对象编程)的补充和完善。
Spring AOP
AOP 是⼀种思想,而 Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和IOC 与 DI 类似。
AOP的组成
切面(Aspect)
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
切面是包含了:通知、切点和切⾯的类,相当于 AOP 实现的某个功能的集合。
连接点(Join Point)
应用执行过程中能够插⼊切面的⼀个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个 AOP 功能的所有方法。
切点(Pointcut)
切点是匹配连接点的谓词。Pointcut 的作用就是提供⼀组规则(使用 AspectJ pointcut expression language 来描述)来匹配连接点,给满足规则的连接点添加通知。
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)。
通知(Advice)
切面的工作被称之为通知。通知定义了切面是什么,何时使用,描述了切面要完成的工作,还解决何时执行这个工作的问题。
AOP 整个组成部分的概念如下图所示,以多个页⾯都要访问用户登录权限为例:
Spring AOP的实现
使用 Spring AOP 来实现⼀下 AOP 的功能,完成的目标是拦截所有 UserController 里面的方法,每次调用UserController 中任意⼀个方法时,都执行相应的通知事件。
@Slf4j
@RestController
@RequestMapping("user")
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....";
}
//登录
@RequestMapping("/login")
public String login(){
log.info("login...");
return "login.....";
}
}
添加AOP框架支持
在pom.xml中添加如下配置:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义切面和切点
切点指的是具体要处理的某⼀类问题,⽐如⽤户登录权限验证就是⼀个具体的问题,记录所有⽅法的执行日志就是⼀个具体的问题,切点定义的是某⼀类问题
@Aspect //表明此类为一个切面
public class LoginAspect {
// 定义切点,这⾥使⽤ AspectJ 表达式语法
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){ }
}
切点表达式:
AspectJ ⽀持三种通配符
*
匹配任意字符,只匹配⼀个元素(包,类,或方法,方法参数)
...
匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。
+
表示按照类型匹配指定类的所有类,必须跟在类名后⾯。
切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,⽤来匹配方法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
修饰符可以省略,返回类型不能省略。在上述示例中,就是要匹com.example.demo.controller.UserController
下的所有方法(.*
),参数任意(..
),返回值为任意(*
)。
pointcut 方法为空方法,它不需要有方法体,此方法名就是起到⼀个“标识”的作用,标识下面的通知方法具体指的是哪个切点。
定义通知
通知定义的是被拦截的方法具体要执行的业务,比如用户登录权限验证方法就是具体要执行的业务。Spring AOP 中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
前置通知@Before
:通知方法会在目标方法调用之前执行。
后置通知@After
:通知方法会在目标方法返回或者抛出异常后调用。
返回之后通知@AfterReturning
:通知方法会在目标方法返回后调用。
抛异常后通知@AfterThrowing
:通知方法会在目标方法抛出异常后调用。
环绕通知@Around
:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
@Slf4j
@Aspect //表明此类为一个切面
@Component
public class LoginAspect {
//定义切点方法
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){ }
//前置通知
@Before("pointcut()")
public void doBefore(){
log.info("do before....");
}
//后置通知
@After("pointcut()")
public void doAfter(){
log.info("do after...");
}
//return后通知
@AfterReturning("pointcut()")
public void doAfterReturning(){
log.info("do afterReturning...");
}
//抛出异常后通知
@AfterThrowing("pointcut()")
public void doAfterThrowing(){
log.info("do afterThrowing..");
}
//环绕通知
@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;
}
}
在正常执行时,AfterReturning先于After执行,不会执行AfterThrowing;在执行出现异常时,AfterThrowing会先于After执行,此时不会执行AfterReturning。
在环绕通知中,ProceedingJoinPoint joinPoint表示当前连接点,且在环绕通知中必须要有返回结果。同样在正常执行时不执行AfterThrowing,而执行异常时,会执行AfterThrowing且不会执行方法调用之后的自定义方法。
在上述写法中,一个切面包含多个通知和切点。如果一个切点只有一个通知,可以把切点的规则放在通知上。对于一个方法可以有多个切面(Aspect)。
练习:使用 AOP 统计 UserController 每个方法的执行时间
//环绕通知
@Around("pointcut()")
public Object doAround(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) + "ms");
return oj;
}
Spring AOP的实现原理
代理模式
定义:为其他对象提供⼀种代理以控制对这个对象的访问。在某些情况下,⼀个对象不适合或者不能直接引用另⼀个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。代理模式分为静态代理和动态代理。
1.静态代理
静态代理中,对目标对象的每个方法的增强都是手动完成的,⾮常不灵活(比如接口⼀旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写⼀个代理类)。从 JVM 层⾯来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
2.动态代理
相比于静态代理来说,动态代理更加灵活。不需要针对每个目标类都单独创建⼀个代理类,并且也不需要必须实现接口。从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。 Spring AOP 就是构建在动态代理基础上的。
比如在租房时,静态代理就好比每一个房子都由不同的中介来代理,我们在看不同的房时要找不同的中介;动态代理就好比一个中介可以代理多个房子,根据我们看房的时机不同,中介就会代理不同的房子。
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。
JDK动态代理
JDK 实现时,先通过实现 InvocationHandler
接口创建方法调用处理器,再通过 Proxy
来创建代理类。
JDK 动态代理类使用步骤:
- 定义⼀个接口及其实现类;
- 自定义 InvocationHandler 并重写invoke方法,在 invoke ⽅法中我们会调用原生方法(被代理类的方法)并⾃定义⼀些处理逻辑;
- 通过 Proxy.newProxyInstance 方法创建代理对象;
定义JDK动态代理类:
public class JDKInvocationHandler implements InvocationHandler {
//⽬标对象即就是被代理对象
private Object target;
public JDKInvocationHandler(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 class Main {
/**
* JDK动态代理
* @param args
*/
public static void main(String[] args) {
PayService target= new AliPayService();
//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{PayService.class},
new JDKInvocationHandler(target)
);
proxy.pay();
}
}
Proxy 类中使⽤频率最最的方法是:newProxyInstance() ,这个方法主要用来生成⼀个代理对象:
这个方法⼀共有 3 个参数:loader :类加载器,用于加载代理对象;interfaces : 被代理类实现的⼀些接⼝;h : 实现了 InvocationHandler 接⼝的对象;
CGLIB动态代理
JDK 动态代理有⼀个最致命的问题是其只能代理实现了接口的类。为了解决这个问题,可以⽤ CGLIB 动态代理机制来避免。CGLIB(Code Generation Library)是⼀个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。
在 CGLIB 动态代理机制中 MethodInterceptor
接口和 Enhancer
类是核心。
CGLIB 动态代理类使用步骤
- 定义⼀个类;
- 自定义 MethodInterceptor 并重写 intercept 方法,intercept ⽤于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
- 通过 Enhancer 类的 create()创建代理类。
添加依赖
和JDK 动态代理不同, 使用CGLIB 需要手动添加相关依赖。在Spring框架中已经导入了这个依赖。
自定义 MethodInterceptor(方法拦截器)
public class CGLIBInterceptor implements MethodInterceptor {
//被代理对象
private Object target;
public CGLIBInterceptor(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;
}
}
创建代理类并使用
package com.example.demo.proxy;
import org.springframework.cglib.proxy.Enhancer;
import java.lang.reflect.Proxy;
public class Main {
/**
* CGLIB动态代理
* @param args
*/
public static void main(String[] args) {
PayService target= new AliPayService(); //目标对象
PayService proxy= (PayService) Enhancer.create(target.getClass(),new CGLIBInterceptor(target)); //创建代理对象
proxy.pay();
}
}
JDK 动态代理和 CGLIB 动态代理对比
1.JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类;
2.CGLIB 动态代理是通过生成⼀个被代理类的⼦类来拦截被代理类的方法调用,因此不能代理声明为 final。
3.JDK动态代理是JDK提供的,而CGLIB动态代理是第三方提供的。
Spring代理的选择
在创建AOP代理时,根据proxyTargetClass属性来使用不同的代理模式:
- proxyTargetClass 为false, 目标实现了接口, 用JDK代理
- proxyTargetClass 为false, 目标未实现接口, 用CGLIB代理
- proxyTargetClass 为true, 用CGLIB代理。
织入:代理的生成时机
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。
在目标对象的生命周期里有多个点可以进行织入:
编译期:切面在目标类编译时被织⼊。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的;
类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。
运行期:切面在应用运行的某⼀时刻被织入。⼀般情况下,在织入切面时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种方式织入切面的。
继续加油~