代理模式
代理模式也是一种很常见的设计模式。它使用代理对象完成用户请求,屏蔽用户对真实对象的访问。就如同现实中的代理一样,代理人被授权执行当事人的一些事宜,而无需当事人出面,从第三方的角度看,似乎当事人并不存在,因为他只和代理人通信。而事实上,代理人是要有当事人的授权,并且在核心问题上还需要请示当事人。
在现实中,使用代理的情况很普遍,而且原因也很多。比如,当事人因为某些隐私不方便出面,或者当事人不具备某些相关的专业技能,而需要一个职业人员来完成一些专业的操作,也可能由于当事人没有时间处理事务,而聘用代理人出面。
在软件设计中,使用代理模式的意图也很多,比如因为安全原因,需要屏蔽客户端直接访问真实对象;或者在远程调用中,需要使用代理类处理远程方法调用的技术细节(如RMI);也可能是为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的。在本小节中,主要讨论使用代理模式实现延迟加载,从而提升系统的性能和反应速度。
1.代理模式的结构
代理模式的主要参与者有4个,如表2.2所示。
表2.2 代理模式角色
角 色 | 作 用 |
主题接口 | 定义代理类和真实主题的公共对外方法, 也是代理类代理真实主题的方法 |
真实主题 | 真正实现业务逻辑的类 |
代理类 | 用来代理和封装真实主题 |
Main | 客户端,使用代理类和主题接口完成一些工作 |
以一个简单的示例来阐述使用代理模式实现延迟加载的方法及其意义。假设某客户端软件,有根据用户请求,去数据库查询数据的功能。在查询数据前,需要获得数据库连接,软件开启时,初始化系统的所有类,此时尝试获得数据库连接。当系统有大量的类似操作存在时(比如xml解析等),所有这些初始化操作的叠加,会使得系统的启动速度变得非常缓慢。为此,使用代理模式,使用代理类,封装对数据库查询中的初始化操作,当系统启动时,初始化这个代理类,而非真实的数据库查询类,而代理类什么都没有做,因此,它的构造是相当迅速的。
在系统启动时,将消耗资源最多的方法都使用代理模式分离,就可以加快系统的启动速度,减少用户的等待时间。而在用户真正做查询操作时,再由代理类,单独去加载真实的数据库查询类,完成用户的请求。这个过程就是使用代理模式实现了延迟加载。
注意:代理模式可以用于多种场合,如用于远程调用的网络代理、考虑安全因素的安全代理等。延迟加载只是代理模式的一种应用场景。
延迟加载的核心思想是:如果当前并没有使用这个组件,则不需要真正地初始化它,使用一个代理对象替代它的原有的位置,只要在真正需要使用的时候,才对它进行加载。使用代理模式的延迟加载是非常有意义的,首先,它可以在时间轴上分散系统压力,尤其在系统启动时,不必完成所有的初始化工作,从而加速启动时间;其次,对很多真实主题而言,在软件启动直到被关闭的整个过程中,可能根本不会被调用,初始化这些数据无疑是一种资源浪费。图2.2显示了使用代理类封装数据库查询类后,系统的启动过程。
图2.2 代理类的工作流程 |
若系统不使用代理模式,则在启动时就要初始化DBQuery对象,而使用代理模式后,启动时只需要初始化一个轻量级的对象DBQueryProxy。
系统的结构图如图2.3所示,IDBQuery是主题接口,定义代理类和真实类需要对外提供的服务,在本例中了定义了实现数据库查询的公共方法request()函数。DBQuery是真实主题,负责实际的业务操作,DBQueryProxy是DBQuery的代理类。
2.代理模式的实现和使用
基于以上设计,IDBQuery的实现如下,它只有一个request()方法:
- public interface IDBQuery {
- String request();
- }
(点击查看大图)图2.3 代理模式的一种实现 |
DBQuery实现如下,它是一个重量级对象,构造会比较慢:
- public class DBQuery implements IDBQuery{
- public DBQuery(){
- try {
- Thread.sleep(1000); //可能包含数据库连接等耗时操作
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- @Override
- public String request() {
- return "request string";
- }
- }
代理类DBQueryProxy是轻量级对象,创建很快,用于替代DBQuery的位置:
- public class DBQueryProxy implements IDBQuery {
- private DBQuery real=null;
- @Override
- public String request() {
- //在真正需要的时候,才创建真实对象,创建过程可能很慢
- if(real==null)
- real=new DBQuery();
- //在多线程环境下,这里返回一个虚假类,类似于Future模式
- return real.request();
- }
- }
最后,主函数如下,它引用IDBQuery接口,并使用代理类工作:
- public class Main {
- public static void main(String args[]){
- IDBQuery q=new DBQueryProxy(); //使用代理
- q.request(); //在真正使用时才创建真实对象
- }
- }
注意:将代理模式用于实现延迟加载,可以有效地提升系统的启动速度,对改善用户体验有很大的帮助。
3.动态代理介绍
动态代理是指在运行时,动态生成代理类。即,代理类的字节码将在运行时生成并载入当前的ClassLoader。与静态代理类相比,动态类有诸多好处。首先,不需要为真实主题写一个形式上完全一样的封装类,假如主题接口中的方法很多,为每一个接口写一个代理方法也是非常烦人的事,如果接口有变动,则真实主题和代理类都要修改,不利于系统维护;其次,使用一些动态代理的生成方法甚至可以在运行时指定代理类的执行逻辑,从而大大提升系统的灵活性。
注意:动态代理使用字节码动态生成加载技术,在运行时生成并加载类。
生成动态代理类的方法很多,如,JDK自带的动态代理、CGLIB、Javassist或者ASM库。JDK的动态代理使用简单,它内置在JDK中,因此不需要引入第三方Jar包,但相对功能比较弱。CGLIB和Javassist都是高级的字节码生成库,总体性能比JDK自带的动态代理好,而且功能十分强大。ASM是低级的字节码生成工具,使用ASM已经近乎于在使用Java bytecode编程,对开发人员要求最高,当然,也是性能最好的一种动态代理生成工具。但ASM的使用实在过于繁琐,而且性能也没有数量级的提升,与CGLIB等高级字节码生成工具相比,ASM程序的可维护性也较差,如果不是在对性能有苛刻要求的场合,笔者还是推荐CGLIB或者Javassist。
4.动态代理实现
以上例中的DBQueryProxy为例,使用动态代理生成动态类,替代上例中的DBQueryProxy。首先,使用JDK的动态代理生成代理对象。JDK的动态代理需要实现一个处理方法调用的Handler,用于实现代理方法的内部逻辑。
- public class JdkDbQeuryHandler implements InvocationHandler {
- IDBQuery real=null; //主题接口
- @Override
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- if(real==null)
- real=new DBQuery(); //如果是第一次调用,则生成真实对象
- return real.request(); //使用真实主题完成实际的操作
- }
- }
以上代码实现了一个Handler,可以看到,它的内部逻辑和DBQueryProxy是类似的。在调用真实主题的方法前,先尝试生成真实主题对象。接着,需要使用这个Handler生成动态代理对象:
- public static IDBQuery createJdkProxy(){
- IDBQuery jdkProxy = (IDBQuery) Proxy.newProxyInstance(
- ClassLoader.getSystemClassLoader(),
- new Class[] { IDBQuery.class },
- new JdkDbQeuryHandler()); //指定Handler
- return jdkProxy;
- }
以上代码生成一个实现了IDBQuery接口的代理类,代理类的内部逻辑由JdkDbQeuryHandler决定。生成代理类后,由newProxyInstance()方法返回该代理类的一个实例。至此,一个完整的JDK动态代理就完成了。
CGLIB和Javassist的动态代理的使用和JDK的动态代理非常类似。下面,尝试使用CGLIB生成动态代理。CGLIB也需要实现一个处理代理逻辑的切入类:
- public class CglibDbQueryInterceptor implements MethodInterceptor {
- IDBQuery real=null;
- @Override
- public Object intercept(Object arg0, Method arg1, Object[] arg2,
- MethodProxy arg3) throws Throwable {
- if(real==null) //代理类的内部逻辑
- //和前文中的一样
- real=new DBQuery();
- return real.request();
- }
- }
在这个切入对象的基础上,可以生成动态代理:
- public static IDBQuery createCglibProxy(){
- Enhancer enhancer = new Enhancer();
- enhancer.setCallback(new CglibDbQueryInterceptor());
- //指定切入器,定义代理类逻辑
- enhancer.setInterfaces(new Class[] { IDBQuery.class });
- //指定实现的接口
- IDBQuery cglibProxy = (IDBQuery) enhancer.create();
- //生成代理类的实例
- return cglibProxy;
- }
使用Javassist生成动态代理可以使用两种方式:一种是使用代理工厂创建,另一种通过使用动态代码创建。使用代理工厂创建时,方法与CGLIB类似,也需要实现一个用于代理逻辑处理的Handler:
- public class JavassistDynDbQueryHandler implements MethodHandler {
- IDBQuery real=null;
- @Override
- public Object invoke(Object arg0, Method arg1, Method arg2, Object[] arg3)
- throws Throwable {
- if(real==null)
- real=new DBQuery();
- return real.request();
- }
- }
以这个Handler为基础,创建动态Javasssit代理:
- public static IDBQuery createJavassistDynProxy() throws Exception {
- ProxyFactory proxyFactory = new ProxyFactory();
- proxyFactory.setInterfaces(new Class[] { IDBQuery.class });//指定接口
- Class proxyClass = proxyFactory.createClass();
- IDBQuery javassistProxy = (IDBQuery) proxyClass.newInstance();
- //设置Handler处理器
- ((ProxyObject) javassistProxy).setHandler(new JavassistDynDbQuery- Handler());
- return javassistProxy;
- }
Javassist使用动态Java代码创建代理的过程和前文的方法略有不同。Javassist内部可以通过动态Java代码,生成字节码。这种方式创建的动态代理可以非常灵活,甚至可以在运行时生成业务逻辑。
- public static IDBQuery createJavassistBytecodeDynamicProxy() throws Exception {
- ClassPool mPool = new ClassPool(true);
- //定义类名
- CtClass mCtc = mPool.makeClass(IDBQuery.class.getName() + "Javaassist-
- BytecodeProxy");
- //需要实现的接口
- mCtc.addInterface(mPool.get(IDBQuery.class.getName()));
- //添加构造函数
- mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
- //添加类的字段信息,使用动态Java代码
- mCtc.addField(CtField.make("public " + IDBQuery.class.getName() + "
- real;", mCtc));
- String dbqueryname=DBQuery.class.getName();
- //添加方法,这里使用动态Java代码指定内部逻辑
- mCtc.addMethod(CtNewMethod.make("public String request() { if(real==
- null)real=new "+dbqueryname+"();return real.request(); }", mCtc));
- //基于以上信息,生成动态类
- Class pc = mCtc.toClass();
- //生成动态类的实例
- IDBQuery bytecodeProxy = (IDBQuery) pc.newInstance();
- return bytecodeProxy;
- }
在以上代码中,使用CtField.make()方法和CtNewMehod.make()方法在运行时生成了代理类的字段和方法。这些逻辑由Javassist的CtClass对象处理,将Java代码转换为对应的字节码,并生成动态代理类的实例。
注意:与静态代理相比,动态代理可以很大幅度地减少代码行数,并提升系统的灵活性。
在Java中,动态代理类的生成主要涉及对ClassLoader的使用。这里以CGLIB为例,简要阐述动态类的加载过程。使用CGLIB生成动态代理,首先需要生成Enhancer类实例,并指定用于处理代理业务的回调类。在Enhancer.create()方法中,会使用DefaultGeneratorStrategy.Generate()方法生成动态代理类的字节码,并保存在byte数组中。接着使用ReflectUtils. defineClass()方法,通过反射,调用ClassLoader.defineClass()方法,将字节码装载到ClassLoader中,完成类的加载。最后使用ReflectUtils.newInstance()方法,通过反射,生成动态类的实例,并返回该实例。无论使用何种方法生成动态代理,虽然实现细节不同,但主要逻辑都如图2.4所示。
图2.4 实现动态代理的基本步骤 |
- public static final int CIRCLE=30000000;
- public static void main(String[] args) throws Exception {
- IDBQuery d=null;
- long begin=System.currentTimeMillis();
- d=createJdkProxy(); //测试JDK动态代理
- System.out.println("createJdkProxy:"+(System.currentTimeMillis()-beg in));
- System.out.println("JdkProxy class:"+d.getClass().getName());
- begin=System.currentTimeMillis();
- for(int i=0;i<CIRCLE;i++)
- d.request();
- System.out.println("callJdkProxy:"+(System.currentTimeMillis()-begin ));
- begin=System.currentTimeMillis();
- d=createCglibProxy(); //测试CGLIB动态代理
- System.out.println("createCglibProxy:"+(System.currentTimeMillis()-b egin));
- System.out.println("CglibProxy class:"+d.getClass().getName());
- begin=System.currentTimeMillis();
- for(int i=0;i<CIRCLE;i++)
- d.request();
- System.out.println("callCglibProxy:"+(System.currentTimeMillis()-beg in));
- begin=System.currentTimeMillis();
- d=createJavassistDynProxy(); //测试Javaassist动态代理
- System.out.println("createJavassistDynProxy:"+(System.currentTimeMil lis()-begin));
- System.out.println("JavassistDynProxy class:"+d.getClass().getName());
- begin=System.currentTimeMillis();
- for(int i=0;i<CIRCLE;i++)
- d.request();
- System.out.println("callJavassistDynProxy:"+(System.currentTimeMilli s()-begin));
- begin=System.currentTimeMillis();
- d=createJavassistBytecodeDynamicProxy(); //测试Javassist动态代理
- System.out.println("createJavassistBytecodeDynamicProxy:"+(System.cu rrentTimeMillis()-begin));
- System.out.println("JavassistBytecodeDynamicProxy class:"+d.getClass().
- getName());
- begin=System.currentTimeMillis();
- for(int i=0;i<CIRCLE;i++)
- d.request();
- System.out.println("callJavassistBytecodeDynamicProxy:"+(System.curr entTimeMillis()-begin));
- }
以上代码分别生成了4种代理,并对生成的代理类进行高频率的调用,最后输出各个代理类的创建耗时,动态类类名和方法调用耗时。结果如下:
- createJdkProxy:0
- JdkProxy class:$Proxy0
- callJdkProxy:610
- createCglibProxy:140
- CglibProxy class:$javatuning.ch2.proxy.IDBQuery$$EnhancerByCGLIB$$b75a4bbf
- callCglibProxy:594
- createJavassistDynProxy:47
- JavassistDynProxy class:javatuning.ch2.proxy.IDBQuery_$$_javassist_0
- callJavassistDynProxy:1422
- createJavassistBytecodeDynamicProxy:94
- JavassistBytecodeDynamicProxy class:javatuning.ch2.proxy.IDBQueryJavaassistBytecodeProxy
- callJavassistBytecodeDynamicProxy:562
可以看到,JDK的动态类创建过程最快,这是因为在这个内置实现中defineClass()方法被定义为native实现,故性能高于其他几种实现。但在代理类的函数调用性能上,JDK的动态代理就不如CGLIB和Javassist的基于动态代码的代理,而Javassist的基于代理工厂的代理实现,代理的性能质量最差,甚至不如JDK的实现。在实际开发应用中,代理类的方法调用频率通常要远远高于代理类的实际生成频率(相同类的重复生成会使用cache),故动态代理对象的方法调用性能应该作为性能的主要关注点。
注意:就动态代理的方法调用性能而言,CGLIB和Javassist的基于动态代码的代理都优于JDK自带的动态代理。此外,JDK的动态代理要求代理类和真实主题都实现同一个接口,而CGLIB和Javassist没有强制要求。
5.Hibernate中代理模式的应用
用代理模式实现延迟加载的一个经典应用就在Hibernate框架中。当Hibernate加载实体bean时,并不会一次性将数据库所有的数据都装载。默认情况下,它会采取延迟加载的机制,以提高系统的性能。Hiberante中的延迟加载主要有两种:一是属性的延迟加载,二是关联表的延时加载。这里以属性的延迟加载为例,简单阐述Hibernate是如何使用动态代理的。
假定有用户模型:
- public class User implements java.io.Serializable {
- private Integer id;
- private String name;
- private int age;
- //省略getter和setter
使用以下代码,通过Hibernate加载一条User信息:
- public static void main(String[] args) throws SecurityException,
- NoSuchFieldException,
- IllegalArgumentException,
- IllegalAccessException {
- //从数据库载入ID为1的用户
- User u=(User)HibernateSessionFactory.getSession().load(User.class, 1);
- //打印类名称
- System.out.println("Class Name:"+u.getClass().getName());
- //打印父类名称
- System.out.println("Super Class Name:"+u.getClass().getSuperclass().
- getName());
- //实现的所有接口
- Class[] ins=u.getClass().getInterfaces();
- for(Class cls:ins){
- System.out.println("interface:"+cls.getName());
- }
- System.out.println(u.getName());
- }
以上代码中,在session.load()方法后,首先输出了User的类名、它的超类、User实现的接口,最后输出调用User的getName()方法取得数据库数据。这段程序的输出如下(本例中使用Hibernate 3.2.6,不同的Hibernate版本实现会有细节上的差异):
- Class Name:$javatuning.ch2.proxy.hibernate.User$$EnhancerByCGLIB$$
- 96d498be
- Super Class Name:javatuning.ch2.proxy.hibernate.User
- interface:org.hibernate.proxy.HibernateProxy
- Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from test.user user0_ where user0_.id=?
- Geym
仔细观察这段输出,可以看到,session的载入类并不是之前定义的User类,而是名叫javatuning.ch2.proxy.hibernate.User$$EnhancerByCGLIB$$96d498bed的类。从名称上可以推测,它是使用CGLIB的Enhancer类生成的动态类。该类的父类才是应用程序定义的User类。
此外,它实现了HibernateProxy接口。由此可见,Hibernate使用一个动态代理子类替代用户定义的类。这样,在载入对象时,就不必初始化对象的所有信息,通过代理,拦截原有的getter方法,可以在真正使用对象数据时,才去数据库加载实际的数据,从而提升系统性能。由这段输出的顺序来看,也正是这样,在getName()被调用之前,Hibernate从未输出过一条SQL语句。这表示:User对象被加载时,根本就没有访问数据库,而在getName()方法被调用时,才真正完成了数据库操作。
注意:Hibernate框架中对实体类的动态代理是代理模式用于延迟加载的经典实现。有兴趣的读者,可以深入研究Hibernate的内部实现。