RMI动态类加载

以下翻译自Java RMI的Chapter 19.Dynamic Classloading(动态类加载)

部署一个分布式应用程序有可能是相当困难的。运行每个部分应用程序的计算机,其相关部分都必须安装。对于一个局域网来说,这是一个耗时的操作,但处理起来并不特别困难。然而,当应用程序部署在大规模的且频繁更新的网络环境中时,部署过程就变得相当困难了。

动态类加载是RMI中内置的用于简化这种部署的一种技术。

19.1 部署可能是困难的

让我们假设以部署banking应用程序的最新版本开始,我们需要按以下步骤进行:

1.配置服务器

2.将stub类和其他类(如套接字工厂和值对象,这些对象可能需要在命名服务中实例化)加入到命名服务的类路径中。

3.如果是与第一次相反的重部署,你可能需要重启命名服务并重新注册所有的对象以删除先前存在于命名服务JVM中的对象。

4.在每台客户机上安装和配置程序。包括跟踪当前不能使用的机器,并在随后某个时间为其安装应用程序。

与典型的applet部署过程相比,我们必须:

1.配置服务器

2.写包含APPLET tag的web页面

简单地说,部署一个web应用程序不会涉及到客户端或命名服务的改动。相反地,当web浏览器下载了一个包含applet tag的web页面时,它同时也下载了运行applet所必须的Java类文件。这种部署应用程序的方式具有非常少的耗时,且更有可能是正解的。

但,如果每次你都要更新或重部署一个应用程序,这种差别是非常具大的。

当然,Applets在很多方面都是有限制的。由于所有的字节码都是在程序运行的时候再下载的,所以它们必须是很小的,又由于浏览器可能会使用老的JVM,导致编写applet的开发者不能利用最新的Java特性(如Swing GUI工具箱).此外,由于浏览器具有严格的安全模型,applet打开套接字连接或文件的能力是严格受限的。

好消息是RMI包含一个很有意思和有用的技术,名为动态类加载,它可以合并这种模型。


它可以让你构建独立的应用程序,至少是codebase的某些部分,并拥有像applet一样的部署模型。也就是说,在需要类定义的时候,动态类加载允许RMI应用程序在运行时动态加载来自http://或ftp://URL的类的字节码定义。

19.2 类加载器

你可能已经知道Java源代码将将会编译成.class文件。每个.class文件包含了单个class的字节码(编译过的JVM指令).

你也可能已经知道每个Java类是分开编译的,作为结果的字节码将分布在不同的.class文件中,并在运行时将其动态链接。

//
动态链接

大部分传统的编程语言使用的是静态链接模型。为了生成一个可执行的应用程序,源代码代码必须被编译,所有的引用必须立即得到解析。换句话说,应用程序的个别组件必须被连接在一起。

动态链接则尽可能延迟这种处理过程。这是非常有可能的,因为在一个运行的应用程序中,有些引用实际上没有被解析的,举例来说,没有调用引用的方法。

起初这看起来有点奇怪,C++程序员可能要花很多时间来考虑这种情形。但是仔细考虑的话,为了能使Java工作,某种形式的动态链接是必须使用的。考虑典型的applet情形:

1.我使用Sun的JDK1.2版本在Linux机器上编写了一个applet。

2.你在Macintosh上下载了一个web页面,并使用Apple的JDK1.1.6在MRJ中运行applet

这种情况对于C++来说,是不可能实现的;链接被推迟到了执行期。

不幸的是,当应用程序在部分更新时,动态链接也会引发一些问题。如果你只改动了极少的.class文件,那么你可能无意地得到了不能连接在一起的两个.class文件(比如,一个老的.class文件调用了一个已从新.class文件中删除的方法).

///

19.2.1 类是怎样被加载的

JVM是通过使用类加载器来加载和验证单独的类的。类加载系统按如下步骤工作:

1.当JVM第一次启动时,一个单独的类加载器(通常指的是bootstrap classloader(启动类加载器))就会被创建。此加载器负责加载和创建大部分在JVM中要使用到的类。

2.应用程序也可以通过编程的方式来创建更多的类加载器。每个类加载器都拥有与启动类加载器一样的异常,且都有一个父加载器。这组加载器以启动类加载器作为根,并最终形成一棵树。

3.当需要一个对象时,JVM将查询适当的类加载器并请求它通过loadClass()方法来加载对象。此加载器将首先请求其父加载器来加载,如果父加载器返回null,再由它自己来加载。

隐含在其后的意思的是类加载器是一种能将一组类进行分区并使类隔离的方式。最后一点暗含的事实是:两个兄弟类加载器(它们来自同一个父亲)当被同一个类请求时,那么class对象将会被加载和初始化两次。

在两种情况中类加载器是很有用的。第一个是当你想从非标准的来源加载一个class对象时。启动类加载器试图使用CLASSPATH系统变量从文件系统来加载类,且期望类的包名和名字能够与包含那个类的字节码的.class文件中一致。如果你想从不同的来源加载类,比如,从一个URL,那么你必须安装另一个像java.net.URLClassLoader这样的类加载器.

第二种情况涉及到在同一个时间运行同一个类的不同版本。由于类加载器只检查其父加载器,不检查兄弟类加载器,为了能检查一个类是否已经被加载了,那么很有可能将同一个类的不同版本安全地加载到JVM中。

19.3 动态类加载是如何工作的

在第10节,当讨论RMI是怎样定制序列化算法时,我曾经说过:

当它输出类描述(class descriptions)时,ObjectOutputStream会调用annotateClass()方法。Annotations(注解)被用来提供关于类(来自于序列化机制而非类本身)的信息.基本的序列化机制是不需要注解的,其大部分信息都已经存储在了流...

另一方面,RMI使用注解来记录codebase信息。也就是说,除了记录类描述信息,它也记录了加载字节码的位置信息(即字节码来源信息)。

RMI的动态加载是基于以下两种理念上的:

1.当一个序列化的对象通过网络发送时,它所需的类定义在另一边可能是不可用的。

Automatically including URLs from which classes can be downloaded inside serialized objects allows special-purpose classloaders,such as the URLClassLoader class,to automatically load classes from over the wire when they're needed to deserialize an object.
2.自动包含类可以从哪里下载的URL序列化对象,允许专用类加载器(如URLClassLoader类)在它们需要反序列化一个对象时可以通过网络自动加载类。

RMI中反序列化机制按如下步骤工作:

1.实例通过网络发送。作为实例的部分序列化信息,所有相关的类定义也会通过网络发送(包括接口定义).每个类都会包含codebase注解.

2.反序列化机制为那些已描述过的类查询合适的类加载器。它能这样做的原因是RMI运行时保留了URLClassLoader实例的索引集合,此集合是使用codebase注解进行索引的。

在JDK1.4中,你可以通过实现java.rmi.server.RMIClassLoaderSpi接口并将java.rmi.server.RMIClassLoaderSpi系统属性设置为你的新实现类,就可以定制类加载器的行为。

3.如果类已经存在了,URLClassLoader实例就会简单地返回它们。反之,则加载它们。

由于类加载器使用的是委托父类加载机制,因此第一次加载将会从文件系统加载类(如,启动类加载器将首选加载类)。当启动类加载器不能加载类的时候,RMI才会使用codebase注解中的URL来加载类。

19.3.1 重部署情况

假设你已经实现并部署了bank example.三个月后,你必须使用Account2接口这个新版本:



public interface Account2 extends Account{
//has new reporting functionality
public TransactionList getLastNTransaction(int numberOfTransactions)throws RemoteException;

public TransactionLIst getTransactionsSince(Date since)throws RemoteException;

}


你有两个选择。第一个是传统的重部署方式,即关闭所有运行的程序,并在每个客户端上重新安装应用程序。

第二个选择是分阶段推出。分阶段推出的关键是要了解:由于Account2继承至Account,第一个客户端程序将会同新服务器工作得很好。从命名服务获取stub的代码为:



private void getAccount(){

try{
_account = (Account)Naming.lookup(_accountNameField.getText());
}catch(Exception e){
System.out.println("Couldn't find account.Error was \n"+e);
e.printStackTrace();
}
return;
}


如果stub实现了Account2,Account2继承至Account--转换将会成功,此段代码会正常工作.老的客户端程序将不能访问新的功能,但它仍是一个好程序并且对象大多数任务都很有用。

如果你能在不必重部署客户端的情况下安装新的服务器,那么你可以在新的客户端程序逐步地分阶段引入。当你启动新服务器并绑定了Account2实例时,老的客户端程序将会收到新到新stub的序列化实例。由于老的程序在本地没有合适的类,它将从URL中下载它们。这种转换将会成功,并且服务器会以一种相对于客户端完全透明的方式进行升级。

同样的思想也可以应用于较少激烈的升级。比如,改动一个支持类(如套接字工厂),在名字改变了或老的类没有实际部署在客户端文件系统上情况下,可以立即完成。

19.3.2 多重部署情况(A Multiple-Deployment Scenario)

第一种情况是逐步将应用程序发动发布到客户端的。我们的第二种情况也是相似的,而且还会涉及到在不影响已有安装的情况下逐步更新服务器应用程序。

在这种情况下,我们可以会将同一个服务器的两个不同版本绑定到一个单独的命名服务中。它们是:

Server 1

Uses a version of the server class that implements the Account interface as of 02/10

Server 2

Uses a version of the server class that implements the Account interface as of 06/10

在这种情况下,我们不会将stub类放置到命名服务的类路径中。启动类加载器将会从文件系统中只加载一个类定义。无论加载的谁,其中的一服务器将不能绑定在命名服务中绑定它的stubs.

这个问题的解决方案是很简单的:不需要在命名服务的类路径中安装任何classes,并给两个应用程序不同的codebase URLs.RMI类加载算法将会动态地加载它们,并正确地使用它们。这些都是自动发生地。

/
Marshalled Objects and CodeBases

服务器能够下载classes,和实例一样,导致了在序列化时发生的一些有趣的问题。假设,举例来说,一个客户端需要实现一个简单地持久化层。一种自然的方式是通过采用序列化:客户端程序可以创建一个FileInputStream的实例并使用序列化来持久化对象的拷贝。

如果存储在文件中的实例是客户端程序下载的类(如,类的实例不在客户端程序的类路径中)的实例,那么有可能发生中断。

当客户端尝试反序列化文件中的实例时,它需要文件中实例的类定义。如果类定义不在客户端的类路径中,那么反序列化将会因为一般的序列化和反序列化算法没有存储在codebase属性中而失败。对于这种问题的一个解决方案是使用MarshalledObject的实例。

MarshalledObject是一个类,其实例可以包含另一个类的序列化实例。在17节中,我说明了MarshalledObject使用RMI的序列化定制版本来存储数据。这意味着,MarshalledObject包含的任何序列经实例都有一个codebase,当实例使用get()方法反序列化,如果有必要的话,可以从codebase位置中获取到类。

因此,如果你想在RMI应用程序中使用序列化来实现一个简单的客户端持久化层,那么你应该使用下面三个步骤:

1.创建一个FileInputStream的实例
2.序列化FileInputStream实例,并以参数的形式传递给MarshalledObject的构造参数
3.序列化MarshalledObject的实例(Serialize the instance of MarshalledObject to the instance of FileInputStream.
比起直接序列化file的实例,这可能有点复杂,但可以保证序列化实例在稍后可以读回来。
/
19.4 The Class Server

到目前为止的讨论中暗含着一个理念:类文件可以从URL中下载。也就是说,可以从一个类的codebase 注解(包含字符串分隔的URL序列和类名(classname)),RMI反序列算法可以查询和加载一个类。我已经提过RMI使用URLClassLoader类的实例来完成加载的。在这一节中,我们将讨论URLClassLoader是如何工作的。并且如何创建一个服务器来响应来自URLClassLoader的请求。

在下面的讨论中,我们将假设URLClassLoader的http://.实例也能处理ftp://.的URL.

19.4.1 请求一个类

理解HTTP的工作原理是很重要的,关键点在于:

1.HTTP是请求响应方式工作的。客户端发出一个请求,典型的是通过GET或POST请求,然后服务器发回一个响应。在响应发送完之后,连接就结束了。

2.HTTP是一个基于ASCII的协议。一个HTTP消息是由一系列的ASCII头,其后紧跟着同消息一同发送的二进制数据。

3.HTTP请求的第一行由方法,路径,协议组成的。路径通常与访问服务器上文件的物理路径相对应的。

4.HTTP响应的第一行由状态码和返回值描述组成。在那之后,响应还包括headers和content body.

URLClassLoader实例接受Http://machineName:port/path和类名形式的URL.步骤如下:

1.It creates a path from the classname by interpreting each package as a directory.For example,the class com.ora.rmibook.chapter9.valueobejcts.Money becomes the path /com/ora/rmibook/chapter9/valueobjects/Money.class

如,类com.ora.rmibook.chapter9.valueobjects.Money将会转换成路径/com/ora/rmibook/chapter9/valueobjects/Money.class

2.It prepends the path from the URL to the path it just created in order to form a request path.

3.It then issues an HTTP GET to a web server running on port of machineName.this request has the following format:

GET request-path HTTP/1.1

如.如果URL是http://localhost:80/,类名是com.ora.rmibook.chapter9.valueobjects.Money,由URLClassLoader实例将发送下面的请求到localhost的80端口上:

GET /com/ora/rmibook/chapter9/valueobjects/Money.class HTTP/1.1

19.4.2 接收一个class
一旦URLClassLoader实例发送了一个请求,它需要返回一个包含类的HTTP响应.也就是说,它期望接收到具有以下四个特征的消息:

1.响应码必须是HTTP 200
2.Content-Length头必须已被精确设置.
3.Content-Type头必须被设置且必须与application/java相等
4.在所有头部之后,必须包含类的字节码.

19.4.3 处理JAR文件

对于个别的类文件来说,我描述过的都工作得很好.但是对于包含在JAR文件中的类来说,却不能工作.从JAR文件中获取类的关键是从URL中删除结尾的/.

如果URL是以/结尾的,那么它遵守先前提到的算法.另一方面,如果URL不是以/结尾的,那么它将被假设成JAR文件的位置.

因此,如果URL是http://localhost:80/myclasses.jar,类为com.ora.rmibook.chapter9.valuesobjects.Money,那么URLClassLoader的实例将会这样做:

1.发送下面的HTTP命令到locahost的80端口
GET /myclasses.jar HTTP1.1
2.试图从JAR文件中加载com.ora.rmibook.chapter9.valueobjects.Money

19.4.4 sun的class server

为了不强迫安装和配置一个web服务器,Sun在不同的时期,提供了一个简单的用于处理服务端类文件响应URLClassLoader实例请求大部分细节的java类.此类如下:



public abstract class ClassServer implements Runnable {

private ServerSocket server = null;

private int port;

public abstract byte[] getBytes(String path) throws IOException,
ClassNotFoundException;

protected ClassServer(int port) throws IOException {

this.port = port;
server = new ServerSocket(port);
newListener();
}

public void run() {

Socket socket;
try {
socket = server.accept();
} catch (IOException e) {
System.out.println("Class Server died:" + e.getMessage());
e.printStackTrace();
return;
}

newListener();
try {
DataOutputStream out = new DataOutputStream(
socket.getOutputStream());
try {
BufferedReader in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
String path = getPath(in);
byte[] bytecodes = getBytes(path);
try {
out.writeBytes("HTTP/1.0 200 OK\r\n");
out.writeBytes("Content-Length: " + bytecodes.length
+ "\r\n");
out.writeBytes("Content-Type:application/java\r\n\r\n");
out.write(bytecodes);
out.flush();
} catch (IOException ie) {
return;
}
} catch (Exception e) {
out.writeBytes("HTTP/1.0 400 " + e.getMessage() + "\r\n");
out.writeBytes("Content-Type:text/html\r\n\r\n");
out.flush();
}
} catch (IOException ex) {
ex.printStackTrace();

} finally {
try {
socket.close();
} catch (IOException e) {
}
}
}

private void newListener() {
(new Thread(this)).start();
}

private static String getPath(BufferedReader in) throws IOException {
String line = in.readLine();
String path = "";
if (line.startsWith("GET /")) {
line = line.substring(5, line.length() - 1).trim();
int index = line.indexOf(".class");
if (index != -1) {
path = line.substring(0, index).replace("/", ".");
}
}
do {
line = in.readLine();
} while ((line.length() != -1) && (line.charAt(0) != '\r')
&& (line.charAt(0) != '\n'));
if (path.length() != 0) {
return path;
} else {
throw new IOException("Malformed Header");
}
}
}



这可能看来有点复杂,但它真的是一个非常简单的类:它监听一个端口,解析到来的HTTP请求,并把它们转变为对getBytes()抽象方法的调用.而且,作为解析的一部分,它还将请求路径转变成类名.如,它转变:

GET /com/ora/rmibook/chapter9/valueobjects/Money.class HTTP/1.1转变成使用com.ora.rmibook.chapter9.valueobjects.Money为参数getBytes()方法调用.getBytes()方法的返回值将同很少的头一起打包进HTTP响应返回给调用者.

当然,由于ClassServer是一个抽象类,它不能直接被使用.以下是接受单个命令行参数作为根目录的具体子类:


public class SimpleClassServer extends ClassServer {

private static String _pathPrefix;

private SimpleClassServer() throws IOException {
super(80);
}

@Override
public byte[] getBytes(String path) throws IOException,
ClassNotFoundException {
path = path.replace('.', '\\');
String actualPath = _pathPrefix + path + ".class";
FileInputStream fileInputStream = new FileInputStream(actualPath);
ByteArrayOutputStream inMemoryCopy = new ByteArrayOutputStream();
copy(fileInputStream, inMemoryCopy);
return inMemoryCopy.toByteArray();
}

private void copy(InputStream inputStream, OutputStream outputStream)
throws IOException {
int nextByte;
while ((nextByte = inputStream.read()) != -1) {
outputStream.write(nextByte);
}
}

public static void main(String[] args) throws IOException {
_pathPrefix = args[0];
new SimpleClassServer();
}
}


为了使用SimpleClassServer,你所要做的就是传递一个命令行参数,此参数作为存储类文件的基本目录.

比如,我将类文件存储在D:\classes\\中

基于Sun解析命令行参数的理由,末尾的\\是必须的

19.5 在应用程序中使用动态类加载

一旦你设置了一个class服务器(使用web服务器或上面的代码),你仍然需要调整你的应用程序.调整一个使用动态类加载的应用程序不需要做太多工作,只需要改动客户端和服务器端.

19.5.1服务端改动

服务器端的主要改动涉及明确地说明codebase,这可以通过java.rmi.server.codebase系统参数来完成.java.rmi.server.codebase的值应该设置为包含空格分隔的URL序列.如,下面的命令行调用就将codebase设置为http://localhost:80/:

start java -Djava.security.manager -Djava.rmi.server.codebase="http://localhost:80/" -Djava.security.policy="d:\\java.policy" com.rmi.ora.rmibook.chapter9.applications.ImplLaucher Bob 10000 Alex 1223

在注解序列化对象的类描述时,RMI将使用java.rmi.server.codebase的值.RMI使用以下规则来判断哪个注解将与类描述一起发送:

1.如果类是从文件系统加载的,并且设置了java.rmi.server.codebase,那么这个类的注解就等于java.rmi.server.codebase系统参数的值.
2.如果类是从文件系统加载的,但并没有设置java.rmi.server.codebase,那么类将不会被注解.
3.如果类不是从文件系统而是通过动态类加载机制加载进来的,那么原来的注解将会被保留和并以class注解形式发送.

通常情况下,通过命令行来设置java.rmi.server.codebase属性就可以满足要求了.但如果代码是通过激活框架启动的,那么就需要改动代码了.

以下是17章节中创建activation group的代码:



private static void createActivationGroup()throws ActivationException,RemoteException{
ActivationGroupID oldID = ActivationGroup.currentGroupID();
Properties pList = new Properties();
pList.put("java.security.policy","d:\\java.policy");
pList.put("sun.rmi.transport.connectionTimeout","30000");
ActivationGroupDesc.CommandEnvironment configInfo = null;
ActivationGroupDesc description = new ActivationGroupDesc(pList,configInfo);
ActivationGroupID id = (ActivationGroup.getSystem()).registerGroup(description);
ActivationGroup.createGroup(id,description,0);
return;
}



在代码的中部,创建了pList并填充了激活框架启动JVM时所需要的系统属性值.如果应用程序打算使用动态类加载的话,pList还应该包括java.rmi.server.codebase属性值.

即增加如下代码:



String codebase = System.getProperty("java.rmi.server.codebase");

if(null!=codebase&&0!=codebase.length){
pList.put("java.rmi.server.codebase",codebase);
}



你也许注意到了,当我们在启动服务器的时候,我们修正了安全策略.为了防止客户端在尝试连接注册表或服务器时抛出安全异常,这是必须的.仅在这期间,可以使用下面的安生策略文件:

grant{
permission java.security.AllPremission;
}


19.5.2命名服务上的改动

在大部分的应用程序架构中,启动代码将stubs绑定定命名服务中,客户端再从命名服务中获取stubs.也就是说,含有stub实例的命名服务需要所有相关的类定义.在banking应用程序中,我们在RMI注册表中绑定了Account接口的实现类:


public interface Account extends Remote{
public Money getBalance() throws RemoteException;
public void makeDeposit(Money amount)throws RemoteException,NegativeAmountException;
public void makeWithdrawal(Money amount)throws RemoteException,OverdraftException,NegativeAmountException;
}

那么包含注册表的JVM需要加载下面的类:

Account
AccountImpl_Stub
Money
NegativeAmountException
OverdraftException

这里有一个问题:如果注册表在它的本地类路径中有任何一个这样的类,那么它就会从文件系统而不是从注解包含的URL中加载.但是,如果命名服务从本地文件系统加载了类的话,那么,当客户端在请求实例时,原来的注解将会丢失(类没有注解或被新的java.rmi.server.codebase值覆盖).

解决这个问题的唯一方法是通过我们先前的第三条规则来保护codebase注解.此规则如下:

如果一个类是不是通过文件而是通过动态类加载方式加载的,用来在第一个位置加载类的原始注解将被保留并以class注解形式发送.

这就意味着不能有任何特定应用程序的类位于命名服务的构建路径之上.

/
不能清除命名服务的类路径可能是使得动态类加载中断的最决普通的原因.它真的很令人讨厌因为调用bind()或rebind()总会成功.只有当客户端尝试获取stub的时候,这种失败才会变得明显.
/

19.5.3 客户端改动

在客户端,需要做两个地方的发动.第一个是必须安装安全管理器.如果没有安全管理器的话,RMI是不会动态加载类的.

安装安全管理器可以在应用程序之前进行.如下面所示:


pubilc static void main(String[] args){
System.setSecurityManager(new RMISecurityManager());
}


需要被动态加载的类必须从客户端的类路径中删除.经过这些改动,RMI应用程序就可以运行时动态从服务器加载类了.

/
一旦你安装了安全管理器,你也需要安装一个安全策略.
/

19.5.4 完全禁用动态类加载

有多种情况可以使你禁用动态类加载.比如说,当从服务器下载类到客户端时,这可能非常有意义,但当客户端上传类到服务器时就会更多的风险.一般来说,这是一个典型的企业应用程序应该避免的安全风险.

有两种方式可以禁用动态类加载.第一种是不安装安全管理器,在这种情况下,RMI是不会动态地加载类的.第二种方式是通过设置专门的系统属性来禁用动态类加载.如果java.rmi.server.useCodeBaseOnly系统属性被设置为true,不管类是否有codebase注解,它都只从本地文件系统加载类.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值