代理模式性能提升--延时加载
一、代理模式的结构
代理模式主要参与者又4个,如下表所示:
角色 | 作用 |
主题接口 | 定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法 |
真实主题 | 真正实现业务逻辑的类 |
代理类 | 用来代理和封装真实主题 |
Main | 客户端,使用代理类和主题接口完成一些工作 |
以一个简单的示例来阐述使用代理模式实现延时加载的方法及意义。假设客户端软件,有根据用户请求,去数据库查询数据的功能。
在查询数据库之前,需要获得数据库连接,软件开启时,初始化系统的所有类,此时尝试获得数据库连接,当系统有大量的类似操作存在时,
(比如xml解析等),所有初始化这些操作的叠加,会使得系统启动速度变得非常缓慢。为此使用代理模式,使用代理类,封装对数据库查询
中的初始化操作,当系统启动时,初始化这个代理类,而非真实的数据库查询类,而代理什么都没有做,因此,它的构造是非常快速的。
在系统启动时,将消耗资源最多的方法都使用代理模式分离,就可以加快系统的启动速度,减少用户的等待时间。而在用户真正的查询
操作时,再由代理类,单独去加载真实的数据库查询类,完成用户的请求,这个过程就是使用了代理模式现实了延迟加载。
延时加载的核心思想是:如果当前并没有使用这个组件,则不需要真正的初始化它,使用一个代理对象他带它原有的位置,
只要在真正需要使用的时候,才对它进行加载。使用代理模式的延时加载是非常有意义的,首先,它可以在时间轴上分散系统压力,
尤其在系统启动时,不必完成所有的初始化工作,从而加快系统的启动速度;其次,对很多真实主题而言,在软件启动直到被关闭的
整个过程中,可能根本不会被调用,初始化这些数据完全是在浪费资源。下图显示了使用代理类封装数据库查询类后,系统的启动过程。
系统若不使用代理模式,则在启动时就回去初始化DBQuery对象,而使用代理模式之后,启动时只需要
初始化一个轻量级的对象DBQueryProxy。
系统的机构如下图所示,IDBQuery是主题接口,定义代理类和和真实类需要对外提供的服务,在本例中
定义了实现数据库查询的公共方法request()函数。DBQuery是真实主题,负责实际的业务操作,DBQueryProxy
是DBQuery的代理类。
二、代理模式的实现和使用
基于以上设计,IDBQuery的实现如下,它只有一个request()方法:
public interface IDBQuery{ String request(); }
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(); //在真正使用时才创建真是对象 } }
三、动态代理介绍
动态代理是指在运行时,动态的生成代理类。即,代理类的字节码将在运行时生成并载入当前的ClassLoader。于静态代理类相比,
动态代理类有诸多好处。首先,不需要位真实主题写一个形式上完全一样的封装类,假如主题接口中的方法比较多,为每一个接口写一
个代理方法也是非常烦人的事,如果接口有变动,则真实主题和代理类都需要改动,不利于系统维护;其次,使用一些动态代理的生成
方法甚至可以在运行时指定代理类的执行逻辑,从而大大挺升系统的灵活性。
生成动态代理的方法很多,如,JDK自带的动态代理、CGLIB、Javassist或者ASM库。JDK的动态代理很简单,它内置在JDK中,
因此不需要引入第三方开发包,但相对功能比较弱。CGLIB和Javassist都是高级的字节码生成库,总体性能比JDK自带的动态代理好,
而且功能十分强大。ASM是低级的字节码生成工具,使用ASM已经近乎于在使用Java bytecode编程,对开发人员要求最高,当然,
也是性能最好的一种动态代理生成工具。但ASM的使用实在过于频繁,而且性能也没有数量级的提升,于CGLIB等高级字节码生成
工具相比,ASM程序的可维护性也很差,如果不是在对性能有苛刻要求的场合,还是推荐CGLIB和Javassist。
四、动态代理实现
以上例中的DBQueryProxy为例,使用动态代理生成动态类,替代上例中的DBQueryProxy。首先,使用JDK的动态代理生成动
态对象。JDK的动态代理需要实现一个处理方法调用Handle,用于实现代理方法的内部逻辑。
public class JdkDbQueryHandler 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 JdkDbQueryHandler()); return jdkProxy; }
以上代码生成类一个实现了IDBQuery接口的代理类,代理类的内部逻辑由jdkDbQueryHandler决定。生成代理类之后,
由newProxyInstance()方法返回该代理类的一个实例。致此,一个完整的JDK动态代理就完成了。
CGLIB和Javassist生成动态代理的使用和JDK的动态代理非常类似。下面,尝试使用CGLIB生成动态代理。CGLIB也
需要实现一个处理代理逻辑的切入类:
public class CglibDbQueryInterceptor implements MethodInterceptor { IDBQuery real = null; @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) 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为基础,创建动态Javassist代理:
public static IDBQuery creatJavassistDynProxy() 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 JavassistDynDbQueryHandler()); return javassistProxy; }
Javassist使用动态Java代码创建代理的过程和前文的方法略有不同。Javassist内部可以通过动态Java代码,生成字节码。这种方式创建的
动态代理可以非常灵活,甚至可以在运行是生成业务逻辑:
public static IDBQuery createJavassistBytecodeDynamicProxy() throws Exception { ClassPool mpool = new ClassPool(true); //定义类名 CtClass mCtr = mpool.makeClass(IDBQuery.class.getName()+"JavassistBytecodeProxy"); //需要实现的接口 mCtr.addInterface(mpool.get(IDBQuery.class.getName())); //添加构造函数 mCtr.addConstructor(CtNewConstructor.defaultConstructor(mCtr)); //添加类的字段信息,使用动态Java代码 mCtr.addField(CtField.make("public " +IDBQuery.class.getName() + "real;", mCtr)); String dbqueryname = DBQUery.class.getName(); //添加方法,这里使用动态Java代码指定内部逻辑 mCtr.addMethod(CtNewMethod.make("public String request() { if(real ==" + "null) {real = new "+ dbqueryname+"();} return real.request(); }", mCtr)); //基于以上信息,生成动态类 Class pc = mCtr.toClass(); //生成动态类的实例 IDBQuery bytecodeProxy = (IDBQuery) pc.newInstance(); return bytecodeProxy; }
在以上代码中,使用CtField.make()方法和CtNewMethod.make()方法在运行时生成代理类的字段和方法。这些逻辑由Javassist的CtClass对象处理,将Java代码转换为对应的字节码,并生成动态代理类的实例。
在Java中,动态代理类的生成主要涉及对ClassLoader的使用。这里CGLIB为例,简要阐述动态类的加载过程。使用CGLIB生成动态代理,首先需要生成Enhancer类实例,并指定用于处理代理业务的回调类。在Enhancer.create()方法中,会使用DefaultGeneratorStrategy.Generate()方法生成动态代理类的字节码,并保存在byte数组中。接着使用ReflectUtils.defineClass()方法,通过反射,调用ClassLoader.defindeClass()方法,将字节码装载到ClassLoader中,完成类的加载。最后使用ReflectUtils.newInstance()方法,通过反射,生成动态类的实例,并返回该实例。无论使用何种方法生成动态代理,虽然实现细节不同,但主要逻辑如下图所示。
前文介绍的几种动态代理的生成方法,性能有一定差异。为了能更好地测试他们的性能,去掉
DBQuery类中的sleep()代码,并使用以下方法测试:
public static final int CIRCLE = 30000000; public static void main(String[] args) throws Exception { IDBQuer d = null; long begin = System.currentTimeMillis(); d = createJdkProxy(); //测试JDK动态代理 System.out.println("createJdkProxy:" + (System.currentTimeMillis() - begin)); 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() - begin)); 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() - begin); begin = System.currentTimeMillis(); d = createJavassistDynProxy(); //测试Javassist动态代理 System.out.println("createJavassistDynProxy:" + d.getClass().getName())); begin = System.currentTimeMillis(); for (int i = 0; i < CIRCLE; i++) { d.request(); } System.out.println("callJavassistDynProxy:" + (System.currentTimeMillis() - begin)); begin = System.currentTimeMillis(); d = createJavassistBytecodeDynamicProxy(); //测试Javassist动态代理 System.out.println("createJavassistBytecodeDynamicProxy:" + (System.currentTimeMillis() - begin)); System.out.println("JavassstBytecodeDynamicProxy class:" + d.getClass().getName()); begin = System.currentTimeMillis(); for (int i = 0; i < CIRCLE; i++) { d.request(); } System.out.println("callJavassistBytecodeDynamicProxy:" + System.currentTimeMillis() - begin); }
以上代码分别生成了4中代理,并对生成的代理类进行高频率调用,最后输出各个代理类的创建耗时,动态类类名和方法调用耗时。结果如下:
可以看到,JDK的动态创建过程最快,这是因为在这个内置实现中defineClass()方法被定义为native实现,故性能高于其他的几种实现。但在代理类的函数调用性能上,JDK的动态代理就不如CGLIB和Javassist的基于动态代码的代理,而Javassist的基于代理工程的代理实现,代理的性能质量最差,甚至不如JDK的实现。在实际开发应用中,代理类的方法调用频率通常要远远高于代理类的实际生成频率(相同类的重复生成会使用cache),故动态代理对象的方法调用性能应该作为性能的主要关注点。
五、Hibernate中代理模式的应用
用代理模式实现延时加载的一个经典应用在Hibernate框架中。当Hibernate加载实体bean时,并不会一次性将数据库所有的数据都装载。默认情况下,它会采去延时加载的机制,以提高系统的性能。Hibernate中的延时加载主要有两种:一是属性的延时加载,二是关联表的延时加载。这里以属性的延时加载为例,简单阐述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("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()方法取得数据库数据。这段代码的输出如下(Hibernate3.2.6):
仔细观察这段输出,可以看到,session的载入类并不是之前定义的User 类,而是名叫javatuning.ch2.proxy.hibernate.User$$EnhancerByCGLIB$$96d498be 的类。从名称上可以推测,它是使用了CGLIB的Enhancer类生成的动态类。该类的父类才是应用程序定义的User 类。
此外,它实现类HibernateProxy接口。由此可见,Hibernate使用一个动态代理子类替代用户定义的类。这样,在载入对象是,就不必初始化对象的所有信息,通过代理,拦截所有的getter方法,可以在真正使用对象数据时,采取数据库加载实际的数据,从而提升系统性能。由这段输出的顺序来看,也正是这样,在getName()被调用之前,Hibernate从未输出过一条SQL语句。这表示:User 对象被加载时,根本没有访问数据库,而在getName()方法被调用时,才真正完成了数据库操作。