Java RMI与脚本编程:远程对象垃圾回收及脚本应用解析
1. Java远程方法调用(RMI)概述
Java远程方法调用(RMI)允许一个JVM中的程序调用另一个JVM中Java对象的方法,为使用Java语言开发分布式应用提供了API。一个RMI应用涉及三个运行在不同JVM中的应用程序:
rmiregistry
应用程序、服务器应用程序和客户端应用程序。其中,
rmiregistry
应用程序随JDK一起发布,而服务器和客户端应用程序需要开发者自行开发。
-
服务器应用程序
:创建称为远程对象的Java对象,并将其注册到
rmiregistry
中,以便客户端后续进行名称查找。
-
客户端应用程序
:使用逻辑名称在
rmiregistry
中查找远程对象,获取其引用,然后调用该远程对象引用上的方法,这些方法会被发送到服务器应用程序在远程对象上执行,执行结果再从服务器应用程序返回给客户端应用程序。
2. RMI应用开发规则
开发RMI应用时,涉及远程通信的类和接口需要遵循以下规则:
-
创建远程接口
:必须继承自
Remote
接口,且接口中的所有方法必须包含至少抛出
RemoteException
的
throws
子句。
-
实现远程对象类
:必须实现远程接口。服务器应用程序创建实现远程接口的类的对象,将该对象导出以使其成为真正的远程对象,并将其注册到
rmiregistry
中。客户端应用程序只需要远程接口。
3. 动态类下载
如果三个应用程序中的任何一个需要本地不可用的类,它们可以在运行时动态下载这些类。要使JVM能够动态下载类,必须将
java.rmi.server.useCodebaseOnly
属性设置为
false
,默认情况下该属性设置为
true
,会禁用JVM中的类动态下载。与远程对象引用一起,JVM还会接收名为
java.rmi.server.codebase
的属性值,这是JVM可以(如果其自身的
java.rmi.server.useCodebaseOnly
属性设置允许)下载与远程对象引用一起使用所需类的URL。
4. RMI服务器调试
RMI应用中有多个组件协同工作,调试较为困难。可以通过在运行RMI服务器时将JVM属性
java.rmi.server.logCalls
设置为
true
来记录所有对RMI服务器的调用,所有调用都会记录到标准错误中,也可以将RMI服务器调用记录到文件中。
5. 远程对象的垃圾回收
在RMI应用中,远程对象在服务器的JVM中创建,
RMI
注册表和远程客户端会保留远程对象的引用。远程对象的垃圾回收与本地对象不同,本地对象在同一个JVM中创建和引用,垃圾回收器很容易确定其是否不再被引用。而对于远程对象,需要一个能够跟踪远程JVM中远程对象引用的垃圾回收器。
-
引用计数和租约机制
:RMI垃圾回收器基于引用计数,每个引用计数都关联一个租约,租约有一个有效时间段。当远程客户端(包括RMI注册表)获取远程对象的引用时,会向服务器上的RMI运行时发送消息,请求该远程对象引用的租约。服务器会为该客户端授予指定时间段的租约,将该远程对象的引用计数加1,并将租约返回给客户端。默认情况下,RMI服务器为远程对象授予10分钟的租约。
-
客户端的不同情况
:
-
客户端在租约期内完成使用
:客户端在获取租约的时间段内完成对远程对象引用的使用,会在客户端JVM中对该远程引用进行垃圾回收时向服务器发送消息,表示不再使用该远程对象。
-
客户端续租
:客户端可能希望为远程引用的租约再延长一段时间,会在租约有效期过半时向服务器发送租约续订请求,服务器会更新租约到期时间,在此期间不会对远程对象进行垃圾回收。
-
客户端崩溃
:服务器没有收到客户端的任何消息,客户端获取的远程引用的租约期到期,服务器会取消租约,并将该远程对象的引用计数减1。
以下是客户端与服务器之间消息交互的流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([客户端获取远程引用]):::startend --> B(发送dirty消息请求租约):::process
B --> C{租约有效期过半?}:::decision
C -->|是| D(发送续租请求):::process
C -->|否| E{客户端完成使用?}:::decision
E -->|是| F(发送clean消息取消租约):::process
E -->|否| C
D --> G(服务器更新租约):::process
F --> H(服务器减少引用计数):::process
H --> I{引用计数为0?}:::decision
I -->|是| J(远程对象可能被垃圾回收):::process
I -->|否| K(继续等待):::process
客户端向服务器发送消息的情况总结如下表:
| 发送时机 | 消息类型 | 说明 |
| ---- | ---- | ---- |
| 首次接收远程引用 | dirty | 向服务器表明拥有远程对象的引用并请求租约 |
| 续租时 | dirty | 请求延长租约有效期 |
| 完成使用远程引用 | clean | 通知服务器取消租约 |
6. 设置租约时间
默认租约期为10分钟,可以在启动RMI服务器时使用
java.rmi.dgc.leaseValue
属性设置租约期,租约期值以毫秒为单位。例如,以下命令将服务器程序的租约期设置为5分钟(300000毫秒):
C:\Java9APIsAndModules>java --module-path ^
dist\jdojo.rmi.common.jar;dist\jdojo.rmi.server.jar ^
-Djava.security.policy=file:///C:/mypolicy/rmi.policy ^
-Djava.rmi.dgc.leaseValue=300000 ^
-Djava.rmi.server.codebase=^
file:///C:/Java9APIsAndModules/dist/jdojo.rmi.common.jar ^
--module ^
jdojo.rmi.server/com.jdojo.rmi.server.RemoteServer
Remote server is ready...
7. 远程对象引用计数为零的通知
除了设置租约时间,RMI运行时还能提供远程对象引用计数降为零的通知。如果远程对象持有一些资源,希望在没有远程客户端引用它时释放这些资源,获取此通知很重要。要获取此通知,需要在远程对象实现类中实现
java.rmi.server.Unreferenced
接口,其声明如下:
public interface Unreferenced {
void unreferenced();
}
当远程对象的远程引用计数变为零时,
unreferenced()
方法会被调用。以下是一个实现
Unreferenced
接口的
RemoteUtilityImpl
类的示例:
// RemoteUtilityImpl.java
package com.jdojo.rmi.server;
import com.jdojo.rmi.common.RemoteUtility;
import java.rmi.server.Unreferenced;
import java.time.ZonedDateTime;
public class RemoteUtilityImpl implements
RemoteUtility, Unreferenced {
public RemoteUtilityImpl() {
}
@Override
public String echo(String msg) {
return msg;
}
@Override
public ZonedDateTime getServerTime() {
return ZonedDateTime.now();
}
@Override
public int add(int n1, int n2) {
return n1 + n2;
}
@Override
public void unreferenced() {
System.out.println(
"RemoteUtility unreferenced at: " +
ZonedDateTime.now());
}
}
要测试
unreferenced()
方法是否被调用,可以先启动RMI注册表应用程序,再启动RMI服务器应用程序。只要RMI注册表在运行,
unreferenced()
方法就不会被调用,需要关闭RMI注册表应用程序,等待远程对象引用的租约到期或在关闭RMI注册表时被取消,之后会在服务器程序的标准输出中看到
unreferenced()
方法打印的消息。
8. RMI注册表的使用
RMI注册表应仅作为启动远程客户端的引导手段,之后远程客户端可以通过对另一个远程对象的方法调用接收远程对象的引用。如果远程客户端通过对远程对象的远程方法调用接收远程对象引用,则该远程对象的引用无需在RMI注册表中注册。在这种情况下,最后一个远程客户端完成对远程引用的使用后,服务器会对远程对象进行垃圾回收,而不是在其绑定到RMI注册表时将其保留在内存中。
9. Java中的脚本编程
Java虚拟机(JVM)并非只能执行用Java编程语言编写的程序,它执行与语言无关的字节码,只要程序能编译成Java字节码,就可以执行任何编程语言编写的程序。
-
脚本语言的概念
:脚本语言是一种提供编写脚本能力的编程语言,这些脚本由称为脚本引擎(或解释器)的运行时环境进行评估(或解释)。脚本是使用脚本语言的语法编写的字符序列,用作解释器执行程序的源。解释器会解析脚本,生成程序的内部表示形式即中间代码,并执行该中间代码,同时将脚本中使用的变量存储在称为符号表的数据结构中。
-
脚本语言与编译语言的区别
:通常,与编译型编程语言不同,脚本语言的源代码(称为脚本)不进行编译,而是在运行时解释。不过,一些脚本语言编写的脚本可能会编译成可以由JVM运行的Java字节码。
-
Java的脚本支持
:Java平台包含了脚本支持,允许Java应用程序执行用JavaScript、Groovy、Jython、JRuby等脚本语言编写的脚本,支持双向通信,脚本还可以访问主机应用程序创建的Java对象,Java运行时和脚本语言运行时可以相互通信并使用彼此的功能。Java对脚本语言的支持通过Java脚本API实现,Java脚本API中的所有类和接口都在
javax.script
包中,该包位于
java.scripting
模块中。
-
使用脚本语言的优势
:
- 大多数脚本语言是动态类型的,使程序编写更简单。
- 提供了一种更快的方式来开发和测试小型应用程序。
- 允许最终用户进行定制。
- 脚本语言可能提供Java中没有的特定领域功能。
以下是Java脚本编程相关模块的声明示例:
// module-info.java
module jdojo.script {
requires java.scripting;
requires jdk.unsupported;
// <- needed for GraalVM JavaScript
exports com.jdojo.script;
}
使用Java脚本API的模块需要像
jdojo.script
模块一样读取
java.scripting
模块。
综上所述,Java的RMI和脚本编程功能为开发者提供了强大的工具,能够构建分布式应用和灵活的脚本化应用。在实际开发中,合理利用这些功能可以提高开发效率和应用的灵活性。
Java RMI与脚本编程:远程对象垃圾回收及脚本应用解析
10. 执行脚本并传递参数
在Java中执行脚本并传递参数是常见的操作。下面将详细介绍如何实现这一过程。
首先,需要获取脚本引擎实例。以JavaScript脚本为例,以下是获取JavaScript脚本引擎的代码:
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class ScriptExecutionExample {
public static void main(String[] args) {
// 创建脚本引擎管理器
ScriptEngineManager manager = new ScriptEngineManager();
// 获取JavaScript脚本引擎
ScriptEngine engine = manager.getEngineByName("JavaScript");
if (engine == null) {
System.out.println("未找到JavaScript脚本引擎");
return;
}
try {
// 向脚本引擎中设置参数
engine.put("message", "Hello from Java!");
// 执行脚本
engine.eval("print(message);");
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
上述代码的执行步骤如下:
1. 创建
ScriptEngineManager
对象,用于管理脚本引擎。
2. 通过
getEngineByName
方法获取JavaScript脚本引擎。
3. 使用
put
方法向脚本引擎中设置参数,这里设置了一个名为
message
的参数。
4. 使用
eval
方法执行脚本,脚本中打印了传入的参数。
11. ScriptContext的使用
ScriptContext
在执行脚本时起着重要作用,它用于管理脚本的输入、输出和变量。以下是一个使用
ScriptContext
的示例:
import javax.script.*;
import java.io.StringWriter;
public class ScriptContextExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
// 创建ScriptContext对象
SimpleScriptContext context = new SimpleScriptContext();
// 创建StringWriter用于捕获脚本输出
StringWriter writer = new StringWriter();
context.setWriter(writer);
try {
// 执行脚本并指定ScriptContext
engine.eval("print('This is a test.');", context);
// 输出脚本执行结果
System.out.println(writer.toString());
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
上述代码的操作步骤如下:
1. 创建
ScriptEngineManager
和脚本引擎。
2. 创建
SimpleScriptContext
对象,并创建
StringWriter
用于捕获脚本输出。
3. 将
StringWriter
设置为
ScriptContext
的输出目标。
4. 使用
eval
方法执行脚本,并指定
ScriptContext
。
5. 输出脚本执行结果。
12. 在脚本中使用Java编程
在脚本中可以使用Java编程,以下是一个在JavaScript脚本中使用Java对象的示例:
import javax.script.*;
public class UseJavaInScript {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
try {
// 执行脚本,创建Java对象并调用其方法
engine.eval("var date = new java.util.Date(); print(date);");
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
上述代码的执行流程为:
1. 创建脚本引擎管理器和脚本引擎。
2. 使用
eval
方法执行脚本,在脚本中创建
java.util.Date
对象并打印。
13. 实现脚本引擎
实现一个脚本引擎需要遵循一定的步骤,以下是一个简单的脚本引擎实现示例:
import javax.script.*;
import java.io.Reader;
// 自定义脚本引擎工厂
class MyScriptEngineFactory implements ScriptEngineFactory {
@Override
public String getEngineName() {
return "MyScriptEngine";
}
@Override
public String getEngineVersion() {
return "1.0";
}
@Override
public String getLanguageName() {
return "MyLanguage";
}
@Override
public String getLanguageVersion() {
return "1.0";
}
@Override
public String getMethodCallSyntax(String obj, String method, String... args) {
return null;
}
@Override
public String getOutputStatement(String toDisplay) {
return null;
}
@Override
public String getProgram(String... statements) {
return null;
}
@Override
public Object getParameter(String key) {
return null;
}
@Override
public String[] getNames() {
return new String[]{"myengine"};
}
@Override
public String[] getMimeTypes() {
return new String[0];
}
@Override
public String[] getExtensions() {
return new String[0];
}
@Override
public ScriptEngine getScriptEngine() {
return new MyScriptEngine(this);
}
}
// 自定义脚本引擎
class MyScriptEngine extends AbstractScriptEngine {
private final ScriptEngineFactory factory;
public MyScriptEngine(ScriptEngineFactory factory) {
this.factory = factory;
}
@Override
public Object eval(String script, ScriptContext context) throws ScriptException {
// 简单实现,直接返回脚本内容
return script;
}
@Override
public Object eval(Reader reader, ScriptContext context) throws ScriptException {
return null;
}
@Override
public Bindings createBindings() {
return new SimpleBindings();
}
@Override
public ScriptEngineFactory getFactory() {
return factory;
}
}
public class ImplementScriptEngine {
public static void main(String[] args) {
MyScriptEngineFactory factory = new MyScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine();
try {
Object result = engine.eval("This is a test script.");
System.out.println(result);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
实现脚本引擎的步骤如下:
1. 创建自定义的
ScriptEngineFactory
类,实现必要的方法,如
getEngineName
、
getScriptEngine
等。
2. 创建自定义的
ScriptEngine
类,继承自
AbstractScriptEngine
,实现
eval
等方法。
3. 在
main
方法中创建
ScriptEngineFactory
对象,获取脚本引擎并执行脚本。
14. 总结
本文详细介绍了Java远程方法调用(RMI)和Java中的脚本编程。RMI允许一个JVM中的程序调用另一个JVM中Java对象的方法,通过引用计数和租约机制实现远程对象的垃圾回收。同时,Java平台提供了脚本支持,允许执行多种脚本语言编写的脚本,支持双向通信和在脚本中使用Java对象。以下是本文的关键知识点总结:
| 主题 | 关键知识点 |
| ---- | ---- |
| Java RMI | 远程对象创建、注册,动态类下载,远程对象垃圾回收,引用计数和租约机制,RMI注册表的使用 |
| Java脚本编程 | 脚本语言概念,Java的脚本支持,执行脚本并传递参数,ScriptContext的使用,在脚本中使用Java编程,实现脚本引擎 |
通过合理运用这些技术,可以开发出更灵活、高效的Java应用程序。
超级会员免费看

122

被折叠的 条评论
为什么被折叠?



