2.1 善用设计模式

来源:互联网 发布:网络连接不稳定 编辑:程序博客网 时间:2024/06/06 19:14


第2章  设计优化

本章主要介绍与软件设计相关的性能优化方法和思想。软件的结构对系统整体性能有着重要的影响。优秀的设计结构可以规避很多潜在的性能问题,对系统性能的影响可能远远大于代码的优化。因此,熟悉一些常用的软件设计模式和方法,对设计高性能软件有着重要的帮助。本章着眼于设计优化,主要讲解了一些常用的与性能相关的设计模式、组件和设计方法。

本章涉及的主要知识点有:

单例模式的使用和实现;

代理模式的实现和深入剖析;

享元模式的应用;

装饰者模式对性能组件的封装;

观察者模式的使用;

使用Value Object模式减少网络数据传输;

使用业务代理模式添加远程调用缓存;

缓冲和缓存的定义和使用;

对象池的使用场景及其基本实现;

构建负载均衡系统以及Terracotta框架的简单使用;

时间换空间和空间换时间的基本思路。

2.1  善用设计模式

设计模式是前人工作的总结和提炼。通常,被人们广泛流传的设计模式都是对某一特定问题的成熟的解决方案。如果能合理地使用设计模式,不仅能使系统更容易被他人理解,同时也能使系统拥有更加合理的结构。本节总结归纳了一些经典的设计模式,并详细说明它们与软件性能之间的关系。

2.1.1  单例模式(1)

单例模式是设计模式中使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。在Java语言中,这样的行为能带来两大好处:

(1)对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。

(2)由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。

因此对于系统的关键组件和被频繁使用的对象,使用单例模式便可以有效地改善系统的性能。

单例模式的参与者非常简单,只有单例类和使用者两个,如表2.1所示。

表2.1  单例模式角色

image

它的基本结构如图2.1所示。

单例模式的核心在于通过一个接口返回唯一的对象实例。一个简单的单例实现如下:

  1. public class Singleton { 
  2.     private Singleton(){ 
  3.         System.out.println("Singleton is create"); //创建单例的过程可能会比较慢 
  4.     } 
  5.     private static Singleton instance = new Singleton(); 
  6.     public static Singleton getInstance() { 
  7.         return instance; 
  8.     }  

注意代码中的重点标注部分,首先单例类必须要有一个private访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化,这点是相当重要的;其次,instance成员变量和getInstance()方法必须是static的。

注意:单例模式是非常常用的一种结构,几乎所有的系统中都可以找到它的身影。因此,希望读者可以通过本节,了解单例模式的几种实现方式及其各自的特点。

这种单例的实现方式非常简单,而且十分可靠。它唯一的不足仅是无法对instance实例做延迟加载。假如单例的创建过程很慢,而由于instance成员变量是static定义的,因此在JVM加载单例类时,单例对象就会被建立,如果此时,这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到。比如单例类作为String工厂,用于创建一些字符串(该类既用于创建单例Singleton,又用于创建String对象):

  1. public class Singleton { 
  2.     private Singleton() { 
  3.         System.out.println("Singleton is create");  
  4.                                             //创建单例的过程可能会比较慢 
  5.     } 
  6.  
  7.     private static Singleton instance = new Singleton(); 
  8.     public static Singleton getInstance() { 
  9.         return instance; 
  10.     } 
  11.  
  12.     public static void createString(){      //这是模拟单例类扮演其他角色 
  13.         System.out.println("createString in Singleton"); 
  14.     } 

2.1.1  单例模式(2)

当使用Singleton.createString()执行任务时,程序输出:

  1. Singleton is create 
  2. createString in Singleton

可以看到,虽然此时并没有使用单例类,但它还是被创建出来,这也许是开发人员所不愿意见到的。为了解决这个问题,并以此提高系统在相关函数调用时的反应速度,就需要引入延迟加载机制。

  1. public class LazySingleton { 
  2.     private LazySingleton(){ 
  3.         System.out.println("LazySingleton is create"); 
  4.                                             //创建单例的过程可能会比较慢 
  5.     } 
  6.     private static LazySingleton instance = null; 
  7.         public static synchronized LazySingleton getInstance() { 
  8.         if (instance==null) 
  9.             instance=new LazySingleton(); 
  10.         return instance; 
  11.     } 

首先,对于静态成员变量instance初始值赋予null,确保系统启动时没有额外的负载;其次,在getInstance()工厂方法中,判断当前单例是否已经存在,若存在则返回,不存在则再建立单例。这里尤其还要注意,getInstance()方法必须是同步的,否则在多线程环境下,当线程1正新建单例时,完成赋值操作前,线程2可能判断instance为null,故线程2也将启动新建单例的程序,而导致多个实例被创建,故同步关键字是必须的。

使用上例中的单例实现,虽然实现了延迟加载的功能,但和第一种方法相比,它引入了同步关键字,因此在多线程环境中,它的时耗要远远大于第一种单例模式。以下测试代码就说明了这个问题:

  1. @Override 
  2. public void run(){ 
  3.     for(int i=0;i<100000;i++) 
  4.         Singleton.getInstance(); 
  5.         //LazySingleton.getInstance(); 
  6.     System.out.println("spend:"+(System.currentTimeMillis()-begintime));     

开启5个线程同时完成以上代码的运行,使用第1种类型的单例耗时0ms,而使用LazySingleton却相对耗时约390ms。性能至少相差2个数量级。

注意:在本书中,会使用很多类似的代码片段用于测试不同代码的执行速度。在不同的计算机上其测试结果很可能与笔者不同。读者大可不必关心测试数据的绝对值,只要观察用于比较的目标代码间的相对耗时即可。

为了使用延迟加载引入的同步关键字反而降低了系统性能,是不是有点得不偿失呢?为了解决这个为问题,还需要对其进行改进:

  1. public class StaticSingleton { 
  2.     private StaticSingleton(){ 
  3.         System.out.println("StaticSingleton is create"); 
  4.     } 
  5.     private static class SingletonHolder { 
  6.         private static StaticSingleton instance = new StaticSingleton(); 
  7.     } 
  8.     public static StaticSingleton getInstance() { 
  9.         return SingletonHolder.instance; 
  10.     } 

在这个实现中,单例模式使用内部类来维护单例的实例,当StaticSingleton被加载时,其内部类并不会被初始化,故可以确保当StaticSingleton类被载入JVM时,不会初始化单例类,而当getInstance()方法被调用时,才会加载SingletonHolder,从而初始化instance。同时,由于实例的建立是在类加载时完成,故天生对多线程友好,getInstance()方法也不需要使用同步关键字。因此,这种实现方式同时兼备以上两种实现的优点。

注意:使用内部类的方式实现单例,既可以做到延迟加载,也不必使用同步关键字,是一种比较完善的实现。

2.1.1  单例模式(3)

通常情况下,用以上方式实现的单例已经可以确保在系统中只存在唯一实例了。但仍然有例外情况,可能导致系统生成多个实例,比如,在代码中,通过反射机制,强行调用单例类的私有构造函数,生成多个单例。考虑到情况的特殊性,本书中不对这种极端的方式进行讨论。但仍有些合法的方法,可能导致系统出现多个单例类的实例。

一个可以被串行化的单例:

  1. public class SerSingleton implements java.io.Serializable{ 
  2.     String name; 
  3.      
  4.     private SerSingleton() { 
  5.         System.out.println("Singleton is create"); 
  6.                                         //创建单例的过程可能会比较慢 
  7.         name="SerSingleton"; 
  8.     } 
  9.  
  10.     private static SerSingleton instance = new SerSingleton(); 
  11.     public static SerSingleton getInstance() { 
  12.         return instance; 
  13.     } 
  14.  
  15.     public static void createString(){ 
  16.         System.out.println("createString in Singleton"); 
  17.     } 
  18.      
  19.     private Object readResolve(){       //阻止生成新的实例,总是返回当前对象 
  20.         return instance;   
  21.     }   

测试代码如下:

  1. @Test 
  2. public void test() throws Exception { 
  3.     SerSingleton s1 = null; 
  4.     SerSingleton s = SerSingleton.getInstance(); 
  5.     //先将实例串行化到文件 
  6.     FileOutputStream fos = new FileOutputStream("SerSingleton.txt"); 
  7.     ObjectOutputStream oos = new ObjectOutputStream(fos); 
  8.     oos.writeObject(s); 
  9.     oos.flush(); 
  10.     oos.close(); 
  11.     //从文件读出原有的单例类 
  12.     FileInputStream fis = new FileInputStream("SerSingleton.txt"); 
  13.     ObjectInputStream ois = new ObjectInputStream(fis); 
  14.     s1 = (SerSingleton) ois.readObject(); 
  15.      
  16.     Assert.assertEquals(s, s1); 

使用一段测试代码测试单例的串行化和反串行化,当去掉SerSingleton代码中加粗的readReslove()函数时,以下测试代码抛出异常:

  1. junit.framework.AssertionFailedError: expected:javatuning.ch2.singleton. 
  2. serialization.SerSingleton@5224ee 
  3. but was:javatuning.ch2.singleton.serialization.SerSingleton@18fe7c3 

说明测试代码中s和s1指向了不同的实例,在反序列化后,生成多个对象实例。而加上readReslove()函数的,程序正常退出。说明,即便经过反序列化,仍然保持了单例的特征。事实上,在实现了私有的readReslove()方法后,readObject()已经形同虚设,它直接使用readReslove()替换了原本的返回值,从而在形式上构造了单例。

注意:序列化和反序列化可能会破坏单例。一般来说,对单例进行序列化和反序列化的场景并不多见,但如果存在,就要多加注意。

2.1.2  代理模式(1)

代理模式也是一种很常见的设计模式。它使用代理对象完成用户请求,屏蔽用户对真实对象的访问。就如同现实中的代理一样,代理人被授权执行当事人的一些事宜,而无需当事人出面,从第三方的角度看,似乎当事人并不存在,因为他只和代理人通信。而事实上,代理人是要有当事人的授权,并且在核心问题上还需要请示当事人。

在现实中,使用代理的情况很普遍,而且原因也很多。比如,当事人因为某些隐私不方便出面,或者当事人不具备某些相关的专业技能,而需要一个职业人员来完成一些专业的操作,也可能由于当事人没有时间处理事务,而聘用代理人出面。

在软件设计中,使用代理模式的意图也很多,比如因为安全原因,需要屏蔽客户端直接访问真实对象;或者在远程调用中,需要使用代理类处理远程方法调用的技术细节(如RMI);也可能是为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的。在本小节中,主要讨论使用代理模式实现延迟加载,从而提升系统的性能和反应速度。

1.代理模式的结构

代理模式的主要参与者有4个,如表2.2所示。

表2.2  代理模式角色

image

以一个简单的示例来阐述使用代理模式实现延迟加载的方法及其意义。假设某客户端软件,有根据用户请求,去数据库查询数据的功能。在查询数据前,需要获得数据库连接,软件开启时,初始化系统的所有类,此时尝试获得数据库连接。当系统有大量的类似操作存在时(比如xml解析等),所有这些初始化操作的叠加,会使得系统的启动速度变得非常缓慢。为此,使用代理模式,使用代理类,封装对数据库查询中的初始化操作,当系统启动时,初始化这个代理类,而非真实的数据库查询类,而代理类什么都没有做,因此,它的构造是相当迅速的。

在系统启动时,将消耗资源最多的方法都使用代理模式分离,就可以加快系统的启动速度,减少用户的等待时间。而在用户真正做查询操作时,再由代理类,单独去加载真实的数据库查询类,完成用户的请求。这个过程就是使用代理模式实现了延迟加载。

注意:代理模式可以用于多种场合,如用于远程调用的网络代理、考虑安全因素的安全代理等。延迟加载只是代理模式的一种应用场景。

延迟加载的核心思想是:如果当前并没有使用这个组件,则不需要真正地初始化它,使用一个代理对象替代它的原有的位置,只要在真正需要使用的时候,才对它进行加载。使用代理模式的延迟加载是非常有意义的,首先,它可以在时间轴上分散系统压力,尤其在系统启动时,不必完成所有的初始化工作,从而加速启动时间;其次,对很多真实主题而言,在软件启动直到被关闭的整个过程中,可能根本不会被调用,初始化这些数据无疑是一种资源浪费。图2.2显示了使用代理类封装数据库查询类后,系统的启动过程。

若系统不使用代理模式,则在启动时就要初始化DBQuery对象,而使用代理模式后,启动时只需要初始化一个轻量级的对象DBQueryProxy。

系统的结构图如图2.3所示,IDBQuery是主题接口,定义代理类和真实类需要对外提供的服务,在本例中了定义了实现数据库查询的公共方法request()函数。DBQuery是真实主题,负责实际的业务操作,DBQueryProxy是DBQuery的代理类。

2.1.2  代理模式(2)

2.代理模式的实现和使用

基于以上设计,IDBQuery的实现如下,它只有一个request()方法:

  1. public interface IDBQuery { 
  2.     String request(); 

DBQuery实现如下,它是一个重量级对象,构造会比较慢:

  1. public class DBQuery implements IDBQuery{ 
  2.     public DBQuery(){ 
  3.         try { 
  4.             Thread.sleep(1000);             //可能包含数据库连接等耗时操作 
  5.         } catch (InterruptedException e) { 
  6.             e.printStackTrace(); 
  7.         } 
  8.     } 
  9.     @Override 
  10.     public String request() { 
  11.         return "request string"; 
  12.     } 

代理类DBQueryProxy是轻量级对象,创建很快,用于替代DBQuery的位置:

  1. public class DBQueryProxy implements IDBQuery { 
  2.     private DBQuery real=null;  
  3.     @Override 
  4.     public String request() { 
  5.         //在真正需要的时候,才创建真实对象,创建过程可能很慢 
  6.         if(real==null) 
  7.             real=new DBQuery(); 
  8.         //在多线程环境下,这里返回一个虚假类,类似于Future模式 
  9.         return real.request(); 
  10.     } 

最后,主函数如下,它引用IDBQuery接口,并使用代理类工作:

  1. public class Main { 
  2.     public static void main(String args[]){ 
  3.         IDBQuery q=new DBQueryProxy();      //使用代理 
  4.         q.request();                        //在真正使用时才创建真实对象 
  5.     } 

注意:将代理模式用于实现延迟加载,可以有效地提升系统的启动速度,对改善用户体验有很大的帮助。

2.1.2  代理模式(3)

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,用于实现代理方法的内部逻辑。

  1. public class JdkDbQeuryHandler implements InvocationHandler { 
  2.     IDBQuery real=null;                 //主题接口 
  3.      
  4.     @Override 
  5.     public Object invoke(Object proxy, Method method, Object[] args) 
  6.             throws Throwable { 
  7.         if(real==null) 
  8.             real=new DBQuery();         //如果是第一次调用,则生成真实对象 
  9.         return real.request();          //使用真实主题完成实际的操作 
  10.     } 

以上代码实现了一个Handler,可以看到,它的内部逻辑和DBQueryProxy是类似的。在调用真实主题的方法前,先尝试生成真实主题对象。接着,需要使用这个Handler生成动态代理对象:

  1. public static IDBQuery createJdkProxy(){ 
  2.     IDBQuery jdkProxy = (IDBQuery) Proxy.newProxyInstance( 
  3.             ClassLoader.getSystemClassLoader(),   
  4.             new Class[] { IDBQuery.class },  
  5.             new JdkDbQeuryHandler());                   //指定Handler 
  6.         return jdkProxy;   

以上代码生成一个实现了IDBQuery接口的代理类,代理类的内部逻辑由JdkDbQeuryHandler决定。生成代理类后,由newProxyInstance()方法返回该代理类的一个实例。至此,一个完整的JDK动态代理就完成了。

CGLIB和Javassist的动态代理的使用和JDK的动态代理非常类似。下面,尝试使用CGLIB生成动态代理。CGLIB也需要实现一个处理代理逻辑的切入类:

  1. public class CglibDbQueryInterceptor implements MethodInterceptor { 
  2.     IDBQuery real=null; 
  3.     @Override 
  4.     public Object intercept(Object arg0, Method arg1, Object[] arg2, 
  5.             MethodProxy arg3) throws Throwable { 
  6.         if(real==null)                              //代理类的内部逻辑 
  7.                                                     //和前文中的一样 
  8.             real=new DBQuery(); 
  9.         return real.request();   
  10.     } 

2.1.2  代理模式(4)

在这个切入对象的基础上,可以生成动态代理:

  1. public static IDBQuery createCglibProxy(){ 
  2.        Enhancer enhancer = new Enhancer();   
  3.        enhancer.setCallback(new CglibDbQueryInterceptor()); 
  4.                                                 //指定切入器,定义代理类逻辑 
  5.        enhancer.setInterfaces(new Class[] { IDBQuery.class }); 
  6.                                                 //指定实现的接口 
  7.        IDBQuery cglibProxy = (IDBQuery) enhancer.create(); 
  8.                                                 //生成代理类的实例 
  9.        return cglibProxy;   

使用Javassist生成动态代理可以使用两种方式:一种是使用代理工厂创建,另一种通过使用动态代码创建。使用代理工厂创建时,方法与CGLIB类似,也需要实现一个用于代理逻辑处理的Handler:

  1. public class JavassistDynDbQueryHandler implements MethodHandler { 
  2.     IDBQuery real=null; 
  3.     @Override 
  4.     public Object invoke(Object arg0, Method arg1, Method arg2, Object[]    arg3) 
  5.             throws Throwable { 
  6.         if(real==null) 
  7.             real=new DBQuery(); 
  8.         return real.request();   
  9.     } 

以这个Handler为基础,创建动态Javasssit代理:

  1. public static IDBQuery createJavassistDynProxy()  throws Exception { 
  2.     ProxyFactory proxyFactory = new ProxyFactory();   
  3.     proxyFactory.setInterfaces(new Class[] { IDBQuery.class });//指定接口 
  4.     Class proxyClass = proxyFactory.createClass();   
  5.     IDBQuery javassistProxy = (IDBQuery) proxyClass.newInstance();   
  6.                                                     //设置Handler处理器 
  7.     ((ProxyObject) javassistProxy).setHandler(new JavassistDynDbQuery-  Handler());   
  8.     return javassistProxy;   

Javassist使用动态Java代码创建代理的过程和前文的方法略有不同。Javassist内部可以通过动态Java代码,生成字节码。这种方式创建的动态代理可以非常灵活,甚至可以在运行时生成业务逻辑。

  1. public static IDBQuery createJavassistBytecodeDynamicProxy() throws Exception { 
  2.     ClassPool mPool = new ClassPool(true); 
  3.     //定义类名 
  4.     CtClass mCtc = mPool.makeClass(IDBQuery.class.getName() + "Javaassist- 
  5.     BytecodeProxy"); 
  6.     //需要实现的接口 
  7.     mCtc.addInterface(mPool.get(IDBQuery.class.getName()));   
  8.     //添加构造函数 
  9.     mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));   
  10.     //添加类的字段信息,使用动态Java代码 
  11.     mCtc.addField(CtField.make("public " + IDBQuery.class.getName() + "  
  12.     real;", mCtc));  
  13.     String dbqueryname=DBQuery.class.getName(); 
  14.     //添加方法,这里使用动态Java代码指定内部逻辑 
  15.     mCtc.addMethod(CtNewMethod.make("public String request() { if(real== 
  16.     null)real=new "+dbqueryname+"();return real.request(); }", mCtc));   
  17.     //基于以上信息,生成动态类 
  18.     Class pc = mCtc.toClass();  
  19.     //生成动态类的实例  
  20.     IDBQuery bytecodeProxy = (IDBQuery) pc.newInstance(); 
  21.     return bytecodeProxy;   

2.1.2  代理模式(5)

在以上代码中,使用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  实现动态代理的基本步骤

前文介绍的几种动态代理的生成方法,性能有一定差异。为了能更好地测试它们的性能,去掉DBQuery类中的sleep()代码,并使用以下方法测试:

  1. public static final int CIRCLE=30000000; 
  2. public static void main(String[] args) throws Exception { 
  3.     IDBQuery d=null; 
  4.     long begin=System.currentTimeMillis(); 
  5.     d=createJdkProxy();                     //测试JDK动态代理 
  6.     System.out.println("createJdkProxy:"+(System.currentTimeMillis()-beg    in)); 
  7.     System.out.println("JdkProxy class:"+d.getClass().getName()); 
  8.     begin=System.currentTimeMillis(); 
  9.     for(int i=0;i<CIRCLE;i++) 
  10.         d.request(); 
  11.     System.out.println("callJdkProxy:"+(System.currentTimeMillis()-begin    )); 
  12.  
  13.     begin=System.currentTimeMillis(); 
  14.     d=createCglibProxy();                   //测试CGLIB动态代理 
  15.     System.out.println("createCglibProxy:"+(System.currentTimeMillis()-b    egin)); 
  16.     System.out.println("CglibProxy class:"+d.getClass().getName()); 
  17.     begin=System.currentTimeMillis(); 
  18.     for(int i=0;i<CIRCLE;i++) 
  19.         d.request(); 
  20.     System.out.println("callCglibProxy:"+(System.currentTimeMillis()-beg    in)); 
  21.      
  22.     begin=System.currentTimeMillis(); 
  23.     d=createJavassistDynProxy();            //测试Javaassist动态代理 
  24.     System.out.println("createJavassistDynProxy:"+(System.currentTimeMil    lis()-begin)); 
  25.     System.out.println("JavassistDynProxy class:"+d.getClass().getName()); 
  26.     begin=System.currentTimeMillis(); 
  27.     for(int i=0;i<CIRCLE;i++) 
  28.         d.request(); 
  29.     System.out.println("callJavassistDynProxy:"+(System.currentTimeMilli    s()-begin)); 
  30.      
  31.     begin=System.currentTimeMillis(); 
  32.     d=createJavassistBytecodeDynamicProxy();    //测试Javassist动态代理 
  33.     System.out.println("createJavassistBytecodeDynamicProxy:"+(System.cu    rrentTimeMillis()-begin)); 
  34.     System.out.println("JavassistBytecodeDynamicProxy class:"+d.getClass(). 
  35.     getName()); 
  36.     begin=System.currentTimeMillis(); 
  37.     for(int i=0;i<CIRCLE;i++) 
  38.         d.request(); 
  39.     System.out.println("callJavassistBytecodeDynamicProxy:"+(System.curr    entTimeMillis()-begin)); 

2.1.2  代理模式(6)

以上代码分别生成了4种代理,并对生成的代理类进行高频率的调用,最后输出各个代理类的创建耗时,动态类类名和方法调用耗时。结果如下:

  1. createJdkProxy:0 
  2. JdkProxy class:$Proxy0 
  3. callJdkProxy:610 
  4. createCglibProxy:140 
  5. CglibProxy class:$javatuning.ch2.proxy.IDBQuery$$EnhancerByCGLIB$$b75a4bbf 
  6. callCglibProxy:594 
  7. createJavassistDynProxy:47 
  8. JavassistDynProxy class:javatuning.ch2.proxy.IDBQuery_$$_javassist_0 
  9. callJavassistDynProxy:1422 
  10. createJavassistBytecodeDynamicProxy:94 
  11. JavassistBytecodeDynamicProxy class:javatuning.ch2.proxy.IDBQueryJavaassistBytecodeProxy 
  12. callJavassistBytecodeDynamicProxy:562 

可以看到,JDK的动态类创建过程最快,这是因为在这个内置实现中defineClass()方法被定义为native实现,故性能高于其他几种实现。但在代理类的函数调用性能上,JDK的动态代理就不如CGLIB和Javassist的基于动态代码的代理,而Javassist的基于代理工厂的代理实现,代理的性能质量最差,甚至不如JDK的实现。在实际开发应用中,代理类的方法调用频率通常要远远高于代理类的实际生成频率(相同类的重复生成会使用cache),故动态代理对象的方法调用性能应该作为性能的主要关注点。

注意:就动态代理的方法调用性能而言,CGLIB和Javassist的基于动态代码的代理都优于JDK自带的动态代理。此外,JDK的动态代理要求代理类和真实主题都实现同一个接口,而CGLIB和Javassist没有强制要求。

5.Hibernate中代理模式的应用

用代理模式实现延迟加载的一个经典应用就在Hibernate框架中。当Hibernate加载实体bean时,并不会一次性将数据库所有的数据都装载。默认情况下,它会采取延迟加载的机制,以提高系统的性能。Hiberante中的延迟加载主要有两种:一是属性的延迟加载,二是关联表的延时加载。这里以属性的延迟加载为例,简单阐述Hibernate是如何使用动态代理的。

假定有用户模型:

  1. public class User implements java.io.Serializable { 
  2.     private Integer id; 
  3.     private String name; 
  4.     private int age; 
  5.     //省略getter和setter 

使用以下代码,通过Hibernate加载一条User信息:

  1. public static void main(String[] args) throws SecurityException,  
  2.                                             NoSuchFieldException,  
  3.                                             IllegalArgumentException,  
  4.                                             IllegalAccessException { 
  5.     //从数据库载入ID为1的用户 
  6.     User u=(User)HibernateSessionFactory.getSession().load(User.class, 1); 
  7.     //打印类名称 
  8.     System.out.println("Class Name:"+u.getClass().getName()); 
  9.     //打印父类名称 
  10.     System.out.println("Super Class Name:"+u.getClass().getSuperclass(). 
  11.     getName()); 
  12.     //实现的所有接口 
  13.     Class[] ins=u.getClass().getInterfaces(); 
  14.     for(Class cls:ins){ 
  15.         System.out.println("interface:"+cls.getName()); 
  16.     } 
  17.     System.out.println(u.getName()); 

2.1.2  代理模式(7)

以上代码中,在session.load()方法后,首先输出了User的类名、它的超类、User实现的接口,最后输出调用User的getName()方法取得数据库数据。这段程序的输出如下(本例中使用Hibernate 3.2.6,不同的Hibernate版本实现会有细节上的差异):

  1. Class Name:$javatuning.ch2.proxy.hibernate.User$$EnhancerByCGLIB$$ 
  2. 96d498be 
  3. Super Class Name:javatuning.ch2.proxy.hibernate.User 
  4. interface:org.hibernate.proxy.HibernateProxy 
  5. Hibernate: select user0_.id as id0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from test.user user0_ where user0_.id=? 
  6. Geym 

仔细观察这段输出,可以看到,session的载入类并不是之前定义的User类,而是名叫javatuning.ch2.proxy.hibernate.User$$EnhancerByCGLIB$$96d498bed的类。从名称上可以推测,它是使用CGLIB的Enhancer类生成的动态类。该类的父类才是应用程序定义的User类。

此外,它实现了HibernateProxy接口。由此可见,Hibernate使用一个动态代理子类替代用户定义的类。这样,在载入对象时,就不必初始化对象的所有信息,通过代理,拦截原有的getter方法,可以在真正使用对象数据时,才去数据库加载实际的数据,从而提升系统性能。由这段输出的顺序来看,也正是这样,在getName()被调用之前,Hibernate从未输出过一条SQL语句。这表示:User对象被加载时,根本就没有访问数据库,而在getName()方法被调用时,才真正完成了数据库操作。

注意:Hibernate框架中对实体类的动态代理是代理模式用于延迟加载的经典实现。有兴趣的读者,可以深入研究Hibernate的内部实现。

2.1.3  享元模式(1)

享元模式是设计模式中少数几个以提高系统性能为目的的模式之一。它的核心思想是:如果在一个系统中存在多个相同的对象,那么只需共享一份对象的拷贝,而不必为每一次使用都创建新的对象。在享元模式中,由于需要构造和维护这些可以共享的对象,因此,常常会出现一个工厂类,用于维护和创建对象。

享元模式对性能提升的主要帮助有两点:

(1)可以节省重复创建对象的开销,因为被享元模式维护的相同对象只会被创建一次,当创建对象比较耗时时,便可以节省大量时间。

(2)由于创建对象的数量减少,所以对系统内存的需求也减小,这将使得GC的压力也相应地降低,进而使得系统拥有一个更健康的内存结构和更快的反应速度。

享元模式的主要角色由享元工厂、抽象享元、具体享元类和主函数几部分组成。它们的功能如表2.3所示。

表2.3  享元模式角色

image

基于以上角色,享元模式的结构如图2.5所示。

图2.5  享元模式类图

享元工厂是享元模式的核心,它需要确保系统可以共享相同的对象。一般情况下,享元工厂会维护一个对象列表,当任何组件尝试获取享元类时,如果请求的享元类已经被创建,则直接返回已有的享元类;若没有,则创建一个新的享元对象,并将它加入到维护队列中。

注意:享元模式是为数不多的、只为提升系统性能而生的设计模式。它的主要作用就是复用大对象(重量级对象),以节省内存空间和对象创建时间。

享元模式的一个典型应用是在SAAS系统中。SAAS即Software As A Service,是目前比较流行的一种软件应用模式。

以一个人事管理系统的SAAS软件为例,假设公司甲、乙、丙均为这个SAAS系统的用户,则定义每个公司为这套系统的一个租户。每个公司(租户)又各有100个员工。如果这些公司的所有员工都可以登录这套系统查看自己的收入情况,并且为了系统安全,每个公司(租户)都拥有自己独立的数据库。为了使系统的设计最为合理,在这种情况下,便可以使用享元模式为每个租户分别提供工资查询的接口,而一个公司(租户)下的所有员工可以共享一个查询(因为一个租户下所有的员工数据都存放在一个数据库中,它们共享数据库连接)。这样,系统只需要3个享元实例,就足以应付300个员工的查询请求了。系统的结构如图2.6所示。

2.1.3  享元模式(2)

图2.6中,ReportManagerFactory为享元工厂,负责创建具体的报表工具,它确保每个公司(租户)下所有的员工,都共享一个具体的享元实例(FinancialReportManager或者EmployeeReportManager)。这样,当公司甲的两个员工登录,进行财务查询时,系统不必为两个员工都新建FinancialReportManager,而可以让他们共享一个FinancialReportManager实例。

通过这个示例,还可以进一步了解享元工厂和对象池的一个重要区别。在一个对象池中,所有的对象都是等价的,任意两个对象在任何使用场景中都可以被对象池中的其他对象代替。而在享元模式中,享元工厂所维护的所有对象都是不同的,任何两个对象间不能相互代替。如本例中,为公司甲创建的FinancialReportManagerA和为公司乙创建的FinancialReportManagerB分别对应了后台各自不同的数据库,因此,两者是不可相互替代的。

注意:享元模式和对象池的最大不同在于:享元对象是不可相互替代的,它们各自都有各自的含义和用途;而对象池中的对象都是等价的,如数据库连接池中的数据库连接。

本例中享元对象接口的实现如下,它用于创建一个报表。即,所有的报表生成类将作为享元对象在一个公司(租户)中共享。

  1. public interface IReportManager { 
  2.     public String createReport(); 

以下是两个报表生成的实例,分别对应员工财务收入报表和员工个人信息报表。它们都是具体的享元类。

  1. public class FinancialReportManager implements IReportManager {//财务报表 
  2.     protected String tenantId=null; 
  3.     public FinancialReportManager(String tenantId){          //租户ID 
  4.         this.tenantId=tenantId; 
  5.     } 
  6.     @Override 
  7.     public String createReport() { 
  8.         return "This is a financial report"; 
  9.     } 
  10.  
  11. public class EmployeeReportManager implements IReportManager { //员工报表 
  12.     protected String tenantId=null; 
  13.     public EmployeeReportManager(String tenantId){               //租户ID 
  14.         this.tenantId=tenantId; 
  15.     } 
  16.     @Override 
  17.     public String createReport() { 
  18.         return "This is a employee report"; 
  19.     } 

最为核心的享元工厂类实现如下,它也是享元模式的精髓所在。它确保同一个公司(租户)使用相同的对象产生报表。这是相当有意义的,否则系统可能会为每个员工生成各自的报表对象,导致系统开销激增。

  1. public class ReportManagerFactory { 
  2.      
  3.     Map<String ,IReportManager> financialReportManager= 
  4. new HashMap<String ,IReportManager>(); 
  5.     Map<String ,IReportManager> employeeReportManager= 
  6. new HashMap<String ,IReportManager>(); 
  7.      
  8.     IReportManager getFinancialReportManager(String tenantId){ 
  9.                                                             //通过租户ID 
  10.                                                             //获取享元 
  11.         IReportManager r=financialReportManager.get(tenantId); 
  12.         if(r==null){ 
  13.             r=new FinancialReportManager(tenantId); 
  14.             financialReportManager.put(tenantId, r);        //维护已创建的 
  15.                                                             //享元对象 
  16.         } 
  17.         return r; 
  18.     } 
  19.      
  20.     IReportManager getEmployeeReportReportManager(String tenantId){ 
  21.                                                             //通过租户ID 
  22.                                                             //获取享元 
  23.         IReportManager r=employeeReportManager.get(tenantId); 
  24.         if(r==null){ 
  25.             r=new EmployeeReportManager(tenantId); 
  26.             employeeReportManager.put(tenantId, r);         //维护已创建的 
  27.                                                             //享元对象 
  28.         } 
  29.         return r; 
  30.     } 

使用享元模式的方法如下:

  1. public static void main(String[] args) { 
  2.     ReportManagerFactory rmf=new ReportManagerFactory(); 
  3.     IReportManager rm=rmf.getFinancialReportManager("A"); 
  4.     System.out.println(rm.createReport()); 

ReportManagerFactory作为享元工厂,以租客的ID为索引,维护了一个享元对象的集合,它确保相同租客的请求都返回同一个享元实例,确保享元对象的有效复用。

2.1.4  装饰者模式(1)

装饰者模式拥有一个设计非常巧妙的结构,它可以动态添加对象功能。在基本的设计原则中,有一条重要的设计准则叫做合成/聚合复用原则。根据该原则的思想,代码复用应该尽可能使用委托,而不是使用继承。因为继承是一种紧密耦合,任何父类的改动都会影响其子类,不利于系统维护。而委托则是松散耦合,只要接口不变,委托类的改动并不会影响其上层对象。

装饰者模式就充分运用了这种思想,通过委托机制,复用系统中的各个组件,在运行时,可以将这些功能组件进行叠加,从而构造一个"超级对象",使其拥有所有这些组件的功能。而各个子功能模块,被很好地维护在各个组件的相关类中,拥有整洁的系统结构。

在本小节中提到装饰者模式,是因为这种结构可以很好地将功能组件和性能组件进行分离,彼此互不影响,并在需要的时候,有机地结合起来。为了更好地理解装饰者模式如何做到性能模块的分离,首先,需要对装饰者模式做一个总体的了解。

注意:装饰者模式可以有效分离性能组件和功能组件,从而提升模块的可维护性并增加模块的复用性。

装饰者模式的基本结构如图2.7所示。

装饰者(Decorator)和被装饰者(ConcreteComponent)拥有相同的接口Component。被装饰者通常是系统的核心组件,完成特定的功能目标。而装饰者则可以在被装饰者的方法前后,加上特定的前置处理和后置处理,增强被装饰者的功能。

装饰者模式的主要角色如表2.4所示。

表2.4  装饰者模式角色

image

装饰者模式的一个典型案例就是对输出结果进行增强。比如,现在需要将某一结果通过HTML进行发布,那么首先就需要将内容转化为一个HTML文本。同时,由于内容需要在网络上通过HTTP流传,故,还需要为其增加HTTP头。当然,作为一个更复杂的情况,可能还要为其安置TCP头等。但作为一个示例,这里做简化处理。

2.1.4  装饰者模式(2)

装饰者模式的核心思想在于:无需将所有的逻辑,即,核心内容构建、HTML文本构造和HTTP头生成等3个功能模块粘合在一起实现。通过装饰者模式,可以将它们分解为3个几乎完全独立的组件,并在使用时灵活地进行装配。为实现这个功能,可以使用如图2.8所示的结构。

IPacketCreator即装饰接口,用于处理具体的内容。PacketBodyCreator是具体的组件,它的功能是构造要发布信息的核心内容,但是它不负责将其构造成一个格式工整、可直接发布的数据格式。PacketHTTPHeaderCreator负责对给定的内容加上HTTP头部,PacketHTMLHeaderCreator负责将给定的内容格式化成HTML文本。如图2.8所示,3个功能模块相对独立且分离,易于系统维护。

IPacketCreator的实现很简单,它是一个单方法的接口:

  1. public interface IPacketCreator { 
  2.     public String handleContent();          //用于内容处理 

PacketBodyCreator用于返回数据包的核心数据:

  1. public class PacketBodyCreator implements IPacketCreator{ 
  2.     @Override 
  3.     public String handleContent() { 
  4.         return "Content of Packet";     //构造核心数据,但不包括格式 
  5.     } 

PacketDecorator维护核心组件component对象,它负责告知其子类,其核心业务逻辑应该全权委托component完成,自己仅仅是做增强处理。

  1. public abstract class PacketDecorator implements IPacketCreator{ 
  2.     IPacketCreator component; 
  3.     public PacketDecorator(IPacketCreator c){ 
  4.         ccomponent=c; 
  5.     } 

PacketHTMLHeaderCreator是具体的装饰器,它负责对核心发布的内容进行HTML格式化操作。需要特别注意的是,它委托了具体组件component进行核心业务处理。

  1. public class PacketHTMLHeaderCreator extends PacketDecorator{ 
  2.  
  3.     public PacketHTMLHeaderCreator(IPacketCreator c) { 
  4.         super(c); 
  5.     } 
  6.  
  7.     @Override 
  8.     public String handleContent() {         //将给定数据封装成HTML 
  9.         StringBuffer sb=new StringBuffer(); 
  10.         sb.append("<html>"); 
  11.         sb.append("<body>"); 
  12.         sb.append(component.handleContent()); 
  13.         sb.append("</body>"); 
  14.         sb.append("</html>\n"); 
  15.         return sb.toString(); 
  16.     } 

2.1.4  装饰者模式(3)

PacketHTTPHeaderCreator与PacketHTMLHeaderCreator类似,但是它完成数据包HTTP头部的处理。其余业务处理依然交由内部的component完成。

  1. public class PacketHTTPHeaderCreator extends PacketDecorator{ 
  2.     public PacketHTTPHeaderCreator(IPacketCreator c) { 
  3.         super(c); 
  4.     } 
  5.  
  6.     @Override 
  7.     public String handleContent() {         //对给定数据加上HTTP头信息 
  8.         StringBuffer sb=new StringBuffer(); 
  9.         sb.append("Cache-Control:no-cache\n"); 
  10.         sb.append("Date:Mon,31Dec201204:25:57GMT\n"); 
  11.         sb.append(component.handleContent()); 
  12.         return sb.toString(); 
  13.     } 

对于装饰者模式,另一个值得关注的地方是它的使用方法。在本例中,通过层层构造和组装这些装饰者和被装饰者到一个对象中,使其有机地结合在一起工作。

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.         IPacketCreator pc=new PacketHTTPHeaderCreator( 
  4.                             new PacketHTMLHeaderCreator( 
  5.                                 new PacketBodyCreator())); 
  6.         System.out.println(pc.handleContent()); 
  7.     } 

可以看到,通过装饰者的构造函数,将被装饰对象传入。本例中,共生成3个对象实例,作为核心组件的PacketBodyCreator最先被构造,其次是PacketHTMLHeaderCreator,最后才是PacketHTTPHeaderCreator。

这个顺序表示,首先由PacketBodyCreator对象去生成核心发布内容,接着由PacketHTMLHeaderCreator对象对这个内容进行处理,将其转化为HTML,最后由PacketHTTPHeaderCreator对PacketHTMLHeaderCreator的输出安置HTTP头部。程序运行的输出如下:

  1. Cache-Control:no-cache 
  2. Date:Mon,31Dec201204:25:57GMT 
  3. <html><body>Content of Packet</body></html>

图2.9是本例的调用堆栈,从调用堆栈中,读者应该可以更容易地理解各个组件间的相互关系。

图2.9  装饰者模式示例调用堆栈

在JDK的实现中,有不少组件也是用装饰者模式实现。其中,一个最典型的例子就是OutputStream和InputStream类族的实现。以OutputStream为例,OutputStream对象提供的方法比较简单,功能也比较弱,但通过各种装饰者的增强,OutputStream对象可以被赋予强大的功能。

图2.10显示了以OutputStream为核心的装饰者模式的实现。其中FileOutputStream为

2.1.4  装饰者模式(4)

系统的核心类,它实现了向文件写入数据。使用DataOutputStream可以在FileOutputStream的基础上,增加对多种数据类型的写操作支持,而BufferedOutputStream装饰器,可以对FileOutputStream增加缓冲功能,优化I/O的性能。以BufferedOutputStream为代表的性能组件,是将性能模块和功能模块分离的一种典型实现。

  1. public static void main(String[] args) throws IOException { 
  2.     //生成一个有缓冲功能的流对象 
  3.     DataOutputStream dout= 
  4.     new DataOutputStream(new BufferedOutputStream(new FileOutputStream 
  5.     ("C:\\a.txt"))); 
  6.     //没有缓冲功能的流对象 
  7.     //DataOutputStream dout=new DataOutputStream(new FileOutputStream 
  8.     ("C:\\a.txt")); 
  9.     long begin=System.currentTimeMillis(); 
  10.     for(int i=0;i<100000;i++) 
  11.         dout.writeLong(i); 
  12.     System.out.println("spend:"+(System.currentTimeMillis()-begin)); 

以上代码显示FileOutputStream的典型应用。加粗部分是两种建立OutputStream的方法,第一种加入了性能组件BufferedOutputStream,第二种则没有。因此,第一种方法产生的OutputStream拥有更好的I/O性能。

注意:JDK中OutputStream和InputStream类族的实现是装饰者模式的典型应用。通过嵌套的方式不断地将对象聚合起来,最终形成一个超级对象,并使之拥有所有相关子对象的功能。

下面来看一下装饰者模式如何通过性能组件增强I/O性能。在运行时,工作流程如图2.11所示。

在FileOutputStream.write()的调用之前,会首先调用BufferedOutputStream.write(),它的实现如下:

  1. public synchronized void write(byte b[], int off, int len) throws IOException { 
  2.     if (len >= buf.length) {        //如果要写入的数据数量大于缓存容量 
  3.         flushBuffer();              //写入所有缓存 
  4.         out.write(b, off, len);     //直接将数据写入文件 
  5.         return; 
  6.     } 
  7.     if (len > buf.length - count) { 
  8.         flushBuffer(); 
  9.     } 
  10.     System.arraycopy(b, off, buf, count, len); 
  11.                                     //如果写入的数据比较少,则写入缓存 
  12.     count += len; 
  13. private void flushBuffer() throws IOException { 
  14.     if (count > 0) { 
  15.         out.write(buf, 0, count);   //这里的out对象就是FileOutputStream 
  16.         count = 0; 
  17.     } 

可以看到,并不是每次BufferedOutputStream.write()调用都会去磁盘写入数据,而是将数据写入缓存中,当缓存满时,才调用FileOutputStream.write()方法,实际写入数据。以此实现性能组件与功能组件的完美分离。

2.1.5  观察者模式(1)

观察者模式是非常常用的一种设计模式。在软件系统中,当一个对象的行为依赖于另一个对象的状态时,观察者模式就相当有用。若不使用观察者模式提供的通用结构,而需要实现其类似的功能,则只能在另一个线程中不停监听对象所依赖的状态。在一个复杂系统中,可能会因此开启很多线程来实现这一功能,这将使系统的性能产生额外的负担。观察者模式的意义也就在此,它可以在单线程中,使某一对象,及时得知自身所依赖的状态的变化。观察者模式的经典结构如图2.12所示。

ISubject是被观察对象,它可以增加或者删除观察者。IOberver是观察者,它依赖于ISubject的状态变化。当ISubject状态发生改变时,会通过inform()方法通知观察者。

注意:观察者模式可以用于事件监听、通知发布等场合。可以确保观察者在不使用轮询监控的情况下,及时收到相关消息和事件。

观察者模式的主要角色如表2.5所示。

表2.5  观察者模式角色

image

主题接口的实现如下:

  1. public interface ISubject{   
  2.     void attach(IObserver observer);        //添加观察者   
  3.     void detach(IObserver observer);        //删除观察者   
  4.     void inform();                          //通知所有观察者   

观察者接口的实现如下:

  1. public interface IObserver{   
  2.     void update(Event evt);             //更新观察者   

一个具体的主题实现,注意,它维护了观察者队列,提供了增加和删除观察者的方法,并通过其inform()通知观察者。

  1. public class ConcreteSubject implements ISubject{ 
  2.     Vector<IObserver> observers=new Vector<IObserver>(); 
  3.     public void attach(IObserver observer){   
  4.         observers.addElement(observer);   
  5.     }   
  6.     public void detach(IObserver observer){   
  7.         observers.removeElement(observer);   
  8.     }   
  9.     public void inform(){ 
  10.         Event evt=new Event(); 
  11.         for(IObserver ob:observers){ 
  12.             ob.update(evt);                 //注意,在这里通知观察者 
  13.         } 
  14.     }   

2.1.5  观察者模式(2)

一个具体的观察者实现如下,当其监听的状态发生改变时,update()方法就会被主题回调,进而可以在观察者内部进行业务逻辑的处理。

  1. public class ConcreteObserver implements IObserver{   
  2.     public void update(Event evt){   
  3.         System.out.println("obserer receives information"); 
  4.     }   

观察者模式是如此常用,以致于JDK内部就已经为开发人员准备了一套观察者模式的实现。它位于java.util包中,包括java.util.Observable类和java.util.Observer接口,它们的关系如图2.13所示。

(点击查看大图)图2.13  JDK内置的观察者模式

注意:在JDK中已经实现了一套观察者模式,读者可以直接复用相关代码。

在java.util.Observable类中,已经实现了主要的功能,如增加观察者、删除观察者和通知观察者,开发人员可以直接通过继承Observable使用这些功能。java.util.Observer接口是观察者接口,它的update()方法会在java.util.Observable的notifyObservers()方法中被回调,以获得最新的状态变化。通常在观察者模式中Observer接口总是应用程序的核心扩展对象,具体的业务逻辑总是会被封装在update()方法中。

在JDK中,观察者模式也得到了普遍的应用。一个最典型的应用便是Swing框架的JButton实现,它的事件处理机制如图2.14所示。

JButton继承自AbstractButton,在AbstractButton中维护了一组监听器,它们就扮演着被观察者的角色。而AbstractButton本身就是被观察对象。监听器ActionListener并不是依靠循环监听去获取按钮何时被单击,而是当按钮被单击时,通过AbstractButton的fireActionPermed()方法回调ActionListener.actionPerformed()方法实现。基于这种结构,在应用程序开发时,只需要简单地实现ActionListener接口(也就是Observer),并将其添加到按钮(Subject角色)的观察者列表中,那么当单击事件发生,就可以自动促发监听器的业务处理函数。下面从观察者模式的角度,分析一段按钮单击处理的代码:

  1. public static class BtnListener implements ActionListener{   
  2.                                         //这就是具体的观察者 
  3.     @Override 
  4.     public void actionPerformed(ActionEvent e) { 
  5.                                         //在fireActionPerformed()中被回调 
  6.         System.out.println("click");    //按钮单击时,由具体观察者处理业务 
  7.     } 
  8. public static void main(String args[]){ 
  9.     JFrame   p=new JFrame  (); 
  10.     JButton btn=new JButton("Click ME");        //新建具体主题 
  11.     btn.addActionListener(new BtnListener());   //在具体主题中,加入观察者 
  12.     p.add(btn); 
  13.     p.pack(); 
  14.     p.setVisible(true); 

2.1.5  观察者模式(3)

当按钮被单击时,通过被观察对象通知观察者,以下是AbstractButton中的一段事件处理代码,显示了被观察对象如何通知观察者:

  1. protected void fireActionPerformed(ActionEvent event) { 
  2.     Object[] listeners = listenerList.getListenerList();//这里就是应用层 
  3.                                                     //实现的ActionListener 
  4.     ActionEvent e = null; 
  5.     for (int i = listeners.length-2; i>=0; i-=2) { 
  6.         if (listeners[i]==ActionListener.class) { 
  7.             if (e == null) { 
  8.                   String actionCommand = event.getActionCommand(); 
  9.                   if(actionCommand == null) { 
  10.                      actionCommand = getActionCommand(); 
  11.                   } 
  12.                   e = new ActionEvent(AbstractButton.this, 
  13.                                       ActionEvent.ACTION_PERFORMED, 
  14.                                       actionCommand, 
  15.                                       event.getWhen(), 
  16.                                       event.getModifiers());    //构造事件参数, 
  17.                                  //告诉应用层是何种事件发生 
  18.             } 
  19.             ((ActionListener)listeners[i+1]).actionPerformed(e); 
  20.                                                         //回调应用层的实现 
  21.         } 
  22.     } 

2.1.6  Value Object模式(1)

在J2EE软件开发中,通常会对系统模块进行分层。展示层主要负责数据的展示,定义数据库的UI组织模式;业务逻辑层负责具体的业务逻辑处理;持久层通常指数据库以及相关操作。在一个大型系统中,这些层次很有可能被分离,并部署在不同的服务器上。而在两个层次之间,可能通过远程过程调用RMI等方式进行通信。如图2.15所示,展示层组件作为RMI的客户端,通过中间的业务逻辑层取得一个订单(Order)的信息。假设一个订单由客户名、商品名和数量构成,那么一次交互过程可能由图2.15所描述的这样,RMI的客户端会与服务端进行3次交互,依次取得这些信息。

图2.15  展示层与业务逻辑层交互示例1

基于以上模式的通信方式是一种可行的解决方案,但是它存在两个严重的问题:

(1)对于获取一个订单对象而言,这个操作模式略显繁琐,且不具备较好的可维护性。

(2)前后累计进行了3次客户端与服务器的通信,性能成本较高。

为了解决这两个问题,就可以使用Value Object模式。Value Object模式提倡将一个对象的各个属性进行封装,将封装后的对象在网络中传递,从而使系统拥有更好的交互模型,并且减少网络通信数据,从而提高系统性能。使用Value Object模式对以上结构进行改良,定义对象Order,由Order对象维护客户名、商品名和数量等信息,而Order对象也就是Value Object,它必须是一个可串行化的对象。将Value Object模式应用到本例中,便可以得到如图2.16所示的结构。

图2.16  Value Object模式架构图

在基于Value Object模式的结构中,为了获得一份订单信息,只需要进行一次网络通信,缩短了数据存取的响应时间,减少了网络数据流量。

注意:使用Value Object模式可以有效减少网络交互次数,提高远程调用方法的性能,也能使系统接口具有更好的可维护性。

RMI服务端的接口实现如下,其中getOrder()方法取得一个Value Object,其他方法均取得Order对象的一部分信息:

  1. public interface IOrderManager  extends Remote { 
  2.      public Order getOrder(int id) throws RemoteException; 
  3.                                                 //Value Object模式 
  4.      public String getClientName(int id) throws RemoteException; 
  5.      public String getProdName(int id) throws RemoteException; 
  6.      public int getNumber(int id) throws RemoteException; 

一个最简单的IOrderManager的实现,它什么也没做,只是返回数据:

  1. public class OrderManager extends UnicastRemoteObject implements IOrderManager { 
  2.     protected OrderManager() throws RemoteException { 
  3.         super(); 
  4.     } 
  5.     private static final long serialVersionUID = -1717013007581295639L; 
  6.     @Override 
  7.     public Order getOrder(int id) throws RemoteException {  //返回订单信息 
  8.         Order o=new Order(); 
  9.         o.setClientName("billy"); 
  10.         o.setNumber(20); 
  11.         o.setProdunctName("desk"); 
  12.         return o; 
  13.     } 
  14.     @Override 
  15.     public String getClientName(int id) throws RemoteException {     
  16.                                                         //返回订单的客户名 
  17.         return "billy"; 
  18.     } 
  19.     @Override 
  20.     public String getProdName(int id) throws RemoteException {   
  21.                                                         //返回商品名称 
  22.         return "desk"; 
  23.     } 
  24.     @Override 
  25.     public int getNumber(int id) throws RemoteException {   //返回数量 
  26.         return 20; 
  27.     } 

2.1.6  Value Object模式(2)

作为Value Object的Order对象实现如下:

  1. public class Order implements java.io.Serializable{ 
  2.     private int orderid; 
  3.     private String clientName; 
  4.     private int number; 
  5.     private String produnctName; 
  6.     //省略setter getter方法 

业务逻辑层注册并开启RMI服务器:

  1. public class OrderManagerServer { 
  2.       public static void main(String[] argv)    
  3.        {    
  4.           try   
  5.           {    
  6.              LocateRegistry.createRegistry(1099);           //注册RMI端口 
  7.              IOrderManager usermananger = new OrderManager();//RMI远程对象 
  8.              Naming.rebind("OrderManager", usermananger);   //绑定RMI对象 
  9.              System.out.println("OrderManager is ready.");    
  10.           }    
  11.           catch (Exception e)    
  12.           {    
  13.              System.out.println("OrderManager Server failed: " + e);    
  14.           }    
  15.        }    

客户端的测试代码如下,它分别展示了使用Value Object模式封装数据和不使用Value Object模式的性能差异:

  1. public static void main(String[] argv) { 
  2.     try { 
  3.         IOrderManager usermanager = (IOrderManager) Naming 
  4.                 .lookup("OrderManager"); 
  5.         long begin = System.currentTimeMillis(); 
  6.         for (int i = 0; i < 1000; i++) { 
  7.             usermanager.getOrder(i);                //Value Object模式 
  8.         } 
  9.         System.out.println("getOrder spend:" 
  10.                 + (System.currentTimeMillis() - begin)); 
  11.  
  12.         begin = System.currentTimeMillis(); 
  13.         for (int i = 0; i < 1000; i++) { 
  14.             usermanager.getClientName(i);           //通过多次交互获取数据 
  15.             usermanager.getNumber(i); 
  16.             usermanager.getProdName(i); 
  17.         } 
  18.         System.out.println("3 Method call spend:" 
  19.                 + (System.currentTimeMillis() - begin)); 
  20.         System.out.println(usermanager.getOrder(0).getClientName()); 
  21.     } catch (Exception e) { 
  22.         System.out.println("OrderManager exception: " + e); 
  23.     } 

结果显示,使用getOrder()方法相对耗时469ms,而使用连续3次离散的远程调用耗时766ms。由此可见,对传输数据进行有效的封装,可以明显提升远程方法调用的性能。

2.1.7  业务代理模式(1)

Value Object模式是将远程调用的传递数据封装在一个串行化的对象中进行传输,而业务代理模式则是将一组由远程方法调用构成的业务流程,封装在一个位于展示层的代理类中。比如,如果用户需要修改一个订单,订单修改操作可细分为3个子操作:

校验用户;

获取旧的订单信息;

更新订单。

系统结构如图2.17所示。

图2.17  展示层与业务逻辑层交互示例2

以上结构存在两个问题:

(1)当展示层存在大量并发线程时,这些线程都会直接进行远程方法调用,进而会加重网络负担;

(2)由于缺乏对订单修改操作流程的有效封装,如果将来修改流程发生变化,那么展示层组件需要修改。

为了有效地解决以上两个问题,可以在展示层中加入业务代理对象,业务代理对象负责和远程服务器通信,完成订单修改操作。而业务代理对象本身只暴露简单的updateOrder()订单修改操作供展示层组件使用,修改后的结构如图2.18所示。

图2.18  业务代理模式架构图

注意:业务代理模式将一些业务流程封装在前台系统,为系统性能优化提供了基础平台。在业务代理中,不仅可以复用业务流程,还可以视情况为展示层组件提供缓存等功能,从而减少远程方法调用次数,降低系统压力。

这种结构体现了业务代理模式的核心思想。由于该业务代理对象被所有的展示层请求线程和多个客户端共享,故系统将会有较好的可维护性。如果业务流程发生变化,只需要简单地修改业务代理对象暴露的updateOrder()方法即可。除此之外,通过业务代理对象,可以更容易地在多个线程或者客户端请求之间共享数据,从而有效地利用缓存,减少远程调用次数,提高系统性能。

一个未使用业务代理模式的展示层实现可能如以下代码所示:

  1. public static void main(String[] argv) { 
  2.     try { 
  3.         IOrderManager usermanager = (IOrderManager) Naming 
  4.                 .lookup("OrderManager"); 
  5.         if (usermanager.checkUser(1)) {     //所有的远程调用都会被执行 
  6.                                                         //当并发量较大时,严重影响性能 
  7.             Order o = usermanager.getOrder(1); 
  8.             o.setNumber(10); 
  9.             usermanager.updateOrder(o); 
  10.         } 
  11.     } catch (Exception e) { 
  12.         System.out.println("OrderManager exception: " + e); 
  13.     } 

2.1.7  业务代理模式(2)

而使用了业务代理后,展示层组件可以优化成:

  1. public static void main(String[] argv) throws Exception { 
  2.     BusinessDelegate bd=new BusinessDelegate(); 
  3.     Order o=bd.getOrder(11);     
  4.     o.setNumber(11); 
  5.     bd.updateOrder(o);                          //使用业务代理完成更新订单 

在业务代理对象BusinessDelegate中,可以增加缓存,从而直接减少远程方法调用的次数。以下是一段不完整的实例代码,但足以说明问题:

  1. public class BusinessDelegate { 
  2.     IOrderManager usermanager =null;            //封装远程方法调用的流程 
  3.      
  4.     public BusinessDelegate(){ 
  5.         try { 
  6.             usermanager = (IOrderManager) Naming.lookup("OrderManager"); 
  7.         } catch (MalformedURLException e) { 
  8.             e.printStackTrace(); 
  9.         } catch (RemoteException e) { 
  10.             e.printStackTrace(); 
  11.         } catch (NotBoundException e) { 
  12.             e.printStackTrace(); 
  13.         } 
  14.     } 
  15.      
  16.     public boolean checkUserFromCache(int uid){ 
  17.         return true; 
  18.     } 
  19.     public boolean checkUser(int uid) throws RemoteException{ 
  20.                                                 //当前对象被多个客户端共享, 
  21.                                                 //可以在本地缓存中校验用户 
  22.         if(!checkUserFromCache(uid)){ 
  23.             return usermanager.checkUser(1); 
  24.         } 
  25.         return true; 
  26.     } 
  27.      
  28.     public Order getOrderFromCache(int oid){ 
  29.         return null; 
  30.     } 
  31.     public Order getOrder(int oid) throws RemoteException{   
  32.                                                 //可以在本地缓存中获取订单 
  33.                                                 //减少远程方法调用次数 
  34.         Order order=getOrderFromCache(oid); 
  35.         if(order==null){ 
  36.             return usermanager.getOrder(oid); 
  37.         } 
  38.         return order; 
  39.     } 
  40.  
  41.      
  42.     public boolean updateOrder(Order order) throws Exception{ 
  43.                                                 //暴露给展示层的方法 
  44.                                                 //封装了业务流程 
  45.          if(checkUser(1)){                      //可能在缓存中执行 
  46.              Order o=getOrder(1); 
  47.              o.setNumber(10); 
  48.              usermanager.updateOrder(o); 
  49.          } 
  50.          return true; 
  51.     } 
原创粉丝点击