本篇将介绍Spring AOP的相关原理。
一、代理模式
Spring 在实现AOP时使用了代理模式这种设计模式,什么是代理模式呢我们来了解一下。
代理模式可以理解为当我们需要调用某个类的方法时,在我们与这个目标类之间增加一个代理类,我们要使用目标类的方法时,通过代理类访间接访问,而不是直接访问,这个目标类的方法称之为被代理对象。
在我们生活中 ,其实很多地方都有使用代理模式的思想,例如当我们去租房子时,我们需要与房东去建立联系,但通常情况下我们并不会直接去联系房东,而是通过中介去联系房东。此时,我们就相当于调用方,而房东则是我们的被代理对象,而中介就是我们的代理类。
对于代理模式的实现,通常有两种方式,一种是动态代理,一种是静态代理。这两种代理的区别主要在于代理类的生成时间不同。静态代理的代理类在项目编译前就已经存在,而动态代理则是在项目运行期间根据被代理对象的情况动态生成代理类。下面我们通过实现前面租房子的场景具体了解一下这两种代理模式。
静态代理
在前面的租房子场景中,一共提到了三个角色租客(调用方)、中介(代理类)、房东(被代理类)。下面我们分别创建代码来代替这三个角色。
租客:
中介:
房东:
运行代码:
通过打印的日志可以发现,租客与房东之间都是通过中介来进行联系的,并且中介还可以在房东与租客之间新增一些处理逻辑。
上述这种静态代理的模式事实上存在很多缺陷,代理类是写死的,如果房东的业务做出了改变,我们还需要重新对代理类进行修改,例如我们房东需要新增一个出售门面的业务:
代理类中介也得跟者进行修改: 这也就意味着每修改一次被代理对象,代理类都得重新进行调整,这样不仅麻烦,还会降低我们的开发效率。因此在Spring AOP中并没有使用这种静态代理的模式,而是另外一种动态代理的模式。
动态代理
在Spring AOP中对于动态代理的实现方式有两种,一种是JDK动态代理,另一种则是CGLIB动态代理。
JDK动态代理
jdk动态代理是一种基于反射创建代理类的动态代理方式,使用这种方式的步骤如下:
- 创建一个接口,并以该接口的实现类作为目标对象的类。
- 创建InvocationHandler接口的实现类,重写其中的invoke方法,并在这个方法中调用我们的目标方法并编写一些我们需要增强的功能
- 通过Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)方法基于反射创建代理类。其中loader为类加载器,interfaces为被代理类所实现的接口,h为我们在上一步创建的InvocationHandler的实现类。
- 当调用目标方法时,使用前面的代理类来调用。
下面我们根据这个步骤来对前面的静态代理的代码来改造一下。
改造结果如下:
InvocationHandler的实现类:
创建目标方法所在类的接口,并让我们前面的房东类实现该接口:
最后创建代理类并使用:
执行结果如下:
可以发现代理类的增强效果已经生效了,并且目标方法也顺利执行了。
在使用JDK动态代理时有一些注意点,第一,只能代理有实现接口的类中的方法,未实现接口的类中的方法无法通过使用这种动态代理方式。第二,只能代理接口中定义的方法,接口中没有的方法,无法被定义,例如我们再往房东(Landlord)类中再添加一个test方法
通过前面我们根据JDK动态代理创建的代理类是无法代理这个方法的,
至于为什么只能代理接口中的方法,原因其实也很容易猜出来,通过上面创建代理类的代码可以发现我们将代理类强转成了Seller类,由此可以推断出这个代理类其实是Seller接口的一个实现类,并重写了里面的方法,并在方法里通过反射获取到另一个实现类,也就是目标方法所在类里重写的方法,然后再执行增强的内容和这些方法(目标方法)。因此,可以总结一下JDK动态代理可以使用的场景:实现了某个接口,并且接口中有定义方法,这样的类才能使用JDK动态代理,并且只能代理接口中有所定义的方法。
CGLIB动态代理
CGLIB动态代理是一种基于ASM的字节码生产库,它是通过继承来实现动态代理的。使用CGLIB动态代的具体步骤如下:
- 引入CGLIB依赖
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
- 创建被代理类,不再需要实现接口
- 创建MenthodIntercept的实现类,并通过intercept方法(和JDK动态代理中的invoke方法类似)来对目标方法进行增强
- 通过Enhancer的create(class type,Callbackcallback)创建代理类其中type为被代理类的类型,callback为上一步MenthodIntercept的实现类
- 使用代理类访问目标方法
接下来我们再将前面JDK版本的动态代理代码改为CGLIB动态代理的
创建MenthodInterCept的实现类:
创建代理类 修改房东类,不需要再实现接口:
使用CGLIB动态代理无需目标类实现接口,因此CGLIB对于绝大多数类都可以进行代理。
Spring AOP中对于动态代理的使用
在Spring AOP中对于CGLIB动态代理和JDK动态代理都进行了使用。具体使用哪种取决于配置项proxyTargetClass和目标类的情况,具体如下:
proxyTargetClass | 目标类是否实现接口 | 使用动态代理的类型 |
true | 是 | CGLIB动态代理 |
true | 否 | CGLIB动态代理 |
false | 是 | JDK动态代理 |
true | 否 | CGLIB动态代理 |
对于Spring Boot 和Spring framework 来说proxyTargetClass的默认值略有不同,在Spring Boot
2.X版本以前,这个值默认和Spring framework一样设置为false,而在Spring Boot2.X版本以后,Spring Boot将proxyTargetClass的默认值改为了true。
在Spring Boot中我们也可以通过配置文件手动对proxyTargetClass进行修改,配置项为pring.aop.proxy-target-class=false,但需要注意的是并不是将proxyTargetClass设为false,对于实现了接口的目标类就一定使用JDK动态代理,还需要结合具体情况来看,如果实现的接口里没有定义方法,是不能使用JDK动态代理的,因此这种情况下还是得使用CGLIB动态代理。