在 Java 中,类的实例化流程分为两个部分:类的加载和类的实例化。类的加载又分为显式加载和隐式加载。大家使用 new 关键字创建类实例时,其实就隐式地包含了类的加载过程。对于类的显式加载来说,比较常用的是 Class.forName。其实,它们都是通过调用 ClassLoader 类的 loadClass 方法来完成类的实际加载工作的。直接调用 ClassLoader 的 loadClass 方法是另外一种不常用的显式加载类的技术。
ClassLoader 在加载类时有一定的层次关系和规则。在 Java 中,有四种类型的类加载器,分别为:BootStrapClassLoader、ExtClassLoader、AppClassLoader 以及用户自定义的 ClassLoader。这四种类加载器分别负责不同路径的类的加载,并形成了一个类加载的层次结构。
BootStrapClassLoader 处于类加载器层次结构的{zg}层,负责 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 可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载
每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。
我们编写的应用类默认情况下都是通过 AppClassLoader 进行加载的。当我们使用 new 关键字或者 Class.forName 来加载类时,所要加载的类都是由调用 new 或者 Class.forName 的类的类加载器(也是 AppClassLoader)进行加载的。要想实现 Java 类的热替换,首先必须要实现系统中同名类的不同版本实例的共存,通过上面的介绍我们知道,要想实现同一个类的不同版本的共存,我们必须要通过不同的类加载器来加载该类的不同版本。另外,为了能够绕过 Java 类的既定加载过程,我们需要实现自己的类加载器,并在其中对类的加载过程进行xx的控制和管理。
为了能够xx掌控类的加载过程,我们的定制类加载器需要直接从 ClassLoader 继承。首先我们来介绍一下 ClassLoader 类中和热替换有关的的一些重要方法。
- findLoadedClass: 每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间中,该方法就是在该名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回 null。这里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。
- getSystemClassLoader : Java2 中新增的方法。该方法返回系统使用的 ClassLoader。可以在自己定制的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。
- defineClass: 该方法是 ClassLoader 中非常重要的一个方法,它接收以字节数组表示的类字节码,并把它转换成 Class 实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。
- loadClass: 加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重新实现,我们可以xx控制和管理类的加载过程。
- resolveClass: 链接一个指定的类。这是一个在某些情况下确保类可用的必要方法,详见 Java 语言规范中“执行”一章对该方法的描述。
了解了上面的这些方法,下面我们来实现一个定制的类加载器来完成这样的加载流程:我们为该类加载器指定一些必须由该类加载器直接加载的类集合,在该 类加载器进行类的加载时,如果要加载的类属于必须由该类加载器加载的集合,那么就由它直接来完成类的加载,否则就把类加载的工作委托给系统的类加载器完 成。
- package com.test.java.classload;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileNotFoundException;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.HashSet;
- public class HotswapCL extends ClassLoader{
- private String?basedir; //?需要该类加载器直接加载的类文件的基目录
- private HashSet?dynaclazns; //?需要由该类加载器直接加载的类名
- public HotswapCL(String?basedir,?String[]?clazns) throws IOException?{
- super(null); //?指定父类加载器为?null
- this.basedir?=?basedir;
- dynaclazns?=?new HashSet();
- loadClassByMe(clazns);
- }
- private void loadClassByMe(String[]?clazns) throws IOException?{
- for (int i?= 0;?i?<?clazns.length;?i++)?{
- loadDirectly(clazns[i]);
- dynaclazns.add(clazns[i]);
- }
- }
- private Class?loadDirectly(String?name) throws IOException?{
- Class?cls?=?null;
- StringBuffer?sb?=?new StringBuffer(basedir);
- String?classname?=?name.replace(‘.’,?File.separatorChar)?+ “.class”;
- sb.append(File.separator?+?classname);
- File?classF?=?new File(sb.toString());
- cls?=?instantiateClass(name,new FileInputStream(classF),
- classF.length());
- return cls;
- }
- private Class?instantiateClass(String?name,InputStream?fin,long len) throws IOException{
- byte[]?raw?= new byte[(int)?len];
- fin.read(raw);
- fin.close();
- return defineClass(name,raw,0,raw.length);
- }
- protected Class?loadClass(String?name, boolean resolve)
- throws ClassNotFoundException?{
- Class?cls?=?null;
- cls?=?findLoadedClass(name);
- if(!this.dynaclazns.contains(name)?&&?cls?== null)
- cls?=?getSystemClassLoader().loadClass(name);
- if (cls?== null)
- throw new ClassNotFoundException(name);
- if (resolve)
- resolveClass(cls);
- return cls;
- }
- }
- public class Foo??{
- public void sayHello()?{
- //??????System.out.println(”hello?world!?(version?one)”);
- System.out.println(“hello?world!?(version?two)”);
- }
- }
在当前工作目录下建立一个新的目录 swap,把编译好的 Foo.class 文件放在该目录中。接下来要使用我们前面编写的 HotswapCL 来实现该类的热替换
- package com.test.java.classload;
- import java.lang.reflect.Method;
- public class TestMain?{
- /**
- *?@param?args
- */
- @SuppressWarnings({ “unchecked”, “static-access” })
- public static void main(String[]?args)?{
- HotswapCL?cl;
- String?userDir?=?System.getProperty(“user.dir”);
- int i?= 0;
- try {
- while (i?< 100)?{
- i++;
- cl?=?new HotswapCL(userDir?+ “\\swap”, new String[]?{ “Foo” });
- Class?cls?=?cl.loadClass(“Foo”);
- Object?foo?=?cls.newInstance();
- Method?m?=?foo.getClass().getMethod(“sayHello”, new Class[]?{});
- m.invoke(foo,?new Object[]?{});
- Thread.currentThread().sleep(1000);
- }
- }?catch (Exception?e)?{
- e.printStackTrace();
- }
- }
- }
在上小节中,我们给出了一个 Java 类热替换的实例,掌握了这项技术,就具备了实现在线升级系统的基础。但是,对于一个真正的产品系统来说,升级本省就是一项非常复杂的工程,如果要在线升 级,就会更加复杂。其中,实现类的热替换只是{zh1}一步操作,在线升级的要求会对系统的整体设计带来深远的影响。下面我们来谈谈在线升级系统设计方面的一些 原则:
- 在系统设计一开始,就要考虑系统的哪些部分是需要以后在线升级的,哪些部分是稳定的。虽然我们可以把系统设计成任何一部分都是可以在线升级的,但是其成本是非常高昂的,也没有必要。因此,明确地界定出系统以后需要在线升级的部分是明智之举。这些部分常常是系统业务逻辑规则、算法等等。
- 设计出规范一致的系统状态转换方法。替换一个类仅仅是在线升级系统所要做的工作中的一个步骤,为了使系统能够在升级后正常运行,就必须保持升级前后系统状态的一致性。因此,在设计时要考虑需要在线升级的部分所涉及的系统状态有哪些,把这些状态设计成便于获取、设置和转换的,并用一致的方式来进行。
- 明确出系统的升级控制协议。这个原则是关于系统在线升级的时机和流程控制的,不考虑系统的当前运行状态就贸然进行升级是一项非常危险的活动。因此在系统设计中, 就要考虑并预留出系统在线升级的控制点, 并定义清晰、明确的升级协议来协调、控制多个升级实体的升级次序,以确保系统在升级的任何时刻都处在一个确定的状态下。
- 考虑到升级失败时的回退机制。即使我们做了非常缜密细致的设计,还是难以从根本上保证系统升级一定是成功的,对于大型分布式系统来说尤其如此。因此在系统设计时,要考虑升级失败后的回退机制。