一、代理模式
1、代理模式(Proxy Pattern):指为其他对象提供一种代理,以控制对这个对象的访问。(结构型设计模式)
Spring AOP就是用代理模式实现的,包括事务代理、非侵入式日志监听等。
代理对象在客户端和目标对象之间起到中介作用。生活中的代理模式:(目标对象也即被代理对象)
2、适用场景
- 保护目标对象
- 增强目标对象
3、优点
- 能将代理对象与真实被调用的目标对象分离
- 一定程度上降低了系统的耦合度,易于扩展
- 代理可以起到保护目标对象的作用
- 增强目标对象的职责
4、缺点
- 代理模式会造成系统设计中类的数目增加
- 在客户端和目标对象之间增加了一个代理对象,会造成请求处理速度变慢
- 增加了系统的复杂度
5、类结构图:
Subject是顶层接口,RealSubject是真实对象(目标的对象),Proxy是代理对象,代理对象持有目标对象的引用,客户端调用代理对象方法,同时会也调用目标对象的方法,在代理对象前后增加一些处理。在代码中,我们想到代理,就会理解是代码增强,其实就是在原本逻辑前后增加一些逻辑,而调用者无感。代理模式属于结构型模式,有静态代理和动态代理。静态代理类在编译期就生成,而动态代理类则是在JVM运行时动态生成。静态代理的效率比动态代理相对高一些,但是静态代理代码冗余量大,一旦需要修改接口,代理类和目标对象类都需要修改(违背了开闭原则),动态代理则易于扩展。
二、静态代理
概念:显示声明目标对象。
例子1:人到了适婚年龄,父母总是迫不及待希望抱孙子。而现在社会的人在各种压力之下,都选择晚婚晚育。于是着急的父母就开始到处为自己的子女相亲,比子女自己还着急。这个相亲的过程,就是一种我们人人都有份的代理。来看代码实现:
定义一个顶层Person接口:
儿子要找对象,定义一个Son实现类:
父亲要帮儿子相亲,定义Father类:
测试代码:
运行结果:
例子2:在实际的分布式业务场景中,我们通常会对数据库进行分库分表,之后使用Java操作时,就可能需要配置多个数据源,我们通过设置数据源路由来动态切换数据源。
定义一个Order实体类:
定义一个OrderDao持久层操作类:
定义一个IOrderService接口:
创建OrderServiceImpl实现类:
接下来使用静态代理,主要完成的功能是:根据订单创建时间自动按年进行分库。根据开闭原则,原来写好的逻辑不改,通过代理对象来完成。先创建数据源路由对象,使用ThreadLocal单例实现,定义DynamicDataSourceEntry类:
创建切换数据源的代理OrderServiceImplStaticProxy 类:
测试代码:
运行结果:
类结构图:
三、动态代理
动态代理和静态代理对比基本思路是一致的,只不过用动态代理功能更加强大,随着业务的扩展适应性更强。如果还以找对象为例,使用动态代理相当于是能够适应复杂的业务场景。不仅仅只是父亲给儿子找对象,如果找对象这项业务发展成了一个产业,进而出现了媒婆、婚介所等这样的形式。那么,此时用静态代理成本就更大了,需要一个更加通用的解决方案,要满足任何单身人士找对象的需求。我们升级一下代码,先来看JDK实现方式:
1、JDK Proxy实现方式
例子1:定义一个媒婆(婚介)JDKMeipo类:
定义一个单身客户Customer类:(JDK动态代理的目标对象一定要实现接口,要扫描接口中的所有方法进行实现和覆盖)
测试代码:
运行结果:
例子2:数据源动态路由业务,定义一个OrderServiceImplDynamicProxy类:
测试代码:
运行结果:
依然能够达到相同的结果。但是,动态代理实现之后,我们不仅能实现Order的数据源动态路由。当然,有比较重要的约定,必须要求实现getCreateTime()方法,因为路由规则是根据时间来运算的。当然我们也可以通过接口规范来达到约束的目的,这里不再举例。
JDK Proxy采用字节码重组,重新生成新的对象来代替原始对象,以达到动态代理的目的。
JDK Proxy生成对象的步骤:
① 拿到目标对象的引用,并且通过反射获取它所有的接口
② JDK Proxy类重新生成一个新的类、同时新的类要实现目标类所有实现的接口
③ 动态生成Java代码,把新加的业务逻辑方法由一定的逻辑代码去调用(在代码中体现)
④ 编译新生成的Java代码,生成新的字节码
⑤ 再重新加载到JVM中运行
以上这个过程就叫字节码重组。JDK中有一个规范,在ClassPath下只要是$开头的class文件一般都是自动生成的。
2、CGLib调用API
简单看一下CGLib代理的使用,还是媒婆为例子,创建CglibMeipo类:
创建单身客户Customer类:(CGlib代理的目标对象不需要实现任何接口,它是通过动态继承目标对象实现的动态代理)
测试代码:
运行结果,同样满足要求:
提问:为什么Cglib动态代理执行代理方法效率比JDK的高?
因为Cglib采用了FastClass机制,它的原理简单来说就是:为代理类和目标类各生成一个Class,这个Class会为代理类或目标类的方法分配一个index(int类型)。这个index当做一个入参,FastClass就可以直接定位到要调用的方法直接进行调用,省去了反射调用,所以调用效率比JDK动态代理通过反射调用高。
Cglib和JDK动态代理对比
① JDK动态代理是实现了目标对象的接口,Cglib是继承了目标对象。
② JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。
③ JKD调用代理方法是通过反射机制调用,Cglib是通过FastClass机制直接调用方法,Cglib执行效率更高。
四、代理模式与Spring
1、代理模式在Spring源码中的应用
① 先看ProxyFactoryBean核心的方法就是getObject()方法,我们来看一下源码:
在getObject()方法中,主要调用getSingletonInstance()和newPrototypeInstance()。在Spring配置中,如何不做任何设置,那么Spring代理默认生成的Bean都是单例对象。如果修改scope则每次创建一个新的原型对象。newPrototypeInstance()里面的逻辑比较复杂,以后再研究,这里先做简单的了解。
② Spring利用动态代理实现AOP有两个非常重要的类,分别是JdkDynamicAopProxy类和CglibAopProxy类,类图如下:
2、Spring中的代理选择原则
1、当Bean有实现接口时,Spring就会用JDK的动态代理。
2、当Bean没有实现接口时,Spring会选择Cglib。
3、Spring可以通过配置强制使用Cglib,只需在Spring配置文件中加入如下代码:
五、静态代理和动态代理的本质区别
1、静态代理只能通过手动完成代理操作,如果目标类增加新的方法,代理类需要同步新增,违背开闭原则。
2、动态代理采用在运行时动态生成字节码的方式,取消了对目标类的扩展限制,遵循开闭原则。
3、若动态代理要对目标类的增强逻辑扩展,结合策略模式,只需要新增策略类便可完成,无需修改代理类的代码。