目录
1、AOP理解
2、使用场景
3、AOP组成
3.1、切面(类)
3.2、切点(方法)
3.3、通知(方法具体实现代码)
3.4、连接点
4、实现AOP
4.1、添加Spring Boot AOP框架
4.2、创建切面和切点
4.3、创建通知
4.3.1、前置通知方法
4.3.2、后置通知方法
4.3.3、环绕通知方法
4.4、创建连接点
5、Spring AOP实现原理
5.1、JDK动态实现
5.2、CGLIB动态实现
5.3、JDK和CGLIB实现的区别
5.3.1、JDK
5.3.2、CGLIB
6、AOP和IoC的区别
AOP:是面向切面编程,是一种思想,对某一类事情的集中处理
举个例子例子来说明:例如你访问一个网站(可以登录的那种),登录后服务器这边是判定你登录状态的,进入每个页面也都会判定一下,如果没有被判定为登录,就会跳转到登录页面进行登录,那每个也页面都需要写一个判定登录,这不就很麻烦了嘛(代码冗余)
使用AOP思想:我们只需要在某一处配置一下,所有需要的判断用户登录页面,全部可以实现用户验证,不再需要每个页面都写一遍验证代码
前面举例登录验证就是一个比较常用的场景,处理登录注册都会使用到AOP,当然也不仅止于登操作,对于这种功能统一,且使用的地方较多的功能,就可以考虑AOP来统一处理
实现场景:
<1>统一日志记录
<2>统一方法执行时间统计格式
<3>统一的返回格式设置
<4>统一的异常处理
<5>事务的开启和提交等
也就是说使用AOP可以扩充多个对象的某个能力,针对原来的OOP(面向对象)算是一个升级版的
切面是指某一方面的具体内容就是一个切面的,判定用户登录就是一个切面,时间格式统一处理也是一个切面,就是我们要通过AOP执行的一个具体内容
在执行后,但尚未执行完,进行拦截验证,我们需要判定的内容
制作一个规则作为拦截处理(定义拦截规则)
切点有了,操作是拦截下来,总要干点什么吧
执行AOP逻辑业务(以下会通过代码给友友们演示其作用)
<1>前置通知:在目标方法(真正要执行的方法)调用之前执行的通知(涉及注解@Before)
<2>后置通知:在目标方法调用之后执行的通知(涉及注解@After)
<3>环绕通知:在目标方法调用前、后都会执行的通知(涉及注解@Around)
<4>异常通知:在目标方法抛出异常的时候执行的通知(涉及注解@AfterThrowing)
<5>返回通知:在目标方法返回的时候执行通知(涉及注解@AfterReturning)
所有可能触发切点的点叫做“连接点”
举个例子:
我们先创建一个Spring boot项目,以下我们直接跳转到选择依赖的部分和版本,因为我这里使用的是JDK8所以使用2版本的,如果友友使用的是JDK17以上包括JDK17的这里使用3版本的( 友友们不是很了解前面的创建步骤的,可以看看这篇博客)
这里会有友友们问为啥这里不添加spring AOP的依赖,其实这里是搜不到的,所以我们需要自己去引入,引入之前先做好一系列前置操作
我们去maven仓库,引入一下依赖(直接在浏览地址栏中输入mvn既可):
将依赖放入pom.xml文件中:我这里也直接给出依赖(不加版本的依赖,Spring boot会自己帮你配置对应的版本)
org.springframework.boot spring-boot-starter-aop
这些操作都算是工具或者组件,就把他们放在common层或者config层就行
我们这里就拿common层为例:
@Aspect //注解就是一个切面那个单词
@Component //不能省略 为啥嘞??我们得随着项目一起启动,要么要我们干啥
public class UserAOP {
//切点 (配置拦截规则) AspectJ的语法
@Pointcut("execution(* com.example.demo.controller.Usercontroller).*(..)")
public void pointcut(){
}
}
注:pointcut方法就是一个空方法,它不需要有方法体,此方法名就是起到一个“标识”的作用,标识下面的通知方法具体指的是哪个切点(切点可能会是多个)
这里@Pointcut注解就是切点的注解,参数内容是什么意思,先接收一点基础内容:
AspectJ支持三种通配符
*:匹配任意字符,只匹配一个元素(包,类,方法和方法参数)
..:匹配任意字符,可以匹配的多个元素,在表示类时,必须和*联合使用
+:表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的
所有⼦类包括本身
execution()是最常用的切点函数,用来匹配方法,语法为:
execution(<修饰符><返回类型><包。类.方法(参数)><异常>)
就拿我们写的这个来解释一下
这里为什么没有看见 修饰符和异常呢,修饰符和异常可以写也可以不写,修饰符不写的时候,我们想写什么修饰的就写什么修饰的,也就不用注意了(修饰符是啥:public ,private这些就是)
简单的看几个案例来理解execution():
execution(* com.example.demo.User.*(..)):配置User类中的所有方法
execution(* com.example.demo.User+*(..)):匹配User类以及User子类的所有方法
execution(* com.example.demo.*+*(..)):匹配 com.example.demo包下的所有类的所有方法
execution(*addUser(String,int)):匹配addUser方法,且第一个参数类型是String,第二个参数类型int
通知就是以上提及的5种,切点拦截了,该通过“通知”来写逻辑业务了(这里我们就写3种通知方法作为演示)
这里注解@Before中参数就是我们写切点时的标识方法
@Before("pointcut")
public void doBefore(){
System.out.println("执行了前置通知:"+ LocalDateTime.now());
}
只有注解不同
@After("pointcut")
public void doAfter(){
System.out.println("执行了后置通知"+LocalDateTime.now());
}
环绕通知是什么意思:就是既执行前置方法,也执行后置方法,但是它的前置方法和后置方法与@Before注解和@After注解不是同一个(一会创建连接点了给友友们展示结果)
//环绕通知
@Around("pointcut")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("前置执行环绕通知");
Object obj=joinPoint.proceed();
System.out.println("后置执行环绕通知了");
return obj;
}
这里需要注意的就是两个点,一个是ProceedingJoinPoint类,它就是一个连接点的操作类,通过它定义的参数进行控制,如果你只想执行“前置执行环绕通知”那执行就是,不需要执行ProceedingJoinPoint类定义的参数操作,这里joinPoint.proceed()就是允许继续执行,它就是这里所有通知老大,它决定是否还继续执行
创建连接点,就是创建我们要操作涉及AOP的方法(这里主要时为了演示,没有实际意义):
@RestController
public class UseController {
@RequestMapping("/user/sayhi")
public String sayhi(){
System.out.println("执行了 sayhi 方法");
return " spring boot aop ";
}
}
运行结果及分析:
Spring AOP是以代理的基础上实现的;说到代理就被划分为动态代理和静态代理
静态代理:程序运行前就已经定义好代理类,写固定了,需要为每一个被代理对象创建一个代理类,不灵活
动态代理:通过反射机制动态地生成代理类,代理类与被代理类实现相同的接口或者继承相同的父类,并在方法调用前后进行额外的操作,在实现的技术⼿段上,都是在 class 代码运⾏期,动
态的织⼊字节码
Spring AOP基于动态代理,Spring对AOP的支持局限于方法的拦截
Spring AOP支持JDK Proxy和CGLIB方式实现动态代理。默认情况下,实现了接口的类,这个接口类就是AOP基于JDK生成代理类,没有实现接口类的话就会基于CGLIB生成代理类
Spring AOP代理
<1>CGLIB的动态代理框架,主要作用就是根据目标类和方法,动态生成代理类
<2>Java中的动态代理框架,几乎都是依赖字节码框架(ASM,Javassist等)实现的
注:字节码框架直接操作class字节码的框架,可以加载已有的class字节码文件信息,修改部分信息,或动态生成一个class
这个代理是怎么进行处理的:
调用者想要调用目标对象,前面说了AOP是干什么的了,拦截嘛,所以代理会帮我们先进行调用过滤掉一些不需要的,再将目标对象交给调度者
织入(Weaving):代理生成的时机
织入是把切面用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中
织入时机:
<1>编译期:切面在目标类编译时期被织入,例如:lombok在编译时期就生成的代码,编译过后就会生成字节码
<2>类加载期:切面在目标类加载的JVM时被织入,例如JVM有一个垃圾回收的类,JVM启动时这个垃圾回收的线程就会启动也就是类加载时期进行织入的
<3>运行期:切面在应用执行时刻被织入,例如:AOP容器会为目标对象动态一个代理对象,Spring AOP的这个代理在运行时才会织入
JDK实现,先通过实现InvocationHandler接口创建方法调用处理器,再通过Proxy来创建代理类
下面简单了解一下:
import com.example.demo.service.UserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JDKDynamicProxy implements InvocationHandler {
//⽬标对象即就是被代理对象
private Object target;
public JDKDynamicProxy( 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("记录开始时间");
//5、通过反射调⽤被代理类的⽅法 也就是接口过来重写的方法
Object retVal = method.invoke(target, args);
//<4>.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
//1、 实例 ,重写了接口的方法,但是这里不会执行内部重写的方法
UserService target= new UserService() {
@Override
public void sayhi() {
System.out.println("hello world");;
}
};
//2、⽅法调⽤处理器
InvocationHandler handler =
new JDKDynamicProxy(target);
//3、创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
UserService proxy = (UserService) Proxy.newProxyInstance(
//第一个参数是代理类,我们开始创建的
target.getClass().getClassLoader(),
//第二参数是被代理实现的接口
new Class[]{UserService.class},
//方法嗲用处理器
handler
);
//4、这里的 proxy 代理 在调用目标方法时就会 调用 到 invoke 最后再调用目标方法
proxy.sayhi();
//System.out.println(proxy.sayhi());
}
}
注:这里的 UserService是我们自己写的接口,友友们如果是想复制看看效果一定要自己写一个接口放上去
运行结果:
CGLIB不需要通过接口来实现动态代理,而是通过被代理类的子类作为代理
我们创建的类需要实现MethodInterceptor接口重写intercept方法,也就是我们现在写的
import com.example.demo.service.ArticleService;
import com.example.demo.service.UserService;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CGLIBDynamicProxy implements MethodInterceptor {
//被代理对象
private Object target;
public CGLIBDynamicProxy(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("记录开始时间");
//3、通过CGLIB的代理⽅法调⽤
Object retVal = methodProxy.invoke(target,args);
//<4>.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
//1、实例 该接口一个子类作为动态代理类
UserService target=new ArticleService();
//1、开始创建CGLIB动态代理: 第一参数被代理类的子类 第二个参数 实例一个将被代理类的子类
UserService proxy= (UserService) Enhancer.create(target.getClass(),new CGLIBDynamicProxy(target));
//2、代理执行
proxy.sayhi();
}
}
注:这里UserService是接口,ArticleService一个类继承了UserService接口
<1>JDK 动态代理是基于接口实现的,它要求被代理对象实现一个接口,通过 InvocationHandler 及 Proxy在运⾏时动态的在内存中⽣成了代理类对象,该代理类是在运⾏期时,动态的织⼊统⼀的业务逻辑字节码来完成
性能:JDK动态代理性能相对较高,生成代理对象速度较快
代理条件:JDK动态代理无法代理fina类和final方法
<1>CGLIB 动态代理则是通过继承被代理对象来实现的,它不需要被代理对象实现接口,而是直接生成一个被代理对象的子类作为代理对象
性能:CGLIB动态代理性能相对较低,生成代理对象较慢
代理条件:CGLIB动态代理可以代理任意类的方法,但是CGLIB被代理类不能是final修饰的最终类
注:JDK8以后包括JDK8版本,JDK动态代理优化后相比CGLIB性能好
相似之处:Spring AOP就提供了AOP思想的框架,它俩的关系类似于:IOC是思想和DI是IoC的实现
不同之处:
AOP通过动态代理机型方法级别的拦截,调用者调用目标方法会先通过动态代理,在再拿到目标方法(总的来说其实还是自己去调用的方法)
相比IoC容器,IoC是将对象都实例好存储Spring中,调用的时候也通过Spring拿的(通过Spring获取对象后进行调用,Spring中的IoC)