『动态代理』其实源于设计模式中的代理模式,而代理模式就是使用代理对象完成用户请求,屏蔽用户对真实对象的访问。
举个最简单的例子,比如我们想要「FQ」访问国外网站,因为我们并没有墙掉所有国外的 IP,所以你可以将你的请求数据报发送到那些没有被屏蔽的国外主机上,然后你通过配置国外主机将请求转发到目的地并在得到响应报文后转发回我们国内主机上。
这个例子中,国外主机就是一个代理对象,而那些被墙掉的主机就是真实对象,我们不能直接访问到真实对象,但可以通过一个代理间接的访问到。
代理模式的一个好处就是,所有的外部请求都经过代理对象,而代理对象有权利控制是否允许你真正的访问到真实对象,如果不合法的请求,代理对象完全可以拒绝你而不用实际麻烦到真实对象。
代理模式的一个最典型的应用就是 Spring 框架,Spring 的 AOP 以面向切面式编程将实际的业务逻辑和相关日志异常等信息隔离开,而你每次对业务逻辑的请求都对应的是一个代理对象,这个代理对象中除了进行必要的权限检查,日志打印,就是真实的业务逻辑处理块。
静态代理
代理模式的实现者主要有两种,『静态代理』和『动态代理』,这两者的本质区别就在于,前者的代理类是需要程序员手动编码的,而后者的代理类是自动生成的。所以,这也是你几乎没有听过『静态代理』这个概念的原因,当然,了解一下静态代理自然更容易去理解『动态代理』。
有一点大家需要清楚,代理对象代理了真实对象所有的方法,也就是代理对象需要向外提供至少和真实对象一样的方法名供调用,所以一个代理对象就需要定义出真实对象拥有的所有方法,包括父类中的方法。
我们看一个简单的静态代理示例:
为了说明问题,我们定义了一个 IService 接口,并让我们的真实类继承并实现该接口,这样我们的真实类中就有两个方法了。
那么代理类该怎样定义才能完成对真实对象的代理呢?
一般来说,代理类的本质就是,定义出真实类中所有的方法并在方法内部添加一些其他操作,最后再调用真实类的该方法。
代理类要代理真实类中所有的方法,也就是说需要定义和真实类中那些方法签名一模一样的方法,而这些方法的内部还是会间接调用真实类的该方法。
所以一般来说,代理类会选择直接继承真实类所有的接口和父类以便拿到真实类所有的父级方法签名,也就是先代理所有的父级方法。
接着,代理真实类中非父级方法,以这里的例子来说,doService 方法就是真实类自己的方法,我们的代理类也要定义一个一模一样方法签名的方法对其进行代理。
这样,我们的代理类就算是完成了,以后对于真实类中所有方法的调用都可以通过代理类进行代理。像这样:
proxyClass 作为一个代理类对象,可以代理真实类中所有的方法,并在这些方法执行之前,打印了一些「无关紧要」的信息。
代理模式的一个基本实现思路基本是这样,但是动态代理不同于这种静态代理的一点在于,动态代理不用我们一个一个方法的定义,虚拟机会自动为你生成这些方法。
JDK 动态代理机制
动态代理区别于静态代理的一点是,动态代理的代理类由虚拟机在运行时动态创建并于虚拟机卸载时清除。
我们复用上述静态代理中使用的类,看看 JDK 的动态代理具体是如何做到代理出某个类实例的所有方法的。
涉及的代码还是比较多的,我们一点点来分析。首先,realClass 作为我们的被代理类实现了接口 IService 并在内部定义了一个自己的方法 doService。
接着,我们定义了一个处理类,它继承了接口 InvocationHandler 并实现了其唯一申明的 invoke 方法。除此之外,我们还得声明一个成员字段用于存储真实对象,也就是被代理对象,因为我们代理的任何方法基本上都是基于真实对象的相关方法的。
关于这个 invoke 方法的作用以及各个形式参数的意义,待会我们反射代理类源码的时候再做详细的分析。
最后,定义好我们的处理类,基本上就可以进行基于 JDK 的动态代理了。核心的方法是 Proxy 类的 newProxyInstance 方法,该方法有三个参数,其一是一个类加载器,其二是被代理类实现的所有接口集合,其三是我们自定义的处理器类。
虚拟机会在运行时使用你提供的类加载器,将所有指定的接口类加载进方法区,然后反射读取这些接口中的方法并结合处理器类生成一个代理类型。
最后一句话可能有点抽象,如何「结合处理器类生成一个代理类型」?这一点我们通过指定虚拟机启动参数,让它保存下来生成的代理类的 Class 文件。
首先,这个代理类的名字是很随意的,一个程序中如果有多个代理类要生成,「$Proxy + 数字」就是它们的类名。
接着,你会注意到这个代理类继承 Proxy 类和我们指定的接口 IService(之前如果指定多个接口,这里就会继承多个接口)。
然后你会发现,这个构造器需要一个 InvocationHandler 类型的参数,并且构造器的主体就是将这个 InvocationHandler 实例传递到父类 Proxy 的对应字段进行保存,这也是为什么所有的代理类都必须使用 Proxy 作为父类的一个原因,就是为了公用父类中的 InvocationHandler 字段。后面我们会知道,这一个小小的设计将导致基于 JDK 的动态代理存在一个致命性的缺点,待会介绍。
登录 | 立即注册