《设计模式》代理模式
1. 基本介绍
定义:
代理模式就是为被访问的目标对象提供一个代理,此时代理对象充当访问对象和目标对象之间的媒介,通过代理对象实现对目标对象的访问。
被代理的对象可以是远程对象、创建开销大的对象以及需要安全控制的对象,Java 中的代理按照代理的生成时机不同分为静态代理和动态代理,静态代理就是在编译期就生成代理对象,而动态代理是在 Java 运行时动态生成,而动态代理又分为 JDK 动态代理和 Cglib 动态代理两种。
代理模式的角色组成:
抽象主题类(Subject):通过接口或抽象类声明真实主题和代理对象实现的业务方法。
真实主题类(Real Subject):实现抽象主题中的具体业务,是代理对象所代表的真实对象,是最终引用的对象。
代理类(Proxy):提供与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
代理模式的使用场景:
- 保护代理:控制对一个对象的访问,根据需要为不同的用户提供不同级别的使用权限。
- 防火墙代理:将浏览器配置成使用代理功能时,防火墙将浏览器的请求转给互联网,当互联网返回数据时,代理服务器再将数据转给浏览器。
代理模式类图如下所示:
2. 静态代理
案例背景:
每到节假日来临前后,火车站一定是人流量最大的场所之一。由于现在的网络购票途径非常成熟,因此大家可以在手机上简单操作几下就可以将票买到手了,非常便利。即使是在网络购票的方式出来之前,我记得在我小的时候就经常看见一些门店会贴着火车票的代售点的字样,在那个网络还不发达的年代,这也算是比较方便的购票方式了,总比跑到火车站现场购票方便很多。如果使用代理模式的思想分析这个生活中的场景,那么火车站就可以看作是目标对象,而代售点就是代理对象,我们通过代售点进行买票,火车站和代售点都有售票的功能,“我们”就是访问对象。
设计类图如下所示:
SellTickets
接口:公共接口
public interface SellTickets { void sell(); }
RailwayStation
类:目标类
public class RailwayStation implements SellTickets{ @Override public void sell() { System.out.println("售票"); } }
Proxy
类:代理类
public class Proxy implements SellTickets{ private RailwayStation railwayStation = new RailwayStation(); @Override public void sell() { System.out.println("代售点收取一些服务费"); railwayStation.sell(); } }
Client
类:访问类
public class Client { public static void main(String[] args) { Proxy proxy = new Proxy(); proxy.sell(); } }
访问对象 Client 通过代理对象 Proxy 进行购票,同时还增加了收取服务费用的功能。
静态代理的优劣:
在不修改目标对象功能的前提下,实现对目标功能扩展。
代理对象需要与目标对象实现相同的接口,会有很多代理类,当接口方法增加时,需要对目标对象和代理对象都进行维护。
3. JDK 动态代理
不同于静态代理,在动态代理中,代理对象不需要实现接口,但是目标对象依然需要实现接口,否则不能使用动态代理。代理对象的生成是利用 JDK 的 API 动态地在内存中构建代理对象。
在 Java API 中提供了一个动态代理类 java.lang.reflect,Proxy,该类不同于上节所说的代理对象的类,而是提供一个创建代理对象的静态方法来获取代理对象。
SellTickets 接口:公共接口
public interface SellTickets { void sell(); }
RailwayStation
类:目标类
public class RailwayStation implements SellTickets{ @Override public void sell() { System.out.println("售票"); } }
ProxyFactory
类:代理工厂,创建代理类
public class ProxyFactory { private RailwayStation railwayStation = new RailwayStation(); public SellTickets getProxyObject() { // 使用 Proxy 获取代理对象 SellTickets sellTickets = (SellTickets) Proxy. newProxyInstance(railwayStation.getClass().getClassLoader(), // 使用目标对象的类加载器加载代理类 railwayStation.getClass().getInterfaces(), // 目标对象实现的接口 new InvocationHandler() { // 代理对象的调用处理程序 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理点收取一些服务费"); // 执行目标对象 Object result = method.invoke(railwayStation, args); return result; } }); return sellTickets; } }
Client
类:访问类
public class Client { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); SellTickets proxyObject = proxyFactory.getProxyObject(); proxyObject.sell(); } }
为了更好地监控动态代理类在内存中的创建过程,需要使用 Java 诊断工具 arthas 来打印出程序在运行过程中代理类的结构,步骤如下:
启动 Client 类,使得程序一直保持运行,因为程序运行结束内存便会被释放,则无法观察到动态代理类在程序运行过程中的结构,并打印出代理对象的类名,方便后面 arthas 工具进行查看,Client 类代码如下:
public class Client { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); SellTickets proxyObject = proxyFactory.getProxyObject(); proxyObject.sell(); System.out.println(proxyObject.getClass()); while (true) { } } }
- 下载 arthas-jar 工具,点击下载跳转地址
- 打开命令行窗口,进入 arthas-boot.jar 所在根目录
- 输入命令:
java -jar arthas-boot.jar
- 找到启动类名称 Client,并输入其对应的序号,如下所示:
加载完成之后,输入命令 jad com.sun.proxy.$Proxy0
,等待打印出来的程序运行过程中代理类的结构,如下所示:
- 完整代码如下:
package com.sun.proxy; import com.hzz.proxy.dynamicproxy.SellTickets; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; public final class $Proxy0 extends Proxy implements SellTickets { private static Method m1; private static Method m2; private static Method m3; private static Method m0; public final void sell() { try { this.h.invoke(this, m3, null); return; } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public $Proxy0(InvocationHandler invocationHandler) { super(invocationHandler); } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); m3 = Class.forName("com.hzz.proxy.dynamicproxy.SellTickets").getMethod("sell", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); return; } catch (NoSuchMethodException noSuchMethodException) { throw new NoSuchMethodError(noSuchMethodException.getMessage()); } catch (ClassNotFoundException classNotFoundException) { throw new NoClassDefFoundError(classNotFoundException.getMessage()); } } public final boolean equals(Object object) { try { return (Boolean)this.h.invoke(this, m1, new Object[]{object}); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public final String toString() { try { return (String)this.h.invoke(this, m2, null); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public final int hashCode() { try { return (Integer)this.h.invoke(this, m0, null); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } }
可以看到,代理类 $Proxy0 其实是实现了 SellTickets 接口的,只不过我们是通过 Proxy.newProxyInstance() 方法帮我们实现的,不用像在静态代理中那样显式实现,因此代理类和目标类都要实现相同的接口还是成立的。
此外,从源码中可以看到在 Proxy.newProxyInstance() 方法中创建的匿名内部类中传递了 railywayStation 目标对象,被传递给了 $Proxy0 的父类 Proxy.
根据类的结构可知,动态代理的执行流程大概如下:
首先,在 Client 中通过代理对象调用 sell 方法。
之后,根据多态性,执行的是代理类 $Proxy0 中的 sell() 方法。
然后,代理类 $Proxy0 中的 sell() 方法又去调用 InvocationHandler 接口的子实现类对象的 invoke() 方法。
最后, invoke() 方法通过反射执行了目标类 RailwayStation 中的 sell() 方法。
JDK 动态代理的注意事项:
使用 JDK 动态代理时,不能代理 private 和 static 方法,代理类和目标类需要实现相同的接口,因为 private 和 static 不能修饰接口。
JDK 动态代理只能对接口进行代理,不能对普通类进行代理,因为 JDK 动态代理类生成的 $Proxy0 类的父类为 Proxy 类,Java 中不支持多继承
4. CGLIB 动态代理
从上面两节可以知道,无论是静态代理还是 JDK 动态代理,都需要目标类去实现一个接口,但是有时目标对象就只是一个单独的对象,并没有去实现任何的接口,这时如果还想使用代理模式的话,就可以使用 CGLIB 动态代理。
CGLIB(Code Generation Library) 动态代理:
CGLIB 通过动态生成一个子类,该子类继承被代理类,重写被代理类的所有非 final 修饰的方法,并在子类中采用方法拦截的技术拦截父类所有的方法调用。
CGLIB 代理为 JDK 动态代理提供了很好的补充,作为一个功能强大且高性能的代码生成包,可以为没有实现接口的类提供代理,被广泛应用于 AOP 框架中,实现方法的拦截作用。
CGLIB 的 jar 坐标如下所示:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency>
RailwayStation
类:目标类
public class RailwayStation implements SellTickets{ public void sell() { System.out.println("售票"); } }
ProxyFactory
类
public class ProxyFactory implements MethodInterceptor { private RailwayStation target = new RailwayStation(); public RailwayStation getProxyObject() { // 创建 Enhancer 对象,类似于 JDK 动态代理的 Proxy 类 Enhancer enhancer = new Enhancer(); // 设置父类的字节码对象 enhancer.setSuperclass(target.getClass()); // 设置回调函数 enhancer.setCallback(this); // 创建代理对象 RailwayStation obj = (RailwayStation) enhancer.create(); return obj; } /** * 重写 intercept 方法,在该方法中会调用目标对象的方法 * @param o 代理对象 * @param method 目标对象的方法的 method 实例 * @param objects 实际参数 * @param methodProxy 代理对象中的方法的 method 实例 * @return RailwayStation * @throws Throwable */ @Override public RailwayStation intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("代理点收取一些服务费"); RailwayStation result = (RailwayStation) methodProxy.invokeSuper(o, objects); return result; } }
Client
类:访问类
public class Client { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); RailwayStation proxyObject = proxyFactory.getProxyObject(); proxyObject.sell(); } }
CGLIB 代理的注意事项:
CGLIB 包的底层是使用字节码处理框架 ASM 来转换字节码并生成新的类。
在内存中动态构建子类,代理的类不能为 final,因为被继承的父类如果为常量类那么无法被继承,会报错 java.lang.IllegalArgumentException.
目标对象的方法不能为 private,因为子类无法访问父类的私有方法;目标对象的方法不能为 final,因为子类无法重写父类的不可变方法;目标对象的方法不能为 static,因为静态方法属于类,是不属于对象的。
5. 区别比较
静态代理和动态代理的区别:
如果接口增加一个方法,静态代理除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,增加了代码维护的复杂度。
动态代理最大的优点就是接口中声明的所有方法都被转移到调用处理器的方法 InvocationHandler.invoke 中处理。 在接口方法数量比较多的时候,可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
JDK 代理和 CGLIB 代理的区别:
如果有接口使用 JDK 动态代理,如果没有接口使用 CGLIB 动态代理。
JDK 动态代理使用 Java 反射技术进行操作,在生成类上更高效。
CGLIB 使用 ASM 框架直接对字节码进行修改,使用了 FastClass 的特性。在某些情况下,类的方法执行会比较高效。