代理模式
代理模式(Proxy Pattern)的定义非常简单,是指为其他对象提供一种代理,以控制对这个对象的访问。代理对象在客服端和目标对象之间起到中介作用,代理模式属于结构型设计模式。使用代理模式主要有两个目的:一保护目标对象,二增强目标对象。
下面我们来看一下代理模式的类结构图:
Subject是顶层接口,RealSubject是真实对象(被代理对象),Proxy是代理对象,代 理对象持有被代理对象的引用,客户端调用代理对象方法,同时也调用被代理对象的方法,但是在代理对象前后增加一些处理。在代码中,我们想到代理,就会理解为是代码增强,其实就是在原本逻辑前后增加一些逻辑,而调用者无感知。
- 代理模式属于结构型模式,有静态代理和动态代理。
静态代理模式
- 静态代理模式比较简单,我们用一个业务场景来演示
在分布式业务场景中,我们通常会对数据库进行分库分表,分库分表之后使用Java操作时,就可能需要配置多个数据源,我们通过设置数据源路由来动态切换数据源。
- 创建Order订单实体类
- 创建OrderDao持久层
- 创建OrderService业务层
接下来使用静态代理,主要完成的功能是,根据订单创建时间自动按年进行分库。根据开闭原则,原来写好的逻辑我们不去修改,通过代理对象来完成。先创建数据源路由对象,我们使用ThreadLocal的单例实现,DynamicDataSourceEntry类:
- 创建切换数据源的代理OrderServiceSaticProxy类:
- 直接进行测试
- 测试结果
Proxy before method
静态代理类自动分配到【DB_2017】数据源处理数据
OrderService调用orderDao创建订单
OrderDao创建订单成功
Proxy after method
静态代理在使用的时候需要定义接口或者父类,被代理对象(目标对象)与代理对象一起实现相同的接口或者继承相同的父类, 通过调用这些对应的方法对目标对象进行扩展
静态代理在不修改目标对象的功能前提下,通过代理对象对目标对象进行扩展
静态代理下,代理对象需要和目标对象实现一样的接口,所以可能有多个代理类,如果接口更改的话,代理对象和目标对象都要进行更改。
动态代理模式
动态代理和静态对比基本思路是一致的,只不过动态代理功能更加强大,随着业务的扩展适应性更强。使用动态代理相当于是能够适应复杂的业务场景。
- 动态代理有两种实现模式
- JDK动态代理
- 对上面的代码进行简单的修改,就能够实现动态代理
- 简单的测试
其余的代码没有任何变化仅仅只是修改了代理类的形式,就可以完成相同的运行效果。
-
但是我们得不仅知其然,还得知其所以然
-
JDK Proxy生成对象的步骤如下:
- 拿到被代理对象的引用,并且获取到它的所有的接口,反射获取。
- JDKProxy类重新生成一个新的类、同时新的类要实现被代理类所有实现的所有的接 口。
- 动态生成Java代码,把新加的业务逻辑方法由一定的逻辑代码去调用(在代码中体 现)。
- 编译新生成的Java代码.class。
- 再重新加载到JVM中运行。
以上这个过程就叫字节码重组。 JDK中有一个规范,在ClassPath下只要是$开头的class 文件一般都是自动生成的。我们通过文件流输出到一个新的class文件,然后,利用反编译工具查看class的源代码。来看测试代码:
public class JDKProxyTest {
public static void main(String[] args) {
try {
Person obj = (Person)new JDKMeipo().getInstance(new Customer());
obj.findLove();
//通过反编译工具可以查看源代码
byte [] bytes = ProxyGenerator.generateProxyClass("$Proxy0",new Class[]{Person.class});
FileOutputStream os = new FileOutputStream("E://$Proxy0.class"); os.write(bytes); os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行之后,我们能在 E://盘下找到一个$Proxy0.class 文件。使用 Jad 反编译,得到
$Proxy0.jad文件,打开可以看到如下内容:
- 这就是JDK代理完成后的class字节码文件
我们发现$Proxy0 继承了 Proxy 类,(这就是为什么JDK动态代理的原对象必须要实现接口的原因)同时还实现了我们的 Person 接口,而且重写了findLove()等方法。而且在静态块中用反射查找到了目标对象的所有方法,而且保存了所有方法的引用,然后通过调用Proxy的InvocationHandler属性调用我们自己写的invoke方法,在重写的方法用反射调用目标对象的方法和我们写在InvocationHandler类中自己的增强方法。Proxy.class字节码文件中的所有代码都是JDK帮我们自动生成的。
- CGLIB动态代理
静态代理和JDK代理都要求目标对象实现一个接口,但是有时候目标对象如果没有实现任何的接口,这时候可以使用目标对象的子类实现代理(Cglib代理)【通过继承来代理】Cglib代理又称为子类代理,它是在内存中构建一个子类对象,从而实现对目标对象的功能拓展。
- 测试用例
- 有个小细节,CGLib 代理的目标对象不需要实现任何接口,它是通过动态继承目标对象 实现的动态代理。来看测试代码:
-
CGLib的实现原理又是怎样的呢?将CGLib代理后的class写入到磁盘
-
我们会发现在E://cglib_proxy_class目录下多了三个class文件
- 通过调试跟踪,我们发现Customer$$EnhancerByCGLIB$$3feeb52a.class就是CGLib生成的代理类,继承了Customer类。
-部分代码如下
重写了Customer 类的所有方法。我们通过代理类的源码可以看到,代理类会获得所有在父类继承来的方法 ,并且会有 MethodProxy 与之对应 ,比如 MethodCGLIB0findLoveProxy;这些方法在代理类的findLove()中都有调用。
调 用 过 程 : 代 理 对 象 调 用 this.findLove() 方 法 -> 调用拦截器->methodProxy.invokeSuper->CGLIB0->被代理对象findLove()方法。 此时,我们发现拦截器 MethodInterceptor 中就是由 MethodProxy 的 invokeSuper 方法调用代理方法的,MethodProxy非常关键,我们分析一下它具体做了什么。
上面代码调用过程就是获取到代理类对应的FastClass,并执行了代理方法。还记得之前 生成三个class文件吗?
$$FastClassByCGLIB$$6aad62f1.class就是代理类的FastClass,
Customer$$FastClassByCGLIB$$2669574a.class就是被代理类的FastClass。
CGLib动态代理执行代理方法效率之所以比JDK的高是因为Cglib采用了FastClass机 制,它的原理简单来说就是:为代理类和被代理类各生成一个Class,这个Class会为代理类或被代理类的方法分配一个index(int类型)。这个index当做一个入参,FastClass就可以直接定位要调用的方法直接进行调用,这样省去了反射调用,所以调用效率比JDK动态代理通过反射调用高。
CGLib和JDK动态代理对比
- JDK动态代理是实现了被代理对象的接口,CGLib是继承了被代理对象。
- JDK和CGLib都是在运行期生成字节码, JDK是直接写Class字节码, CGLib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。
- JDK调用代理方法,是通过反射机制调用, CGLib是通过FastClass机制直接调用方法,CGLib执行效率更高。
代理模式在Spring的运用
-
最典型的就是AOP
-
Spring利用动态代理实现AOP有两个非常重要的类,一个是JdkDynamicAopProxy类和CglibAopProxy类
Spring中的代理选择原则
- 当Bean有实现接口时,Spring就会用JDK的动态代理
- 当Bean没有实现接口时,Spring选择CGLib。
- 也可以强制使用JDK或者CGLIB代理,但是如果没有接口的话还是会使用CGLIB代理
静态代理和动态的本质区别
- 静态代理只能通过手动完成代理操作,如果被代理类增加新的方法,代理类需要同步新增,违背开闭原则。
- 动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则。
- 若动态代理要对目标类的增强逻辑扩展,结合策略模式,只需要新增策略类便可完成, 无需修改代理类的代码。
代理模式的优缺点
使用代理模式具有以下几个优点:
- 代理模式能将代理对象与真实被调用的目标对象分离。
- 一定程度上降低了系统的耦合度,扩展性好。
- 可以起到保护目标对象的作用。
- 可以对目标对象的功能增强。
当然,代理模式也是有缺点的:
- 代理模式会造成系统设计中类的数量增加。
- 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。
- 增加了系统的复杂度。