java-技术-Java类中热替换的概念设计与实现

Posted 07H_JH

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java-技术-Java类中热替换的概念设计与实现相关的知识,希望对你有一定的参考价值。

类的热替换是Java在线升级系统设计中的基础技术,从文中给出的实例来看,构建在线升级系统不仅仅是一个技术问题,还牵扯到很多管理方面的因素,比如:如何管理、部署系统中的可在线升级部分和不可在线升级部分以降低系统的管理、维护成本等。

对于许多关键性业务或者庞大的Java系统来说,如果必须暂停系统服务才能进行系统升级,既会大大影响到系统的可用性,同时也增加了系统的管理和维护成本。因此,如果能够方便地在不停止系统业务的情况下进行系统升级,则可以很好地解决上述问题。

JavaClassLoader技术剖析

要构建在线升级系统,一个重要的技术就是能够实现Java类的热替换——也就是在不停止正在运行的系统的情况下进行类(对象)的升级替换。而Java的ClassLoader正是实现这项技术的基础。

在Java中,类的实例化流程分为两个部分:类的加载和类的实例化。类的加载又分为显式加载和隐式加载。大家使用new关键字创建类实例时,其实就隐式地包含了类的加载过程。对于类的显式加载来说,比较常用的是Class.forName。其实,它们都是通过调用ClassLoader类的loadClass方法来完成类的实际加载工作的。直接调用ClassLoader的loadClass方法是另外一种不常用的显式加载类的技术。

 

图1.Java类加载器层次结构图

ClassLoader在加载类时有一定的层次关系和规则。在Java中,有四种类型的类加载器,分别为:BootStrapClassLoader、ExtClassLoader、AppClassLoader以及用户自定义的ClassLoader。这四种类加载器分别负责不同路径的类的加载,并形成了一个类加载的层次结构。

BootStrapClassLoader处于类加载器层次结构的最高层,负责sun.boot.class.path路径下类的加载,默认为jre/lib目录下的核心API或-Xbootclasspath选项指定的jar包。ExtClassLoader的加载路径为java.ext.dirs,默认为jre/lib/ext目录或者-Djava.ext.dirs指定目录下的jar包加载。AppClassLoader的加载路径为java.class.path,默认为环境变量CLASSPATH中设定的值。也可以通过-classpath选型进行指定。用户自定义ClassLoader可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。

这四种类加载器的层次关系图如图1所示。一般来说,这四种类加载器会形成一种父子关系,高层为低层的父加载器。在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,如果已经加载则直接返回该类的引用。如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常。Java类的加载过程如图2所示。

 

图2.Java类的加过程

每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。

我们编写的应用类默认情况下都是通过AppClassLoader进行加载的。当我们使用new关键字或者Class.forName来加载类时,所要加载的类都是由调用new或者Class.forName的类的类加载器(也是AppClassLoader)进行加载的。要想实现Java类的热替换,首先必须要实现系统中同名类的不同版本实例的共存,通过上面的介绍我们知道,要想实现同一个类的不同版本的共存,我们必须要通过不同的类加载器来加载该类的不同版本。另外,为了能够绕过Java类的既定加载过程,我们需要实现自己的类加载器,并在其中对类的加载过程进行完全的控制和管理。

编写自定义的ClassLoader

为了能够完全掌控类的加载过程,我们的定制类加载器需要直接从ClassLoader继承。首先我们来介绍一下ClassLoader类中和热替换有关的的一些重要方法。

◆findLoadedClass:每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间中,该方法就是在该名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回null。这里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。

◆getSystemClassLoader:Java2中新增的方法。该方法返回系统使用的ClassLoader。可以在自己定制的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。

◆defineClass:该方法是ClassLoader中非常重要的一个方法,它接收以字节数组表示的类字节码,并把它转换成Class实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。

◆loadClass:加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重新实现,我们可以完全控制和管理类的加载过程。

◆resolveClass:链接一个指定的类。这是一个在某些情况下确保类可用的必要方法,详见Java语言规范中“执行”一章对该方法的描述。

了解了上面的这些方法,下面我们来实现一个定制的类加载器来完成这样的加载流程:我们为该类加载器指定一些必须由该类加载器直接加载的类集合,在该类加载器进行类的加载时,如果要加载的类属于必须由该类加载器加载的集合,那么就由它直接来完成类的加载,否则就把类加载的工作委托给系统的类加载器完成。

在给出示例代码前,有两点内容需要说明一下:1、要想实现同一个类的不同版本的共存,那么这些不同版本必须由不同的类加载器进行加载,因此就不能把这些类的加载工作委托给系统加载器来完成,因为它们只有一份。2、为了做到这一点,就不能采用系统默认的类加载器委托规则,也就是说我们定制的类加载器的父加载器必须设置为null。该定制的类加载器的实现代码如下:

 
  1. class CustomCL extends ClassLoader    
  2.  
  3.  private String basedir; // 需要该类加载器直接加载的类文件的基目录  
  4.     private HashSet dynaclazns; // 需要由该类加载器直接加载的类名  
  5.  
  6.     public CustomCL(String basedir, String[] clazns)    
  7.         super(null); // 指定父类加载器为 null   
  8.         this.basedir = basedir;   
  9.         dynaclazns = new HashSet();   
  10.         loadClassByMe(clazns);   
  11.        
  12.  
  13.     private void loadClassByMe(String[] clazns)    
  14.         for (int i = 0; i < clazns.length; i++)    
  15.             loadDirectly(clazns[i]);   
  16.             dynaclazns.add(clazns[i]);   
  17.            
  18.        
  19.  
  20.     private Class loadDirectly(String name)    
  21.         Class cls = null;   
  22.         StringBuffer sb = new StringBuffer(basedir);   
  23.         String classname = name.replace('.', File.separatorChar) + ".class";  
  24.         sb.append(File.separator + classname);   
  25.         File classF = new File(sb.toString());   
  26.         cls = instantiateClass(name,new FileInputStream(classF),  
  27.             classF.length());   
  28.         return cls;   
  29.            
  30.  
  31.     private Class instantiateClass(String name,InputStream fin,long len)   
  32.         byte[] raw = new byte[(int) len];   
  33.         fin.read(raw);   
  34.         fin.close();   
  35.         return defineClass(name,raw,0,raw.length);   
  36.        
  37.       
  38.  protected Class loadClass(String name, boolean resolve)   
  39.             throws ClassNotFoundException    
  40.         Class cls = null;   
  41.         cls = findLoadedClass(name);   
  42.         if(!this.dynaclazns.contains(name) && cls == null)   
  43.             cls = getSystemClassLoader().loadClass(name);   
  44.         if (cls == null)   
  45.             throw new ClassNotFoundException(name);   
  46.         if (resolve)   
  47.             resolveClass(cls);   
  48.         return cls;   
  49.        
  50.  
  51.   

在该类加载器的实现中,所有指定必须由它直接加载的类都在该加载器实例化时进行了加载,当通过loadClass进行类的加载时,如果该类没有加载过,并且不属于必须由该类加载器加载之列都委托给系统加载器进行加载。

实现Java类的热替换

现在来介绍一下我们的实验方法,为了简单起见,我们的包为默认包,没有层次,并且省去了所有错误处理。要替换的类为Foo,实现很简单,仅包含一个方法sayHello:

 
  1. public class Foo   
  2.     public void sayHello()    
  3.         System.out.println("hello world! (version one)");   
  4.        
  5.   

在当前工作目录下建立一个新的目录swap,把编译好的Foo.class文件放在该目录中。接下来要使用我们前面编写的HotswapCL来实现该类的热替换。具体的做法为:我们编写一个定时器任务,每隔2秒钟执行一次。其中,我们会创建新的类加载器实例加载Foo类,生成实例,并调用sayHello方法。接下来,我们会修改Foo类中sayHello方法的打印内容,重新编译,并在系统运行的情况下替换掉原来的Foo.class,我们会看到系统会打印出更改后的内容。定时任务的实现如下(其它代码省略,请读者自行补齐):

 
  1. public void run()   
  2.     try    
  3.         // 每次都创建出一个新的类加载器  
  4.         HowswapCL cl = new HowswapCL("../swap", new String[]"Foo");   
  5.         Class clcls = cl.loadClass("Foo");   
  6.         Object foo = cls.newInstance();   
  7.  
  8.         Method m = foo.getClass().getMethod("sayHello", new Class[]);   
  9.         m.invoke(foo, new Object[]);   
  10.       
  11.       catch(Exception ex)    
  12.         ex.printStackTrace();   
  13.        
  14.   

编译、运行我们的系统,会出现如下的打印:

 

图3.热替换前的运行结果

好,现在我们把Foo类的sayHello方法更改为:

 
  1. public void sayHello()    
  2.     System.out.println("hello world! (version two)");   
  3.   

在系统仍在运行的情况下,编译,并替换掉swap目录下原来的Foo.class文件,我们再看看屏幕的打印,奇妙的事情发生了,新更改的类在线即时生效了,我们已经实现了Foo类的热替换。屏幕打印如下:

 

图4.热替换后的运行结果

敏锐的读者可能会问,为何不用把foo转型为Foo,直接调用其sayHello方法呢?这样不是更清晰明了吗?下面我们来解释一下原因,并给出一种更好的方法。如果我们采用转型的方法,代码会变成这样:Foofoo=(Foo)cls.newInstance();读者如果跟随本文进行试验的话,会发现这句话会抛出ClassCastException异常,为什么吗?因为在Java中,即使是同一个类文件,如果是由不同的类加载器实例加载的,那么它们的类型是不相同的。在上面的例子中cls是由HowswapCL加载的,而foo变量类型声名和转型里的Foo类却是由run方法所属的类的加载器(默认为AppClassLoader)加载的,因此是完全不同的类型,所以会抛出转型异常。

那么通过接口调用是不是就行了呢?我们可以定义一个IFoo接口,其中声名sayHello方法,Foo实现该接口。也就是这样:IFoofoo=(IFoo)cls.newInstance();本来该方法也会有同样的问题的,因为外部声名和转型部分的IFoo是由run方法所属的类加载器加载的,而Foo类定义中implementsIFoo中的IFoo是由HotswapCL加载的,因此属于不同的类型转型还是会抛出异常的,但是由于我们在实例化HotswapCL时是这样的:

 
  1. HowswapCLcl=newHowswapCL("../swap",newString[]"Foo"); 

其中仅仅指定Foo类由HotswapCL加载,而其实现的IFoo接口文件会委托给系统类加载器加载,因此转型成功,采用接口调用的代码如下:

 
  1. public void run()   
  2.     try    
  3.         HowswapCL cl = new HowswapCL("../swap", new String[]"Foo");   
  4.         Class clcls = cl.loadClass("Foo");   
  5.         IFoo foo = (IFoo)cls.newInstance();   
  6.         foo.sayHello();   
  7.      catch(Exception ex)    
  8.         ex.printStackTrace();   
  9.        
  10.   

确实,简洁明了了很多,在我们的实验中,每当定时器调度到run方法时,我们都会创建一个新的HotswapCL实例,在产品代码中,无需如此,仅当需要升级替换时才去创建一个新的类加载器实例。

在线升级系统的设计原则

在上小节中,我们给出了一个Java类热替换的实例,掌握了这项技术,就具备了实现在线升级系统的基础。但是,对于一个真正的产品系统来说,升级本省就是一项非常复杂的工程,如果要在线升级,就会更加复杂。其中,实现类的热替换只是最后一步操作,在线升级的要求会对系统的整体设计带来深远的影响。下面我们来谈谈在线升级系统设计方面的一些原则:

◆在系统设计一开始,就要考虑系统的哪些部分是需要以后在线升级的,哪些部分是稳定的

虽然我们可以把系统设计成任何一部分都是可以在线升级的,但是其成本是非常高昂的,也没有必要。因此,明确地界定出系统以后需要在线升级的部分是明智之举。这些部分常常是系统业务逻辑规则、算法等等。

◆设计出规范一致的系统状态转换方法

替换一个类仅仅是在线升级系统所要做的工作中的一个步骤,为了使系统能够在升级后正常运行,就必须保持升级前后系统状态的一致性。因此,在设计时要考虑需要在线升级的部分所涉及的系统状态有哪些,把这些状态设计成便于获取、设置和转换的,并用一致的方式来进行。

Java 设计模式—策略模式

java在抽象类中注入属性架构设计

java多态怎么解释?

策略模式—Java实现(转)

JAVA设计模式之策略模式

Java设计模式--策略模式