相信各位读者对于Spring AOP的理解都是一知半解,只懂使用,却不懂原理。网上关于Spring AOP的讲解层出不穷,但是易于理解,让人真正掌握原理的文章屈指可数。笔者针对这一痛点需求,决定写一篇关于Spring AOP原理的优质博客。这篇文章深入浅出,层次递进,循序渐进讲解Spring AOP的底层原理。全文通俗易懂,图文并茂,原理和源码相结合,让你做到知其然知其所以然。通篇阅读预计半小时,相信你读完后,对Spring AOP会有全新的理解,有质的飞跃,面试时也能得心应手,回答游刃有余。
欢迎关注+点赞+收藏~
静态代理
Service层中包含了哪些代码?
Service层中 = 核心功能(几十行、上百代码)+ 额外功能(附加功能)
- 核心功能:业务运算、Dao调用等
- 额外功能:不属于业务、可有可无、代码量很小。比如事务、日志、性能等
举个现实生活中的例子:对于房东这个实体来说,他出租房屋主要包括广告看房和签合同收钱。但是他觉得自己一个人经常发广告看房太累了,只想签合同收钱。也就是对他来说发广告看房是额外功能,签合同收钱才是核心功能。
如果没有中介(代理人):
如果有中介(代理人):
这其实就是现实生活中,代理模式的影子。
那么什么是代理模式呢?
概念:通过代理类,为原始类(目标类)增加额外的功能。
使用代理设计模式的好处:利于原始类(目标类)的维护。
- 目标类(原始类):指的是业务类(核心功能 --> 业务运算 DAO调⽤)
- 目标方法(原始方法):目标类(原始类)中的方法
- 额外功能(附加功能):日志、事务、性能
代理模式的核心:
代理类 = 原始类(目标类)+ 额外功能 + 原始类(目标类)实现相同的接口
也就是说在实现代理模式时,我们需要重点关注三点:原始类、额外功能、接口。其中原始类中执行的肯定是核心功能。额外功能其实是定义在代理类中的,在代理类中可以添加一些额外功能,并且在代理类中会引入原始对象(即原始类的对象),当代理类执行完额外功能后,就会通过原始对象去调用原始类中的核心功能。接口就是定义了一组行为规范,原始类和代理类都必须实现相同的接口。
下面是静态代理模式的一个简单代码示例:
- 接口:UserSerivce。在这个接口中,我们会定义两个方法,即register方法和login方法。
- 原始类:UserServiceImpl。这个原始类会实现UserSerivce接口中定义的方法,然后在这些方法中实现核心功能。
- 额外功能:比如图片中的输出语句。UserServiceIProxy是静态代理类,它和原始类UserServiceImpl都要实现相同的UserSerivce接口中定义的方法。我们可以看到在UserServiceIProxy静态代理类中引入了原始类UserServiceImpl的原始对象userService。当在静态代理类中做完额外功能后,就会通过这个原始对象userService去调用原始类UserServiceImpl中的核心功能。
从上面我们可以知道,静态代理最大的特点:有一个原始类,就必须要有一个对应的静态代理类(比如原始类UserServiceImpl和对应的代理类UserServiceProxy;原始类OrderServiceImpl和对应的代理类OrderServiceProxy)。这些代理类都要由程序员手动写出来。为每⼀个原始类,手工编写⼀个代理类 (.java .class)。
可以总结出静态代理的缺点:
- 静态类文件数量过多,不利于项目管理;
- 额外功能维护性差,静态代理类中额外功能修改复杂;
由此引出下文的动态代理。
Spring动态代理入门
Spring动态代理的开发步骤:
- 创建原始对象(目标对象)
- 定义额外功能(附加功能)
- 定义切点
- 组装切面
-
创建原始对象(目标对象)
-
定义额外功能(附加功能)
Spring提供了MethodBeforeAdvice接口,这个接口中有一个before方法需要我们去实现。我们把额外功能书写在接口的实现中,它会在原始方法执行之前运行额外功能。
- 定义切入点
切入点的定义:额外功能加入的位置。
目的:由程序员根据自己的需要,决定将额外功能加入给哪个原始方法。(比如register方法、login方法)
如果将所有方法都作为切入点,都加入额外的功能。那么可以写成如下:
- 组装切面(将第二步定义好的额外功能与第三步定义的切入点进行整合,组装成切面)
完美图解:
那么我们该如何获得Spring工厂创建的动态代理对象,并进行调用呢?
正常来说,通过ctx.getBean("userService")
得到的应该是UserServiceImpl
这个类的对象。但在动态代理中,其实不是这样的。请注意,它实际上得到的是代理类的对象。
获取代理对象后,怎么确定这个代理对象的类型呢?
在静态代理中,我们知道代理类和原始类都要实现相同接口。在动态代理中其实也是一样的!我们可以通过原始类看它实现什么接口,那么这个代理对象的类型就是该接口。比如这里原始类UserServiceImpl
实现UserService
接口,那么这个代理对象的类型就是UserService
。
我们来实践出真知:
可以发现这和我们的结论确实是一致的!
Spring创建的动态代理类在哪里呢?
在上述的动态代理开发中,我们并没有编写任何像静态代理中的UserServiceImplProxy
代理类,根本就没有出现代理类的影子。那么Spring创建的动态代理类在哪里呢?
原理:Spring框架在运行时,通过动态字节码技术,在JVM中创建动态代理类,运行在JVM内部,等程序结束后,会和JVM⼀起消失。
动态字节码技术:它并不需要.java
和.class
,而是直接通过第三方的动态字节码框架,在JVM中创建对应类的字节码,进而创建对象,当虚拟机结束,动态字节码跟着消失。
动态代理的特点:动态代理不需要定义任何代理类文件,都是JVM运行过程中动态创建的,所以不会造成静态代理中类文件数量过多,影响项目管理的问题。
动态代理编程简化代理的开发
在额外功能不改变的前提下,创建其他原始类的代理对象时,只需要指定原始对象即可。
比如我们要创建OrderServiceImpl
这个原始类时,只需要在配置文件中指定原始对象即可:
动态代理额外功能的维护性大大增强
比如我们想要修改之前创建的额外功能before类,但其实我们并不需要在这个类中直接修改。而是可以新建一个额外功能before1类,在这个新类中完成添加额外功能操作。
Spring动态代理详解
从上面我们知道Spring动态代理开发四步骤:
下面我们将主要详细分析第二步额外功能和第三步切入点。
额外功能详解
分析before方法中各个参数的具体含义:
(1)Method method
它表示我们需要把这个额外功能添加到哪个原始方法中。如我们想要给register原始方法、login原始方法加入这个额外功能,那么这里method就是register或者login。
(2)Object[] agrs
它表示额外功能所增加给的那个原始方法中所含有的形参。
比如参数method是login原始方法,那么这里args对应的就是login方法中的参数。如下图,login方法中的参数是String name和String password。因此这里args数组对应的就是这两个参数。
比如参数method是register原始方法,那么这里args对应的就是register方法中的参数。如下图,register方法中的参数是User user。因此这里args数组对应的就是这个参数。
由此可知,args和method是息息相关的。
(3)Object target
它表示原始对象。比如原始类UserServiceImpl它的原始对象就是userService,原始类OrderServiceImpl它的原始对象就是orderService。
实践出真知:
MethodInterceptor(方法拦截器)
MethodBeforeAdvice和MethodInterceptor的区别:
- MethodBeforeAdvice:额外功能只能运行在原始方法前。
- MethodInterceptor:额外功能可以运行在原始方法之前或者原始方法之后或者原始方法前后。
我们编写一个类Around实现MethodInterceptor接口,这个接口中有一个invoke方法需要我们实现。这里方法中有一个形参MethodInvocation invocation。invoke方法的作用:额外功能书写在invoke中。
我们知道在MethodInterceptor中,额外功能可以运行在原始方法之前或者原始方法之后或者原始方法前后。那怎么确定这个额外功能到底要运行在原始方法之前、之后、前后呢?这个额外功能的运行时机我们怎么在invoke方法中体现出来呢?
在实现invoke方法过程中,我们必须要确定:原始方法怎么运行?
答:一旦我们确定原始方法怎么运行了,那么在原始方法之前写的额外功能就是运行在原始方法之前,在原始方法之后写的额外功能就是运行在原始方法之后,前后都写了就是运行在原始方法的前后。
我们需要先来掌握invoke这个方法的参数。
- MethodInvocation invocation:它表示额外功能所要增加给的那个原始方法。如果我们所开发的这个额外功能是添加给login,那么invocation就代表login方法。
它有点类似于MethodBeforeAdvice中的Method method,不过它是对method的高级封装。
好了,现在我们已经知道invocation就代表原始方法。但是我们想要知道的是原始方法是怎么运行呢?
从源码中可以看出,其实invocation.proceed()
就表示原始方法的运行。
- 如果我们把额外功能添加给login原始方法,那么
invocation.proceed()
就代表login这个原始方法的运行; - 如果我们把额外功能添加给register原始方法,那么
invocation.proceed()
就代表register这个原始方法的运行。
我们再来看一下invoke方法的返回值:
返回值Object其实代表的是原始方法的返回值。也就是说,如果invocation代表login方法,那么Object其实就是login方法的返回值。
(1)额外功能运行在原始方法执行之前的代码示例
(2)额外功能运行在原始方法执行之后的代码示例
(3)额外功能运行在原始方法执行前后的代码示例
什么样的额外功能需要运行在原始方法执行的前后呢?
事务!如下图,原始方法执行之前开启事务,原始方法之前之后提交事务。
MethodInterceptor接口中invoke里面的额外功能也可以运行在原始方法抛出异常的时候:
总结MethodInterceptor中额外功能所运行的四个时机:
- 额外功能运行在原始方法执行之前
- 额外功能运行在原始方法执行之后
- 额外功能运行在原始方法执行前后
- 额外功能运行在原始方法抛出异常时
MethodInterceptor影响原始方法的返回值
如果原始方法的返回值直接作为invoke方法的返回值返回时,那么MethodInterceptor不会影响原始方法的返回值。
那么如果我们想要让MethodInterceptor影响原始方法的返回值呢?此时,我们不要在invoke中把原始方法的返回值作为invoke方法的返回值直接返回了。
切入点详解
切入点表达式分为三种:
- 方法切入点表达式
- 类切入点表达式
- 包切入点表达式
(1)方法切入点表达式
定义login方法作为切入点:* login(..)
定义login方法且login方法有两个字符串类型的参数作为切入点:* login(String, String)
这里我们使用的参数类型是String,它是java.lang包中的类型。
注意:如果此时的参数类型是非java.lang包中的类型,那么必须要写全限定名。比如* register(com.baizhiedu.proxy.User)
。
注意:..
可以和具体的参数类型连用。比如* login(String, ..)
可以代表login(String)
,login(String,String)
,login(String,com.baizhiedu .proxy.User)
。
上面所讲解的这种切入点表达式并不精准!
如下图所示,如果使用* login(..)
,那么下图中的所有login方法都将可以匹配。
那么如何才能做到精准呢?
精准方法切入点限定:
(2)类切入点表达式
指定特定类作为切入点(额外功能加入的位置),自然这个类中的所有方法,都会加上对应的额外功能。
使用* *.UserServiceImpl.*(..)
,只能处理一层包。比如com是第一层包,然后UserServiceImpl类是在com这个包下,那么就是可以的。假设UserServiceImpl类是在com.baozhiedu.proxy包下,此时这是三层包,那么就不能正确切入。
那如果我非要把UserServiceImpl类是在com.baozhiedu.proxy包下呢?那么就应该写成:* *..UserServiceImpl.*(..)
,增加..
其实就表示多级包。
(3)包切入点表达式
注意上面的这种写法* com.baizhiedu.proxy.*.*(..)
必须要求UserServiceImpl类和OrderServiceImpl类都在proxy包中,而不能存在于proxy的子包中。
那么该如何处理上面的这个情况呢?可以写成* com.baizhiedu.proxy..*.*(..)
。
以上三种切入点表达式中,最具实战价值的是包切入点表达式。
切入点函数
切入点函数的作用:用于执行切入点表达式。
切入点函数有:execution、args、within、@annotation。
(2)args
args代码示例:
(3)within
within代码示例:
(4)@annotation
@annotation的代码示例:
切入点函数的逻辑运算
and与操作的代码示例:
注意:and操作不适用于同种类型的运算,比如execution() and execution()是不行的。代码示例如下:
or或操作符的代码示例:
Spring AOP详解
- AOP(Aspect Oriented Programing):⾯向切⾯编程 = Spring动态代理开发 以切⾯为基本单位的程序开发,通过切⾯间的彼此协同,相互调⽤,完成程序的构建。切面 = 切入点 + 额外功能。
- OOP (Object Oritened Programing) ⾯向对象编程 Java 以对象为基本单位的程序开发,通过对象间的彼此协同,相互调⽤,完成程序的构建。
- POP (Producer Oriented Programing) ⾯向过程(⽅法、函数)编程 比如C语言。以过程为基本单位的程序开发,通过过程间的彼此协同,相互调⽤,完成程序的构建。
AOP:本质就是Spring动态代理开发,通过代理类为原始类增加额外功能。
好处:利于原始类的维护。
注意:AOP编程不可能取代OOP,OOP编程有意补充。
AOP编程的开发步骤
- 原始对象
- 额外功能(MethodInterceptor)
- 切入点
- 组装切面(额外功能+切入点)
- 切面 = 切入点 + 额外功能
从几何角度来看,面 = 点 + 相同的性质。
有两个核心问题:
- AOP如何创建动态代理类(动态字节码技术)
- Spring工厂如何加工创建代理对象(通过原始对象的id值,获得的是代理对象)
JDK动态代理
Proxy.newProxyInstance
方法参数详解:
可以看到,newProxyInstance方法中含有三个参数:
ClassLoader loader
:它表示代理类的类加载器。传递给newProxyInstance的类加载器应该是已经被加载到内存中的类或接口的类加载器。这个类加载器将用来定义生成的代理类。Class<?>[] interfaces
:它表示代理类所需要实现的接口列表。生成的代理类将实现这些接口中的每一个,使得代理实例可以被安全地转型为这些接口类型的任何一个。这些接口定义了代理实例可以调用哪些方法。InvocationHandler h
:这是一个处理接口方法调用的调用处理器。当代理实例的方法被调用时,方法调用将被转发到这个调用处理器。这个处理器的invoke方法负责决定如何处理代理实例上的方法调用。这包括调用实际对象的方法、返回一个值或抛出一个异常等。这是实现动态代理功能的核心,允许开发者在运行时动态改变方法的行为。
在Java中,InvocationHandler 接口定义了一个非常重要的方法 invoke,它是实现动态代理的关键。当一个代理实例的方法被调用时,该方法就会被触发。InvocationHandler 的 invoke 方法有三个参数,每个参数都有其特定的用途和意义:
Object proxy
:这是代理类的实例本身,即调用方法的代理对象。通常不直接使用这个对象来调用方法,因为它可能会导致无限递归调用代理方法自身。这个参数主要用于反射相关的信息,比如可以通过它获取代理类的信息,但在方法内部调用它的其他方法时需要小心处理。Method method
:额外功能所要增加给的那个原始方法。这是正被调用的方法的反射对象。这个 Method 对象包含了关于正在被调用的方法的所有元数据,如方法名、返回类型、参数类型等。通过这个对象,你可以访问到任何关于这个方法的信息,甚至可以通过反射来调用它。Object[] args
:它表示原始方法中的形参。这是一个包含了传递给方法的所有参数的数组。这些是调用方法时实际使用的参数值。如果被调用的方法没有参数,则 args 将是一个长度为0的数组。通过这个数组,你可以修改、替换或调整传入方法的参数值,或者在调用原方法之前对它们进行处理。
下面的这幅图中classloader借用的是类TestJDKProxy的classloader:
下面的这幅图中classloader借用的是UserService的classloader:
CGlib动态代理
CGlib创建动态代理的原理:父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样既可以保证二者方法⼀致,同时在代理类中提供新的实现(额外功能+原始方法)。
如何通过cglib方法创建动态代理对象呢?
回顾一下JDK动态代理的过程,Proxy.newProxyInstance(classloader, interfaces, invocationhandler)
。这个方法中需要用到三个参数。其实cglib也是同样的。只不过cglib中没有接口,因此不需要interfaces。但是cglib中是子类继承父类,因此需要保证继承关系。cglib需要三个参数:
Enhancer.setClassLoader()
:等效于JDK动态代理中的classloaderEnhancer.setSuperClass()
:等效于JDK动态代理中的interfacesEnhancer.setCallback()
:等效于JDK动态代理中的invocationhandler。不过使用的是cglib中的MethodInterceptor而不是Spring提供的MethodInterceptor。
代码示例:
JDK动态代理和CGlib动态代理的总结:
- JDK动态代理
Proxy.newProxyInstance()
通过接⼝创建代理的实现类 - Cglib动态代理 Enhancer 通过继承⽗类创建的代理类
现在我们来思考一下"为什么Spring通过原始对象的id值,获得的是代理对象"呢?
要弄明白这个细节,需要把BeanPostProcessor和JDK动态代理/CGlib动态代理结合起来分析。
下面这张图是创建代理过程中Spring通过BeanPostProcessor完成的对原始类UserServiceImpl的加工过程。
首先我们通过bean创建了原始类UserServiceImpl的原始对象,这里设置id是userService,因此userService就是UserServiceImpl原始类的对象。
Spring工厂第一步还是创建出了userService这个原始对象,它调用构造方法把原始对象创建好了。按照BeanPostProcessor加工时机来说,此时应该要进入postProcessBeforeInitialization对它进行加工。但是我们知道不一定都要进行postProcessBeforeInitialization和postProcessAfterInitialization这两次加工。因为我们很少做初始化操作。所以postProcessBeforeInitialization初始化之前的加工和postProcessAfterInitialization初始化后的加工实际上它们大概所起的作用是一样的。那么我们就可以不做postProcessBeforeInitialization这个加工处理了,在postProcessBeforeInitialization中直接return bean交还给Spring即可。总的来说就是创建完成userService这个原始对象后直接交给postProcessAfterInitialization这个方法来处理。
把创建好的原始对象userService传递给postProcessAfterInitialization方法的第一个参数Object bean。在postProcessAfterInitialization方法中的加工,要注意此时不像我们之前做的只是对属性进行简单的改变,我们要让userService这个原始对象通过我们的加工,最后把这个代理对象给创建出来。所以实际上从整个加工代码来讲,我们就用相应代理创建的底层代码来完成了。可以看到postProcessAfterInitialization方法中使用了JDK动态代理来创建出userServiceProxy代理对象。即通过Proxy.newProxyInstance()
把userService原始对象加工成最终我们需要的userServiceProxy代理对象。
这里关注一下Proxy.newProxyInstance()
中的三个参数从何而来呢?
- 对于classloader,我们随便借一个就行。
- 对于interfaces,它是原始对象所实现的接口,这个原始就是userService,它传参给了bean,因此我们可以通过
bean.getClass().getInterfaces()
得到原始对象所实现的接口。 - 对于invocationHandler,我们实现这个接口即可。
最后我们把加工后得到的代理对象userServiceProxy作为返回值交给Spring工厂。因此后续通过ctx.getBean("userService")
得到的就是代理对象userServiceProxy而不是原始类UserServiceImpl它所对应的原始对象了。
代码示例:
Spring AOP编程
加入@Around()注解就等同于我们编写了一个类MyArround去实现了MethodInterceptor接口。然后我们声明的arround()方法(这个方法可以随便命名)就等同于里面的invoke方法。arround()方法的返回值就等同于invoke方法的返回值。arround()方法中的joinpoint就等同于invoke方法的invocation。
代码示例:
- 切入点复用
在切面类中定义一个方法,上面有@Pointcut注解。通过这种方式,定义切点表达式,后续更加有利于切入点复用。
如下图,出现了代码冗余:
解决切入点复用,如下图:
- 基于注解的AOP编程中动态代理的创建方式
回顾AOP底层实现中的两种代理:
- JDK 通过实现接口 做新的实现类方式 创建代理对象
- Cglib通过继承父类 做新的子类 创建代理对象
那么基于注解的AOP编程中动态代理采用的是JDK还是Cglib呢?
从上图中可以看到,默认情况下AOP编程(包括传统的AOP开发和基于注解的AOP开发)底层采用的是JDK动态代理创建方式。
那么如果我们想要采用Cglib呢?
如下图,针对基于注解的AOP编程,我们可以在aop:aspectj-autoproxy
中将proxy-target-class设置为true,那么就可以将默认的JDK动态代理修改为Cglib动态代理了。
如下图,针对传统的AOP开发,我们可以在aop:config标签中将proxy-target-class设置为true。
Spring AOP开发中的坑点
坑点:在同一个业务中,进行业务方法间的相互调用,只有最外层的方法,才是加入额外功能(内部的方法,通过普通的方式调用,都调用的是原始方法)。如果想让内层的方法也调用代理对象的方法,就要ApplicationContextAware获得工厂,进而获得代理对象。
先来看下图,给UserServiceImpl类中的所有方法(这里是register方法和login方法)都添加额外功能:
如下图,我们在register方法中调用了login方法,然后把测试类中的userService.login方法注释掉。从输出结果中可以看出,只给register方法添加了额外功能,并没有给login方法添加额外功能。这显然不符合我们的预期,按理来说应该是都要给register方法和login方法都添加上额外功能呀。为什么login方法没有执行额外功能呢?
我们重新来审视下面这个图,当测试类TestAspectProxt调用register方法时,我们是通过代理对象调的register方法(我们之前反复强调过ctx.getBean(“userService”)得到的是代理对象而并不是原始类UserServiceImpl它的原始对象)。然后我们在register方法中使用this调用了login方法,那么来思考一下这个login方法是属于哪个对象的?
从下图中显然看出login方法是属于this,而这里的this是指的类UserServiceImpl。我们知道UserServiceImpl是原始类,通过this.login它本质上是通过原始对象调的login方法。它并不是通过代理对象调的login。既然不是通过代理对象调的,那么显然你就不能拥有代理对象中添加的额外功能。因此这里只是普通的通过this调用本类UserServiceImpl中的login方法,没有涉及任何额外功能,那么输出结果肯定只有login方法中的输出内容,不会有代理对象中的额外功能。
明白了错因,如果想要解决问题,那么就要在register方法中拿到代理对象,进而通过代理对象调用login方法。最简单的办法就是如下图,把测试类中的这两行代码也写到register方法中:
但是我们知道Spring工厂是重量级资源,创建多个工厂ctx势必会占用大内存,一个应用中应该只能创建一个工厂。因此上述方法不推荐。
其实我们只需要实现ApplicationContextAware这个接口。由于在测试类中已经创建出了一个Spring工厂ctx,我们在UserServiceImpl类中定义一个ctx,然后实现ApplicationContextAware接口中的setApplicationContext方法,把测试类中创建好的那个Spring工厂直接赋值给UserServiceImpl类中的ctx。这样整体上就只有一个Spring工厂。
解决方法的代码示例: