代理模式性能提升--延时加载

一、代理模式的结构

  代理模式主要参与者又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()方法被调用时,才真正完成了数据库操作。

 

 

 

 

 

 

posted @ 2019-09-15 15:40  加了冰的才叫可乐  阅读(677)  评论(0编辑  收藏  举报