代理模式
提出问题
- 为了只有在我们确实需要这个对象时才对它进行初始化
- 我们在写一个功能函数时,经常需要在其中写入与功能不是直接相关但很有必要的代码
问题案例1
考虑一个数据库查询场景,需要将公司的所有员工显示出来,而且不要翻页,在显示全部员工时,只需要显示名称即可,但是必要时也可以查看某位员工的具体信息。
该查询查询的数据条很多,并且每条数据的数据量也很大,则会消耗很大的内存。从用户角度看,y用户可能访问一条数据,也可能不访问等等。而从程序角度则应该减少内存的消耗
问题案例2
如日志记录,信息发送,安全和事务支持等,这些枝节性代码虽然是必要的,但它会带来以下麻烦:
- 枝节性代码游离在功能性代码之外,它不是函数的目的,这是对OO是一种破坏
- 枝节性代码会造成功能性代码对其它类的依赖,加深类之间的耦合,可重用性降低
- 从法理上说,枝节性代码应该监视着功能性代码,然后采取行动,而不是功能性代码通知枝节性代码采取行动,这好比吟游诗人应该是主动记录骑士的功绩而不是骑士主动要求诗人记录自己的功绩
问题案例3
考虑一个场景,即去买演唱会的票,如果直接去现场买,则很难买,而且很浪费时间,如果通过代理买,我们只需要交钱就可以了。但是要额外承担一笔代理的费用。
基础概述
是什么
代理(Proxy)是一种设计模式,定义:为其他对象提供一个代理以控制对某个对象的访问,即通过代理对象访问目标对象.
- 这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.
这里使用到编程中的一个思想:
不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法
所以说代理模式就是:当前对象不愿意干的,没法干的东西委托给别的对象来做,我只要做好本分的东西就好了!
分类
根据实现方式分类
- 静态代理
- 动态代理
根据目的分类
- 远程代理。隐藏一个对象存在于不同地址空间的事实,一般用于RMI等
- 虚代理。可以进行最优化,根据要求创建对象。即根据需要创建开销很大的对象, 该对象只有在真正需要的时候才会被创建。
- 保护代理。提供不同的访问权限,允许在访问一个对象时有一些附加处理
- 智能指引。允许在访问一个对象时有一些附加处理
区别
很明显的是:
- 静态代理需要自己写代理类–>代理类需要实现与目标对象相同的接口
- 而动态代理不需要自己编写代理类—>(是动态生成的)
使用静态代理时:
- 如果目标对象的接口有很多方法的话,那我们还是得一一实现,这样就会比较麻烦
使用动态代理时:
- 代理对象的生成,是利用JDK API,动态地在内存中构建代理对象(需要我们指定创建 代理对象/目标对象 实现的接口的类型),并且会默认实现接口的全部方法。
应用
适用性
- 远程代理,为一个对象在不同的地址空间提供局部代表
- 虚代理,根据需要创建开销很大的对象
- 保护代理,控制对原始对象的访问,用于对象应该有不同的访问权限的时候
- 智能指引,取代了简单的指针,在访问对象时执行一些附加操作
- 对指向实际对象的引用计数,当对象没有用时,可以自动释放
- 在访问一个对象时,对它进行事务、日志等操作
案例1
CopyOnWrite的优化方式,拷贝是一个开销很大的操作,如果拷贝没有被修改,则代理延迟这一拷贝过程,保证只有在这个对象被修改时才进行拷贝
协作
结构
参与者
- Proxy
- 保护一个引用使得代理可以访问实体,如果RealSubject与Subject的接口相同,Proxy会引用Subject
- 提供一个与Subject的接口相同的接口,这样代理就可以用来替代实体
- 控制对实体的存取,并可能负责创建和删除它
- 其他功能依赖于代理的类型
- Remote Proxy负责对请求及其参数进行编码,并向不同地址空间的实体发送已编码的请求
- Virtual Proxy可以缓存实体的附加信息,以便延迟对它的访问
- Protection Proxy检查调用者是否具有实现一个请求所必须的访问权限
- Subject
- 定义RealSubject与Proxy的公共接口,可以在任何使用RealSubject的地方都可以使用Proxy
- RealSubject
- 定义Proxy所代表的实体
协作
- 类关系
- 逻辑关系
- 代理根据其种类,在适当的时候向RealSubject转发请求。
权衡
静态代理
结构
效果(优缺)
实现
实现步骤
案例1
这里有一个Subject接口
1 | public interface Subject { |
实现RealSubject
1 | public class RealSubject implements Subject { |
实现Proxy扩展原有的RealSubject的功能
1 | public class SubjectProxy implements Subject { |
接口功能还是由RealSubject来实现,但每次实现后会有一些扩展功能
1 | public class Main { |
问题案例1
实现一个代理对象,持有用户对象,并拥有查询用户姓名等基础信息的方法,当要查询某一个具体的用户全部信息时,进行reload加载该用户的信息。
透明代理(普通代理)
让真实对象(RealSubject)对外界来说是透明的
1 | public class RealSubjectProxy implements Subject { |
于是乎,实现代理
1 | public class Main { |
动态代理
结构
效果(优缺)
实现
实现步骤
案例1
利用动态代理自动生成代理对象
1 | public class Main { |
Java动态代理Proxy
调用过程
1 | `//简化上述(2)(3)(4)步骤Object o = Proxy.newProxyInstance(Stub.class.getClassLoader(), new Class<?>[] {Subject.class}, client.handler);` |
整体的过程可以拆解为以下过程
1 | public class StubClient { |
动态代理类的属性
- 如果所有的代理接口都是public的,那么代理类就是public、final的,切不是abstract的
- 动态代理类的名称以”$ProxyN”开头,N是代理类的唯一编号.
- 动态代理类都继承于java.lang.reflect.Proxy
- 动态代理类实现了其创建时指定的接口,且保持接口指定的顺序
- 如果动态代理类实现了一个非public接口,那么它将定义和接口相同的包名;否则代理类的包是不确定的,默认是com.sun.proxy,运行时,包密封性不防止特定包成功定义代理类;如果都不是,动态代理类将由同一个类加载器和相同的包与特定签名定义.
- 动态代理类实现了其创建时指定的所有接口,调用代理类Class对象的getInterfaces将返回和创建时指定接口顺序相同的列表,调用 getMethods方法返回所有接口方法的数组对象,调用getMethod会返回代理类接口中期望的method.
- 调用Proxy.isProxyClass方法时,传入Proxy.getProxyClass返回的Class或者Proxy.newProxyInstance返回对象的Class,都会返回true,否则返回false.
- 代理类的java.security.ProtectionDomain是由系统根类加载器加载的,代理类的代码也是系统信任的代码生成的,此保护域通常被授予java.security.AllPermission
- 每一个代理类都有一个public的,含有一个InvocationHandler实现为参数的构造方法,设置了调用处理器接口,就不必使用反射api访问构造方法,通过Proxy.newProxyInstance可以产生和Proxy.getProxyClass和调用句柄相同的调用构造函数行为.
动态代理实例的属性
- 给定一个代理实例proxy,Foo实现的接口之一,表达式 proxy instanceof Foo 返回true,(Foo) proxy能成功转换.
- 每个代理实例都关联一个InvocationHandler, 通过Proxy.getInvocationHandler方法,将返回代理类关联的InvocationHandler.
- 代理类实例调用其代理接口中所声明的方法时,这些方法将被编码,并最终由调用处理器(InvocationHandler)的invoke方法执行.
- 代理类根类java.lang.Object中的hashCode,equals和toString方法,也会被分派到调用处理其的invoke方法执行;可能的原因有:一是因为这些方法为 public 且非 final 类型,能够被代理类覆盖;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。
- 当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。
获取动态代理类时需要注意哪些?
1 | public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) |
通过指定的ClassLoader loader和有序的interfaces,ClassLoader将动态生成实现有序interfaces的代理类,如果这个ClassLoader已经定义过相同有序接口实现的代理类,那么将不在重复定义.
- 所有interfaces中的对象必须都是接口,否则会抛出异常
- interfaces中的接口不能重复
- 所有接口相对指定的ClassLoader必须是可见的
- 所有的非public接口必须在同一个包中,否这不能成功生成实现所有接口的代理类.
- 代理类的接口数目不能超过65535,这个是JVM所限定的
当不满足上述限定中的一条或多条时,将会抛出IllegalArgumentException异常,如果interfaces中的接口对象一个或多个是null,也将抛出NullPointerException.
注意:代理类指定的接口的顺序是很重要的,否则不通顺序的相同接口数组将会导致生成不同的代理类
从源码中理解动态代理类的生成
上面我们讲述了动态代理的使用,动态代理类的属性,动态代理实例的属性,以及获取动态代理类时需要注意的事项,下面我们从源码角度去观察这些东西
Proxy的重要变量
1 | //构造器参数类型 |
Proxy的构造方法
1 | //私有构造函数,禁止外部调用 |
Proxy.newProxyInstance函数分析
1 | public static Object newProxyInstance(ClassLoader loader, |
Proxy.getProxyClass函数分析
1 | public static Class<?> getProxyClass(ClassLoader loader, |
Proxy.checkProxyAccess函数分析
1 | /* |
Proxy.getProxyClass0函数分析
1 | /* |
接下来就是代理类从缓存中获取代理类,jdk1.7中的缓存机制略显复杂,没有去深入研究,后期如有可能再补上跳过如下流程代码分析
- 通过WeakCache的get方法获得代理类的Class对象
- 删除无效缓存,弱key和强subkey的缓存等(略过…)
- 通过WeakCache的内部类Factory的get方法调用Proxy.ProxyClassFactory的apply方法得到代理类的Class对象
ProxyClassFactory分析
1 | private static final class ProxyClassFactory |
至此,动态代理的Class对象生成进入结尾,Proxy的isProxyClass方法和getInvocationHandler方法就比较清晰明显了,请读者自行分析.
动态代理类生成
事物往往不像其看起来的复杂,需要的是我们能够化繁为简,这样也许就能有更多拨云见日的机会.
代理类中方法调用的分派转发推演实现
1 | public final class Proxy$0 extends java.lang.reflect.Proxy implements com.thinkdevos.java.dynamicproxy.Subject { |
用调用处理器调用方法时,在捕获方法本身抛出的异常后,还有可能有未知异常抛出,对于不支持的异常,必须抛 UndeclaredThrowableException 运行时异常.
代理类中方法调用的分派转发推演实现
1 | public final void doSomething() { |
这样我们就完成了对动态代理类的推演实现。
下面我们就实例验证一番,通过如下代码生成字节码文件
工具类: 通过ProxyGenerator.generateProxyClass生成代理类字节数组并保存到文件中
1 | public class ProxyUtils { |
运行生成Proxy$0.class
1 | public class ProxyClass { |
通过 javap -p Proxy$0 查看字节码文件基本信息
1 | public final class Proxy$0 extends java.lang.reflect.Proxy implements com.thinkdevos.java.dynamicproxy.Subject { |
通过 javap -v Proxy$0 查看详细信息,我们主要看下调用处理器对doSomething调用和异常捕获处理(注释是自己加的,字节码文件中没有注释)
1 | public final void doSomething() throws ; |
动态代理的不足之处
动态代理只能支持接口的代理,这也是因为java的继承性本质所限制的,因为所有的动态代理类都继承了Proxy类,所以再也无法同时继承其他类.然而,我们不可否认动态代理设计的伟大之处,世上所有的事物都不可能完美.
动态代理调用过程
我们来看看究竟是怎么请水军的:
Java提供了一个Proxy类,调用它的newInstance方法可以生成某个对象的代理对象,该方法需要三个参数:
- 参数一:生成代理对象使用哪个类装载器【一般我们使用的是被代理类的装载器】
- 参数二:生成哪个对象的代理对象,通过接口指定【指定要被代理类的接口】
- 参数三:生成的代理对象的方法里干什么事【实现handler接口,我们想怎么实现就怎么实现】
在编写动态代理之前,要明确几个概念:
- 代理对象拥有目标对象相同的方法【因为参数二指定了对象的接口,代理对象会实现接口的所有方法
- 用户调用代理对象的什么方法,都是在调用处理器的invoke方法。【被拦截】
- 使用JDK动态代理必须要有接口【参数二需要接口】
上面也说了:代理对象会实现接口的所有方法,这些实现的方法交由我们的handler来处理!
- 所有通过动态代理实现的方法全部通过
invoke()
调用
所以动态代理调用过程是这样子的:
相关模式
- Adapter适配器为它所适配的对象提供了一个不同的接口,代理提供了与它的实体相同的接口。然而用于访问保护的代理可能会拒绝执行实体会执行的操作,因此它的接口可能只是实体的一个自己
- Decorator,他们的实现类似,但是目的不同,Decorator是为对象添加一个或多个功能,而代理是控制对对象的访问。
进阶
反省总结
典型应用
我们之前写中文过滤器的时候,需要使用包装设计模式来设计一个request类。如果不是Servlet提供了实现类给我们,我们使用包装设计模式会比较麻烦。
现在我们学习了动态代理了,动态代理就是拦截直接访问对象,可以给对象进行增强的一项技能
中文过滤器
1 | public void doFilter(final ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { |
总结
本文主要讲解了代理模式的几个要点,其实还有一些细节的:比如“强制代理”(只能通过被代理对象找到代理对象,不能绕过代理对象直接访问被代理对象)。只是用得比较少,我就不说了~~
要实现动态代理必须要有接口的,动态代理是基于接口来代理的(实现接口的所有方法),如果没有接口的话我们可以考虑cglib代理。
cglib代理也叫子类代理,从内存中构建出一个子类来扩展目标对象的功能!
这里我就不再贴出代码来了,因为cglib的代理教程也很多,与动态代理实现差不多~
总的来说:代理模式是我们写代码中用得很多的一种模式了,Spring的AOP底层其实就是动态代理来实现的–>面向切面编程。具体可参考我之前写的那篇文章:
其实只要记住一点:原有的对象需要额外的功能,想想动态代理这项技术!