9.4 类加载器的隔离作用

类加载器的一个重要特性是为它所加载的Java类创建了隔离空间,相当于添加一个新的名称空间。要理解这一点,先来说明Java虚拟机如何判断两个Java类是否相等,即两个Class类的对象是否相等。如果两个对象的类不同,并且两个类之间不存在父类型与子类型的关系,那么在它们之间进行赋值等操作会抛出java.lang.ClassCastException异常。Java虚拟机需要根据两个条件进行判断:一个是Class类的对象表示的Java类的全名是否相同,另一个是Class类的对象的定义类加载器对象是否相同。这两个条件缺一不可。第一个条件很容易理解,如果类的名称不相同,那么它们不可能表示同一个类。第二个条件则比较难理解。在进行Class类的对象的相等性判断中,它们的定义类加载器是非常重要的。相同的字节代码如果由不同的类加载器对象来加载并定义,所得到的Class类的对象是不相等的。

下面通过一个具体的示例来说明类加载器对Class类的对象的相等性的影响。代码清单9-8给出了一个简单的Java类Sample。Sample类中包含了一个setSample方法。在这个方法中,把作为参数传入的Object类的对象强制类型转换成Sample类的对象。如果类型转换时出现ClassCastException异常,就说明参数对象的Java类与当前的Sample类不相等。

代码清单9-8 用来说明Class类的对象相等性判断方式的示例Java类


public class Sample{

private Sample obj;

public void setSample(Object obj){

this.obj=obj;

}

}


对Sample类进行编译之后,将得到的class文件放在某个目录中,用代码清单9-5中的FileSystemClassLoader类的对象进行加载,如代码清单9-9所示。测试方式是使用两个不同的FileSystemClassLoader类的对象来分别加载Sample类的字节代码,并定义出对应的Java类。然后从Java类中创建出新的对象,用一个对象作为参数调用另外一个对象上的setSample方法。实际运行的结果是抛出ClassCastException异常。虽然是从同样的字节代码中创建出来的同名Java类,但是由于定义它们的类加载器对象不同,这两个Class类的对象仍然是不相等的。

代码清单9-9 Class类的对象的相等性测试


public class ClassIdentity{

public void test()throws Exception{

Path path=Paths.get("classData");

FileSystemClassLoader fscl1=new FileSystemClassLoader(path);

FileSystemClassLoader fscl2=new FileSystemClassLoader(path);

String className="com.java7book.chapter9.Sample";

Class<?>class1=fscl1.loadClass(className);

Object obj1=class1.newInstance();

Class<?>class2=fscl2.loadClass(className);

Object obj2=class2.newInstance();

Method setSampleMethod=class1.getMethod("setSample",java.lang.Object.class);

setSampleMethod.invoke(obj1,obj2);//抛出ClassCastException异常

}

}


在很多场合都可以利用类加载器的这个特性为虚拟机中同名的Java类创建一个隔离的名称空间,使同名的Java类可以在虚拟机中共存。同名Java类需要共存的一个典型场景是程序的版本更新。一个程序可能存在多个不同的版本。用户既希望使用新版本的程序,又希望基于旧版本的代码可以继续运行。这就要求两个版本的Java类在虚拟机中同时存在。如果不使用自定义类加载器来划分名称空间,就只能让新旧版本的Java类使用不同的名称,比如在正常的类名后添加类似“V1”和“V2”等后缀进行标识。这种做法使用起来很不方便。使用自定义的类加载器就可以仍然使用相同名称的Java类,在实现时使用不同的类加载器对象来进行加载不同版本的Java类。

在一般的程序版本更新中,会保持接口不变,只修改接口的后台实现。如果接口发生变化,那么客户端代码要做出比较大的修改。这里以接口不变的版本更新为例进行说明。代码清单9-10中给出了一个简单的接口Versionized。

代码清单9-10 用来说明版本更新方式的接口示例


public interface Versionized{

String getVersion();

}


该接口的实现可能随着版本更新而发生变化。客户端代码通过一个工厂方法来获取该接口的具体实现。代码清单9-11给出了这个工厂方法的实现。在工厂方法中不是简单地创建出所需的具体实现的对象,而是创建一个新的类加载器对象。由类加载器对象先加载Java类,再创建出相应的接口实现对象。在具体的实现中,不同版本的Java类的字节代码存放在不同的路径中。使用代码清单9-5中的FileSystemClassLoader类的对象来加载所需版本的Java类。

代码清单9-11 获取接口实现对象的工厂方法


public class ServiceFactory{

public static Versionized getService(String className, String version)throws Exception{

Path path=Paths.get("service",version);

FileSystemClassLoader loader=new FileSystemClassLoader(path);

Class<?>clazz=loader.loadClass(className);

return(Versionized)clazz.newInstance();

}

}


代码清单9-12中给出了代码清单9-11中getService方法的使用方式。Versionized接口的不同版本的实现使用相同的Java类名。由于类加载器带来的隔离作用,这些同名的Java类可以共存在虚拟机中。用户可以根据所需的版本号找到对应的Java类。

代码清单9-12 不同版本的接口实现对象的使用示例


public class ServiceConsumer{

public void consume()throws Exception{

String serviceName="com.java7book.chapter9.SampleService";

Versionized v1=ServiceFactory.getService(serviceName,"v1");

Versionized v2=ServiceFactory.getService(serviceName,"v2");

}

}