目录
一、什么是AOP
1.1 AOP的定义
1.2 为什么要有AOP
二、Spring AOP 应该学习哪些知识?
三、Spring AOP的组成
3.1 切面(Aspect,可以理解为"类")
3.2 切点(Pointcut,可以理解为"方法")
3.3 通知(Advice, 可以理解为方法的具体实现)
3.4 连接点(Join Point)
3.5 AOP概念图
四、Spring AOP 实现
4.1 添加Spring AOP 框架支持
4.2 定义切面,切点,通知
4.3 定义连接点
4.4 关于环绕通知的扩展
4.4.1 环绕通知与前后置通知的执行顺序
4.4.2 环绕通知和CGLIB的关系
五、Spring AOP 实现原理
5.1 AOP实现技术(静态代理,动态代理)
5.2 SpringAOP为什么仅限于对方法级别的拦截
5.3 JDK动态代理和CGLIB动态代理的区别
六、总结
在介绍Spring AOP之前,我们先来看看AOP是什么。
AOP(Aspect Oriented Programming): 面向切面编程,它是一种思想-> 是对某一类事情集中处理。
AOP是一种思想,SpringAOP则是一个框架,其提供了一种对AOP思想的实现,它们的关系和IoC与DI类似。
比如用户登录权限的校验:如果没有AOP的存在,我们就需要在一个网站的某些场景下(只有用户登陆后才能访问的场景),再次对用户是否登录进行判断,这不仅增加了代码修改和维护的成本,而且不利于实现低耦合,无法让开发者把业务逻辑给区分开。
因此,对于以上场景,需要对这种使用次数多,功能又是相同的,就需要考虑AOP来进行统一处理。
当然,除了统一的用户登录判断之外,AOP还可以实现:
也就是说,AOP可以扩充多个对象的某个能力,所以AOP可以说是OOP(Object Oriented
Programming,⾯向对象编程)的补充和完善。
切面是由切点和通知组成的,它既包含了横切逻辑的定义,也包含了连接点的定义。
通俗来说,某一方面的具体内容就是一个切面,比如用户登录判断就是一个切面,而日志的统计记录也是一个切面。
定义具体的拦截规则
定义了切面是什么,描述了切面要完成的工作,简单来说,就是 AOP的执行逻辑。
Spring切面类中,可以在方法上使用以下注释,会设置方法为通知方法,在满足条件后会通知该方法进行调用:
所有可能触发的点就叫做连接点。
AOP的整个组成概念如下图所示,以多个页面都要访问用户登录权限为例:
接下来我们使用Spring AOP来实现AOP的功能,大致的目标是 拦截所有UserController里面的方法,每次调用UserController中任意一个方法时,都执行相应的通知事件。
去Maven仓库中找到Spring AOP的依赖(注意,寻找的其实是用于SpringBoot项目的SpringAOP依赖,如果是用于Spring项目的,Spirng自带的库中是已经添加过该依赖的)
在porm.xml 中添加如下配置:
org.springframework.boot
spring-boot-starter-aop
2.7.10
@Aspect // 切面
@Component // 不能忽略
public class UserAOP {
// 切点 (配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
public void pointcut() {
}
// 前置通知 @Before里面的参数取决于 切点的方法名
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知: " + LocalDateTime.now());
}
}
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
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";
}
}
访问:localhost:8080/user/sayhi 后:控制台信息如下:
4.4.1 环绕通知与前后置通知的执行顺序
我们先来看一组代码:
package com.example.demo.common;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Aspect // 切面
@Component // 不能忽略
public class UserAOP {
// 切点 (配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
public void pointcut() {
}
// 前置通知 @Before里面的参数取决于 切点的方法名
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知: " + LocalDateTime.now());
}
@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;
}
}
package com.example.demo.controller;
import org.springframework.stereotype.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";
}
}
执行结果如下:
我们发现,环绕通知始终是与最外侧的,这是因为joinPoint.proceed执行的是login方法本身,相应的,只有执行到方法本身时候,才会调用相对应的前置通知和后置通知。
因此也就有了这里环绕通知始终在最外侧的体现。
使用以上程序代码,在 Object obj = joinPoint.proceed();处打一个断点,再次访问localhost:8080/user/login 就会发现,其内部是使用了CGLIB代理,通过这个代理调用了方法本身,也就是login方法。
如何利用环绕通知来统计方法的执行时间?
package com.example.demo.common;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Aspect // 切面
@Component // 不能忽略
public class UserAOP {
// 切点 (配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
public void pointcut() {
}
// 前置通知 @Before里面的参数取决于 切点的方法名
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知: " + LocalDateTime.now());
}
@After("pointcut()")
public void doAfter() {
System.out.println("执行了后置通知: " + LocalDateTime.now());
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知开始了");
long startTime = System.currentTimeMillis();
Object obj = joinPoint.proceed();
String methodName = joinPoint.getSignature().getName();
System.out.println("环绕通知结束了");
long endTime = System.currentTimeMillis();
long executionTime = endTime-startTime;
System.out.println(methodName+"执行时间为: "+executionTime);
return obj;
}
}
运行结果:
Spring AOP 是构建在动态代理基础上,因此Spring 对 AOP 的支持仅限于方法级别的拦截。
Spring AOP 支持 JDK Proxy 和CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用AOP会基于JDK生成代理类,没有实现接口的类,会基于CGLIB生成代理类。
织入(Weaving):代理的生成时机
织入是把切面应用到目标对象并创建新的代理对象的过程。
织入的过程可以在编译时,类加载时或运行时进行。根据织入的时间不同,有以下几种织入方式:
SpringAOP就是属于运行时织入切面的。
静态代理
静态代理是一种编译时就已经确认代理关系的代理方式。在静态代理中,代理类和被代理类都要实现同一个接口或继承同一个父类,代理类中包含了被代理类的实例,并在调用被代理的方法前后执行相应的操作。
静态代理的优点是实现简单,易于理解和掌握,但是缺点是需要为每个被代理类编写一个代理类,当被代理类的数量增多时候,代理量会变得很大。
动态代理
动态代理是一种在运行时动态生成代理类的代理方式。在动态代理中,代理类不需要实现同一个接口或继承同一个父类,而是通过Java反射机制动态生成代理类,并在调用被代理类的方法前后执行相应的操作。
动态代理的优点是可以为多个被代理类生成同一个代理类,从而减少了代码量,但是它的缺点是实现相对复杂,需要理解JAVA反射机制和动态生成字节码的技术。
总结:
虽然 Spring AOP 的支持仅限于方法级别,但是这足以满足大部分应用中的拦截和增强需求。对于更复杂的场景,如果需要更精细的控制和更广泛的横切逻辑,可以考虑使用 AspectJ 等更强大的 AOP 框架。
总结:
简单来说,JDK动态代理要求被代理类实现接口,而CGLIB要求被代理类不能是final修饰的最终类,在JDK8意思版本中,因为JDK动态代理做了专门的优化,所以它的性能比CGLIB高。
AOP是对某方面能力的统一实现,它是一种实现思想,Spring AOP是对AOP的具体实现,Spring AOP可通过AspectJ(注解)的方式来实现AOP的功能,SpringAOP的实现步骤是:
SpringAOP是通过动态代理的方式,在运行期将AOP织入到程序中,它的实现方式有两种:JDK Proxy 和CGLIB。