Java17 教程·续(七)
原文:More Java 17
九、Java 远程方法调用
在本章中,您将学习:
-
什么是 Java 远程方法调用(RMI)和 RMI 架构
-
如何开发和打包 RMI 服务器和客户机应用程序
-
如何启动
rmiregistry、RMI 服务器和客户端应用程序 -
如何对 RMI 应用程序进行故障排除和调试
-
RMI 应用程序中的动态类下载
-
RMI 应用程序中远程对象的垃圾收集
RMI 应用程序包含分为三部分的类和接口:
-
服务器部分
-
客户端部分
-
公共部分,同时存在于客户端和服务器端
您将把本章中示例应用程序的三个部分打包成三个模块,分别命名为jdojo.rmi.common、jdojo.rmi.server和jdojo.rmi.client。这些模块的声明如清单 9-1 至 9-3 所示。
// module-info.java
module jdojo.rmi.client {
requires java.rmi;
requires jdojo.rmi.common;
exports com.jdojo.rmi.client;
}
Listing 9-3The Declaration of a jdojo.rmi.client Module
// module-info.java
module jdojo.rmi.server {
requires java.rmi;
requires jdojo.rmi.common;
exports com.jdojo.rmi.server;
}
Listing 9-2The Declaration of a jdojo.rmi.server Module
// module-info.java
module jdojo.rmi.common {
requires java.rmi;
exports com.jdojo.rmi.common;
}
Listing 9-1The Declaration of a jdojo.rmi.common Module
RMI 相关的类和接口在java.rmi模块中。包含 RMI 程序的模块需要读取java.rmi模块。jdojo.rmi.common模块包含将被服务器和客户端应用程序使用的类型,这就是jdojo.rmi.server和jdojo.rmi.client模块读取jdojo.rmi.common模块的原因。
什么是 Java 远程方法调用?
Java 支持各种应用程序架构,这些架构决定了应用程序代码如何以及在哪里部署和执行。在最简单的应用程序架构中,所有的 Java 代码都驻留在一台机器上,一个 JVM 管理所有的 Java 对象以及它们之间的交互。这是一个独立应用程序的例子,其中所需要的只是一台可以启动 JVM 的机器。Java 还支持分布式应用程序架构,其中应用程序的代码和执行可以分布在多台机器上。
在第八章中,你学习了用 Java 进行网络编程,这涉及到至少两个运行在不同机器上的 JVM,它们为客户机和服务器套接字执行 Java 代码。通常,套接字用于在两个应用程序之间传输数据。在套接字编程中,客户端程序可以向服务器程序发送消息。服务器程序创建一个 Java 对象,调用该对象上的方法,并将方法调用的结果返回给客户端程序。最后,客户端程序使用套接字读取结果。在这种情况下,客户机能够调用驻留在不同 JVM 中的 Java 对象上的方法。这种可能性为新的应用程序架构打开了大门,称为分布式编程,其中一个应用程序可以利用多台机器,运行多个 JVM 来处理业务逻辑。虽然可以使用套接字编程来调用驻留在不同 JVM(也可能在不同的机器上)中的对象的方法,但是编写代码并不容易。为了实现这一点,Java 提供了一种称为 Java 远程方法调用(Java RMI)的独立机制。
Java RMI 允许 Java 应用程序调用远程 JVM 中 Java 对象的方法。我使用术语“远程对象”来指由 JVM 创建和管理的 Java 对象,而不是管理调用该“远程对象”上的方法的 Java 代码的 JVM。通常,远程对象还意味着它是由 JVM 管理的,该 JVM 运行在访问它的机器之外的机器上。然而,Java 对象作为远程对象并不一定要存在于不同机器上的 JVM 中。出于学习目的,您将使用一台机器在一个 JVM 中部署远程对象,并在不同的 JVM 中启动另一个应用程序来访问远程对象。RMI 允许您将远程对象视为本地对象。在内部,它使用套接字来处理对远程对象的访问并调用其方法。
RMI 应用程序由两个程序组成,一个客户机和一个服务器,它们运行在两个不同的 JVM 中。服务器程序创建 Java 对象,并使远程客户端程序可以访问这些对象来调用这些对象上的方法。客户端程序需要知道远程对象在服务器上的位置,这样它就可以对它们调用方法。服务器程序创建一个远程对象,并将其引用注册(或绑定)到 RMI 注册表。RMI 注册表是一种名称服务,用于将远程对象引用绑定到名称,因此客户端可以使用注册表中基于名称的查找来获取远程对象的引用。RMI 注册表运行在独立于服务器程序的进程中。它是作为名为rmiregistry的工具提供的。当你在你的机器上安装一个 JDK/JRE 时,它被复制到 JDK/JRE 安装目录下的bin子目录中。
在客户端程序获得远程对象的远程引用后,它调用使用该引用的方法,就好像它是对本地对象的引用一样。RMI 技术负责调用在不同机器上的不同 JVM 上运行的服务器程序中的远程引用上的方法的细节。在 RMI 应用程序中,Java 代码是根据接口编写的。服务器程序包含接口的实现。客户机程序使用接口和远程对象引用来调用存在于服务器 JVM 中的远程对象上的方法。所有支持 Java RMI 的 Java 库类都在java.rmi包及其子包中。
RMI 体系结构
图 9-1 以简化的形式显示了 RMI 架构。图中的矩形框表示 RMI 应用程序中的一个组件。箭头线显示了沿箭头方向从一个组件发送到另一个组件的消息。显示从 1 到 11 的数字的椭圆表示在典型的 RMI 应用中发生的步骤序列。我将在本节中详细解释这些步骤。
图 9-1
RMI 体系结构
让我们假设您已经开发了 RMI 应用程序所需的所有 Java 类和接口。在这一节中,我将带您完成运行 RMI 应用程序时涉及的所有步骤。在接下来的几节中,您将开发每一步所需的 Java 代码。
RMI 应用程序的第一步是在服务器中创建一个 Java 对象。该对象将被用作远程对象。要使普通的 Java 对象成为远程对象,还需要执行一个额外的步骤。这一步被称为导出远程对象。当一个普通的 Java 对象作为远程对象导出时,它就可以接收/处理来自远程客户机的调用了。导出过程产生一个远程对象引用(也称为存根)。远程引用知道导出对象的细节,比如它的位置和可以远程调用的方法。该步骤在图中没有标出。它发生在服务器程序内部。当这一步完成时,远程对象已经在服务器中创建好了,并准备好接收远程方法调用。
下一步由服务器执行,向 RMI 注册中心注册(或绑定)远程引用。服务器为它在 RMI 注册表中注册的每个远程引用选择一个惟一的名称。远程客户端需要使用相同的名称在 RMI 注册表中查找远程引用。这在图中被标记为#1。当这一步完成时,RMI 注册中心已经注册了远程对象引用,对调用远程对象上的方法感兴趣的客户机可以从 RMI 注册中心请求它的引用。
Note
出于安全原因,RMI 注册中心和服务器必须运行在同一台机器上,以便服务器可以向 RMI 注册中心注册远程引用。如果没有施加这种限制,黑客可能会从他们的机器上向您的 RMI 注册表注册他们自己的有害 Java 对象。
这一步包括客户机和 RMI 注册中心之间的交互。通常,客户机和 RMI 注册中心运行在两台不同的机器上。客户机向 RMI 注册中心发送一个远程引用的查找请求。客户端使用名称在 RMI 注册表中查找远程引用。该名称与步骤#1 中服务器用来绑定 RMI 注册表中的远程引用的名称相同。查找步骤在图中标记为#2。RMI 注册中心将远程引用(或存根)返回给客户机,如图中步骤 3 所示。如果远程引用没有在 RMI 注册表中与客户机在查找请求中使用的名称绑定,RMI 注册表将抛出一个NotBoundException。如果这一步成功完成,客户机就收到了远程对象的远程引用(或存根)。
在这一步中,客户机调用存根上的一个方法。如图中步骤#4 所示。此时,存根连接到服务器并传输调用远程对象上的方法所需的信息,例如方法的名称、方法的参数等。存根知道服务器的位置以及如何联系服务器上的远程对象的细节。该步骤在图中被标记为步骤#5。网络层的许多不同层参与了从存根到服务器的信息传输。
框架是客户端存根的服务器端对应部分。它的工作是接收存根发送的数据。这在图中显示为步骤#6。在一个框架收到数据后,它将数据重组为更有意义的格式,并调用远程对象上的方法,如图中的步骤 7 所示。一旦服务器上的远程方法调用结束,框架就接收方法调用的结果(步骤#8),并通过网络层将信息传输回存根(步骤#9)。存根接收远程方法调用的结果(步骤#10),重组结果,并将结果传递给客户端程序(步骤#11)。
可以重复步骤#4 到#11 来调用同一远程对象上的相同或不同的方法。如果一个客户机想要调用一个不同的远程对象上的方法,它必须在启动一个远程方法调用之前首先执行步骤#2 和#3。
在 RMI 应用程序中,典型的情况是,客户机在开始时联系 RMI 注册中心以获得远程对象的存根。如果客户机需要运行在服务器上的另一个远程对象的存根,它可以通过调用它已经拥有的存根上的方法来获得它。请注意,远程对象的方法也可以向远程客户端返回一个存根。这样,远程客户端可以在启动时只在 RMI 注册表中执行一次查找。除了在 RMI 注册表中查找远程对象引用之外,为 RMI 应用程序编写的 Java 代码与非 RMI 应用程序没有什么不同。
开发 RMI 应用程序
本节将向您介绍编写 Java 代码来开发 RMI 应用程序的步骤。您将开发一个远程工具 RMI 应用程序,它将允许您执行三件事情:从服务器回显一条消息,从服务器获取当前日期和时间,以及将两个整数相加。编写 RMI 应用程序涉及以下步骤:
-
编写远程接口。
-
在类中实现远程接口。这个类的对象充当远程对象。
-
编写服务器程序。它创建一个实现远程接口的类的对象,并将其注册到 RMI 注册表中。
-
编写一个客户端程序来访问服务器上的远程对象。
编写远程接口
远程接口类似于任何其他 Java 接口,其方法应该从运行在不同 JVM 中的远程客户端调用。它有四个特殊要求:
-
它必须扩展标记
Remote接口。 -
远程接口中的所有方法都必须抛出一个
RemoteException或异常,即它的超类,如IOException或Exception。RemoteException是被检查的异常。远程方法还可以抛出任意数量的其他特定于应用程序的异常。 -
远程方法可以接受远程对象的引用作为参数。它也可以返回远程对象的引用作为它的返回值。如果远程接口中的方法接受或返回远程对象引用,则参数或返回类型必须声明为类型
Remote,而不是实现Remote接口的类的类型。 -
远程接口只能在其方法的参数或返回值中使用三种数据类型。它可以是基本类型、远程对象或可序列化的非远程对象。远程对象通过引用传递,而非远程可序列化对象通过复制传递。如果一个对象的类实现了
java.io.Serializable接口,那么这个对象就是可序列化的。
您将您的远程接口命名为RemoteUtility。清单 9-4 包含了RemoteUtility远程接口的代码,它是jdojo.rmi.common模块的一个成员。它包含三个方法,分别叫做echo()、getServerTime()和add(),提供了你想要的三个功能。
// RemoteUtility.java
package com.jdojo.rmi.common;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.time.ZonedDateTime;
public interface RemoteUtility extends Remote {
// Echoes a string message back to the client
String echo(String msg) throws RemoteException;
// Returns the current date and time to the client
ZonedDateTime getServerTime() throws RemoteException;
// Adds two integers and returns the result to the
// client
int add(int n1, int n2) throws RemoteException;
}
Listing 9-4A RemoteUtility Interface
实现远程接口
这一步包括创建一个实现远程接口的类。你将把这个类命名为RemoteUtilityImpl。它将实现RemoteUtility远程接口,并将提供三种方法的实现:echo()、getServerTime()和add()。这个类中可以有任意数量的其他方法。您必须做的唯一一件事就是为在RemoteUtility远程接口中定义的所有方法提供实现。远程客户端将只能调用该类的远程方法。如果在这个类中定义的方法不同于远程接口中定义的方法,那么这些方法对于远程方法调用是不可用的。但是,您可以使用其他方法来实现远程方法。清单 9-5 包含了RemoteUtilityImpl类的代码,它是jdojo.rmi.server模块的一个成员。
// RemoteUtilityImpl.java
package com.jdojo.rmi.server;
import com.jdojo.rmi.common.RemoteUtility;
import java.time.ZonedDateTime;
public class RemoteUtilityImpl implements RemoteUtility {
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;
}
}
Listing 9-5An Implementation Class for the RemoteUtility Remote Interface
远程对象实现类非常简单。它实现了RemoteUtility接口,并为该接口的三个方法提供了实现。注意,RemoteUtilityImpl类中的这些方法没有声明它们抛出了一个RemoteException。声明所有远程方法抛出一个RemoteException的要求是针对远程接口的,而不是实现远程接口的类。
有两种方法可以为远程接口编写实现类。一种方法是从java.rmi.server.UnicastRemoteObject类继承它。另一种方法是不从任何类或者从除了UnicastRemoteObject类之外的任何类继承它。清单 9-5 采取了后一种方法。它没有从任何类继承RemoteUtilityImpl类。
如果远程接口的实现类继承自UnicastRemoteObject类或其他类,会有什么不同呢?远程接口的实现类用于创建远程对象,远程对象的方法被远程调用。这个类的对象必须经过一个导出过程,这使得它适合于远程方法调用。UnicastRemoteObject类的构造函数自动为您导出对象。所以,如果你的实现类继承自UnicastRemoteObject类,它将为你以后的整个过程节省一步。有时,您的实现类必须从另一个类继承,这将迫使您不要从UnicastRemoteObject类继承它。需要注意的一点是,UnicastRemoteObject类的构造函数抛出了一个RemoteException。如果从UnicastRemoteObject类继承远程对象实现类,实现类的构造函数必须在其声明中抛出一个RemoteException。
清单 9-6 通过继承UnicastRemoteObject类重写了RemoteUtilityImpl类。这个实现中有两个新东西——在类声明中使用了extends子句,在构造函数声明中使用了throws子句。其他一切都保持不变。当你在本章的后面编写服务器程序时,我将讨论使用清单 9-5 和 9-6 中所示的RemoteUtilityImpl类的实现的区别。
// RemoteUtilityImpl.java
package com.jdojo.rmi.server;
import com.jdojo.rmi.common.RemoteUtility;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.time.ZonedDateTime;
public class RemoteUtilityImpl
extends UnicastRemoteObject
implements RemoteUtility {
// Must throw the RemoteException
public RemoteUtilityImpl() throws RemoteException {
}
@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;
}
}
Listing 9-6Rewriting the RemoteUtilityImpl Class by Inheriting It from the UnicastRemoteObject Class
编写 RMI 服务器程序
服务器程序的职责是创建远程对象,并使远程客户端可以访问它。服务器程序执行以下任务:
-
安装安全管理器
-
创建并导出远程对象
-
向 RMI 注册应用程序注册远程对象
后续部分将详细讨论这些步骤。
您需要确保服务器代码在安全管理器下运行。如果 RMI 程序没有运行安全管理器,它就不能从远程位置下载 Java 类。没有安全管理器,它只能使用本地 Java 类。在 RMI 服务器和 RMI 客户机中,程序可能需要从远程位置下载类文件。您将很快看到从远程位置下载 Java 类的例子。当在安全管理器下运行 Java 程序时,还必须通过 Java 策略文件控制对特权资源的访问。以下代码片段显示了如何安装安全管理器(如果尚未安装)。您可以使用java.lang.SecurityManager类或java.rmi.RMISecurityManager类的对象来安装安全管理器。
SecurityManager secManager = System.getSecurityManager();
if (secManager == null) {
System.setSecurityManager(new SecurityManager());
}
安全管理器通过策略文件控制对特权资源的访问。您需要设置适当的权限来访问 Java RMI 应用程序中使用的资源。对于本例,您将向所有代码授予所有权限。但是,您应该在生产环境中使用适当控制的策略文件。您需要在策略文件中输入以下条目来授予所有权限:
grant {
permission java.security.AllPermission;
};
通常,Java 策略文件驻留在计算机上用户的主目录中,它被命名为.java.policy。请注意,文件名以点开始。
RMI 服务器程序执行的下一步是创建一个实现远程接口的类的对象,它将作为一个远程对象。在您的例子中,您将创建一个RemoteUtilityImpl类的对象:
RemoteUtilityImpl remoteUtility = new RemoteUtilityImpl();
您需要导出远程对象,以便远程客户端可以调用它的远程方法。如果您的远程对象类(本例中为RemoteUtility类)继承自UnicastRemoteObject类,那么您不需要导出它。它会在您创建时自动导出。如果您的远程对象的类不是从UnicastRemoteObject类继承的,您需要使用UnicastRemoteObject类的exportObject()静态方法之一显式导出它。当您导出远程对象时,您可以指定一个端口号,它可以在该端口号上侦听远程方法调用。默认情况下,它监听端口 0,这是一个匿名端口。以下语句导出远程对象:
int port = 0;
RemoteUtility remoteUtilityStub = (RemoteUtility)
UnicastRemoteObject.exportObject(remoteUtility, port);
exportObject()方法返回导出的远程对象的引用,也称为存根或远程引用。您需要保留存根的引用,这样就可以向 RMI 注册中心注册它。
服务器程序执行的最后一步是使用名称向 RMI 注册表注册(或绑定)远程对象引用。RMI 注册中心是一个提供名称服务的独立应用程序。要在 RMI 注册表中注册一个远程引用,您必须首先找到它。RMI 注册表在特定端口的机器上运行。默认情况下,它运行在端口 1099 上。找到注册表后,需要调用它的bind()方法来绑定远程引用。您还可以使用它的rebind()方法,如果指定名称的旧绑定已经存在,它将替换旧绑定。使用的名字是一个String。您将使用名称MyRemoteUtility作为您的远程引用的名称。最好遵循 RMI 注册表中绑定引用对象的命名约定,以避免名称冲突。
Registry registry =
LocateRegistry.getRegistry("localhost", 1099);
String name = "MyRemoteUtility";
registry.rebind(name, remoteUtilityStub);
这就是编写服务器程序所需的全部内容。清单 9-7 包含 RMI 服务器的完整代码,它是jdojo.rmi.server模块的成员。它假设RemoteUtilityImpl类不从UnicastRemoteObject类继承,如清单 9-5 所示。
// RemoteServer.java
package com.jdojo.rmi.server;
import com.jdojo.rmi.common.RemoteUtility;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RemoteServer {
public static void main(String[] args) {
SecurityManager secManager =
System.getSecurityManager();
if (secManager == null) {
System.setSecurityManager(
new SecurityManager());
}
try {
RemoteUtilityImpl remoteUtility =
new RemoteUtilityImpl();
// Export the object as a remote object
int port = 0; // An anonymous port
RemoteUtility remoteUtilityStub =
(RemoteUtility) UnicastRemoteObject.
exportObject(remoteUtility, port);
// Locate the registry
Registry registry =
LocateRegistry.
getRegistry("localhost", 1099);
// Bind the exported remote reference in the
// registry
String name = "MyRemoteUtility";
registry.rebind(name, remoteUtilityStub);
System.out.println(
"Remote server is ready...");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
Listing 9-7An RMI Remote Server Program
如果您使用清单 9-6 中列出的RemoteUtilityImpl类的实现,您将需要修改清单 9-7 中的代码。try-catch块中的代码将变为如下代码。所有其他代码将保持不变:
RemoteUtilityImpl remoteUtility = new RemoteUtilityImpl();
// No need to export the object
// Locate the registry
Registry registry = LocateRegistry.
getRegistry("localhost", 1099);
// Bind the exported remote reference in the registry
String name = "MyRemoteUtility";
registry.rebind(name, remoteUtility);
System.out.println("Remote server is ready...");
您还没有准备好启动您的服务器程序。在接下来的小节中,我将讨论如何启动 RMI 应用程序。
出于安全原因,您只能从与 RMI 注册表运行在同一台机器上的 RMI 服务器程序将远程引用绑定到 RMI 注册表。否则,黑客可能会将任何任意的、可能有害的远程引用绑定到您的 RMI 注册表。默认情况下,LocateRegistry类的getRegistry()静态方法返回一个运行在同一台机器上端口 1099 的注册表的存根。您可以只使用以下代码在服务器程序中定位注册表:
// Get a registry stub for a local machine at port 1099
Registry registry = LocateRegistry.getRegistry();
请注意,对LocateRegistry.getRegistry()方法的调用并不试图连接到注册表应用程序。它只是返回注册表的存根。它是对这个存根控件的后续调用、bind()、rebind()或任何其他方法调用,试图连接到注册中心应用程序。
编写 RMI 客户端程序
RMI 客户端程序调用远程对象上的方法,这些方法存在于远程服务器上。客户端程序必须做的第一件事是知道远程对象的位置。RMI 服务器程序创建并知道远程对象的位置。发布远程对象的位置细节是服务器程序的责任,这样客户端就可以定位和使用它。服务器程序通过将远程对象与 RMI 注册表绑定来发布远程对象的位置细节,并给它一个名称,在您的例子中是MyRemoteUtility。客户端程序联系 RMI 注册表,并执行基于名称的查找来获取远程引用。获得远程引用后,客户端程序调用远程引用上的方法,这些方法在服务器中执行。通常,RMI 客户端程序执行以下操作:
-
它确保它在安全管理器下运行:
-
它定位远程引用被服务器绑定的注册表。您必须知道机器名或 IP 地址,以及运行 RMI 注册表的端口号。在真实的 RMI 程序中,您不会在客户端程序中使用
localhost来定位注册表。相反,RMI 注册中心将在一台单独的机器上运行。对于本例,您将在同一台机器上运行所有三个程序——RMI 注册表、服务器和客户机:
SecurityManager secManager =
System.getSecurityManager();
if (secManager == null) {
System.setSecurityManager(
new SecurityManager());
- 它使用
Registry接口的lookup()方法在注册表中执行查找。它将绑定的远程引用的名称传递给lookup()方法,并获取远程引用(或存根)。注意,lookup()方法必须使用与服务器绑定/重新绑定远程引用相同的名称。lookup()方法返回一个Remote对象。您必须将其转换为远程接口的类型。以下代码片段将从lookup()方法返回的远程引用转换为RemoteUtility接口类型:
// Locate the registry
Registry registry =
LocateRegistry.getRegistry(
"localhost", 1099);
- 它调用远程引用(或存根)上的方法。客户端程序将
remoteUtilStub引用视为对本地对象的引用。对它进行的任何方法调用都被发送到服务器执行。所有远程方法都抛出一个RemoteException。当您调用任何远程方法时,您必须处理RemoteException。
String name = "MyRemoteUtility";
RemoteUtility remoteUtilStub =
(RemoteUtility) registry.
lookup(name);
// Call the echo() method
String reply = remoteUtilStub.echo(
"Hello from the RMI client.");
...
清单 9-8 包含了客户端程序的完整代码,它是jdojo.rmi.client模块的一个成员。暂时不要运行这个程序。在接下来的几节中,您将一步一步地运行您的 RMI 应用程序。您可能会注意到,编写 RMI 代码并不复杂。RMI 中不同组件的管道是复杂的。
// RemoteClient.java
package com.jdojo.rmi;
import com.jdojo.rmi.common.RemoteUtility;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RemoteClient {
public static void main(String[] args) {
SecurityManager secManager =
System.getSecurityManager();
if (secManager == null) {
System.setSecurityManager(
new SecurityManager());
}
try {
// Locate the registry
Registry registry =
LocateRegistry.getRegistry(
"localhost", 1099);
String name = "MyRemoteUtility";
RemoteUtility remoteUtilStub =
(RemoteUtility) registry.
lookup(name);
// Call the echo() method
String reply = remoteUtilStub.echo(
"Hello from the RMI client.");
System.out.println("Reply: " +
reply);
} catch (RemoteException e) {
e.printStackTrace();
} catch (NotBoundException e) {
e.printStackTrace();
}
}
}
Listing 9-8An RMI Remote Client Program
分离服务器和客户端代码
在 RMI 应用程序中,将服务器和客户机程序的代码分开是很重要的。服务器程序需要有以下三个组件:
-
远程接口
-
远程接口的实现类
-
服务器程序
客户端程序需要有以下两个组件:
-
远程接口
-
客户端程序
从本章一开始,您就为客户机-服务器代码分离做好了准备。为了实现这一点,您将把jdojo.rmi.server和jdojo.rmi.common模块部署到服务器上,并且将jdojo.rmi.client和jdojo.rmi.common模块部署到客户机上。当您运行 RMI 应用程序时,在后面的部分中我将这些模块化 jar 称为jdojo.rmi.server.jar、jdojo.rmi.client.jar和jdojo.rmi.common.jar。
运行 RMI 应用程序
您需要按照以下特定顺序启动 RMI 应用程序中涉及的所有程序:
-
运行 RMI 注册表。
-
运行 RMI 服务器程序。
-
运行 RMI 客户端程序。
如果在运行任何程序时遇到任何问题,请参阅本章后面的“排除 RMI 应用程序故障”一节。
您的服务器和客户端程序使用安全管理器。在成功运行 RMI 应用程序之前,必须正确配置 Java 策略文件。出于学习目的,您可以将所有安全权限授予 RMI 应用程序。您可以通过创建一个名为rmi.policy的文本文件(您可以使用您想要的任何其他文件名)并输入以下内容来做到这一点,这将向所有代码授予所有权限:
grant {
permission java.security.AllPermission;
};
当运行 RMI 客户机或服务器程序时,需要使用java.security.policy JVM 选项将rmi.policy文件设置为 Java 安全策略文件。假设您已经将rmi.policy文件保存在 Windows 的C:\mypolicy文件夹中:
java - Djava.security.policy=^
file:///C:/mypolicy/rmi.policy <other-options>
这种设置 Java 策略文件的方法具有暂时的效果。它应该仅用于学习目的。您需要在生产环境中设置细粒度的安全性。
运行 RMI 注册表
RMI 注册表应用程序是随 JDK/JRE 安装一起提供的。它被复制到相应安装主文件夹的bin子文件夹中。在 Windows 平台上,它是rmiregistry.exe可执行文件。您可以通过使用命令提示符启动rmiregistry应用程序来运行 RMI 注册表。它接受将在其上运行的端口号。默认情况下,它运行在端口 1099 上。以下命令在 Windows 上使用命令提示符在端口 1099 启动它:
C:\java9\bin> rmiregistry
以下命令在端口 8967 启动 RMI 注册表:
C:\java9\bin> rmiregistry 8967
rmiregistry应用程序不在提示符下打印任何启动信息。通常,它是作为后台进程启动的。
很可能,该命令在您的机器上不起作用。使用该命令,您将能够成功启动rmiregistry。然而,当您在下一节运行 RMI 服务器应用程序时,您将得到ClassNotFoundException。rmiregistry应用程序需要访问 RMI 服务器应用程序中使用的一些类(已注册的类)。有三种方法可以让rmiregistry使用这些类:
-
适当设置
CLASSPATH。 -
将
java.rmi.server.codebaseJVM 属性设置为包含rmiregistry所需类的 URL。 -
将名为
java.rmi.server.useCodebaseOnly的 JVM 属性设置为false。该属性默认设置为true。如果该属性设置为false,rmiregistry可以从服务器下载需要的类文件。
在启动rmiregistry之前,以下命令将包含服务器类和公共接口的 jar 添加到CLASSPATH:
C:\java9\bin> SET CLASSPATH=^
C:\Java9APIsAndModules\dist\jdojo.rmi.common.jar;^
C:\Java9APIsAndModules\dist\jdojo.rmi.server.jar
C:\java9\bin> rmiregistry
除了设置CLASSPATH来使类对rmiregistry可用,您还可以设置java.rmi.server.codebase JVM 属性,它是一个用空格分隔的 URL 列表,如下所示:
C:\java9\bin> rmiregistry ^
-J-Djava.rmi.server.codebase=^
file:///C:/Java9APIsAndModules/dist/jdojo.rmi.common.jar ^
file:///C:/Java9APIsAndModules/dist/jdojo.rmi.server.jar
下面的命令重置CLASSPATH并将 JVM 的java.rmi.server.useCodebaseOnly属性设置为false,这样rmiregistry将从 RMI 服务器下载任何需要的类文件。您的示例将使用以下命令:
C:\java9\bin> SET CLASSPATH=
C:\java9\bin> rmiregistry ^
-J-Djava.rmi.server.useCodebaseOnly=false
运行 RMI 服务器
在运行 RMI 服务器之前,必须运行 RMI 注册表。回想一下,服务器在一个安全管理器下运行,该安全管理器要求您授予在 Java 策略文件中执行某些操作的权限。确保您已经在策略文件中输入了所需的授权。您可以使用以下命令来运行服务器程序。命令文本在一行中输入;为了清楚起见,已经用多行显示了它。命令文本中的每个部分都应该用空格分隔,而不是换行。在命令中,您需要更改 JAR 和策略文件的路径,以反映它们在您机器上的路径:
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.server.codebase=^
file:///C:/Java9APIsAndModules/dist/jdojo.rmi.common.jar ^
--module ^
jdojo.rmi.server/com.jdojo.rmi.server.RemoteServer
Remote server is ready...
你需要设置一个java.rmi.server.codebase属性。如果 RMI 注册表和客户机程序需要下载它们没有的类文件,它们就会使用这个方法。该属性的值是一个 URL,它可以指向本地文件系统、web 服务器、FTP 服务器或任何其他资源。URL 可以指向一个 JAR 文件,就像本例中一样,也可以指向一个目录。如果它指向一个目录,URL 必须以正斜杠结束。以下命令使用文件夹作为其基本代码。如果 RMI 注册中心和客户机需要任何类文件,它们将尝试从 URL file:///C:/myrmi/classes/下载类文件。
java -Djava.rmi.server.codebase=^
file:///C:/myrmi/classes/ <other-options>
您还可以设置一个java.rmi.server.codebase属性来指向一个 web 服务器,在那里您可以存储您需要的类文件,如下所示:
java -Djava.rmi.server.codebase=^
http://www.jdojo.com/rmi/classes/ <other-options>
如果将类文件存储在多个位置,可以指定所有位置,用空格分隔,如下所示:
java -Djava.rmi.server.codebase=^
"http://www.jdojo.com/rmi/classes/
ftp://www.jdojo.com/rmi/some/classes/c.jar" ^
<other-options>
它将一个位置指定为目录,将另一个位置指定为 JAR 文件。一个使用http协议,另一个使用ftp协议。这两个值由一个空格分隔,并且它们在一行上,而不是如图所示的两行。当您运行服务器或客户端程序时,可能会发生ClassNotFoundException,这很可能是由于java.rmi.server.codebase属性设置不正确或根本没有设置该属性而导致的。
运行 RMI 客户端程序
成功启动 RMI 注册表和服务器应用程序之后,就该启动 RMI 客户机应用程序了。您可以使用以下命令来运行客户端程序:
C:\Java9APIsAndModules>java ^
--module-path ^
dist\jdojo.rmi.common.jar;dist\jdojo.rmi.client.jar ^
-Djava.rmi.server.codebase=^
file:///C:/Java9APIsAndModules/dist/jdojo.rmi.common.jar ^
-Djava.security.policy=file:///C:/mypolicy/rmi.policy ^
--module ^
jdojo.rmi.client/com.jdojo.rmi.client.RemoteClient
Reply: Hello from the RMI client.
对于这个例子,当您运行前面的命令时,您不必包含一个java.rmi.server.codebase选项。但是,如果客户端程序在远程方法中使用参数,并且服务器上没有这些参数类型的类文件,则需要包含此选项。在这种情况下,服务器将从指定的java.rmi.server.codebase选项下载这些类文件。
当客户端程序成功运行时,您应该能够在控制台上看到输出。运行该程序时,您可能会得到不同的输出,因为它会打印当前日期和时间以及运行服务器应用程序的服务器的时区信息。
RMI 应用程序故障排除
在第一次运行 RMI 应用程序之前,很可能会出现许多错误。本节列出了您可能会收到的一些错误。它还将列出这些错误的一些可能原因和一些可能的解决方案。当您试图运行 RMI 应用程序时,不可能列出所有可能的错误。通过查看错误的堆栈打印,您应该能够找出大多数错误。
Java . RMI . server . export exception
当您试图运行rmiregistry应用程序或服务器应用程序时,您会得到一个ExportException。当您试图运行rmiregistry应用程序时,如果您得到这个异常,那么异常堆栈跟踪将类似于图中所示:
java.rmi.server.ExportException:
Port already in use: 1099; nested exception is:
java.net.BindException: Address already in use:
JVM_Bind...
它表明端口号 1099(在您的情况下可能是不同的号码)已经被使用。也许您已经在端口 1099(这是一个rmiregistry应用程序的默认端口号)启动了rmiregistry应用程序,或者一些其他应用程序正在使用端口 1099。您可以执行以下两项操作之一来解决此问题:
-
您可以停止使用端口 1099 的应用程序。
-
您可以在 1099 以外的端口启动
rmiregistry应用程序。
如果在运行服务器程序时得到一个ExportException,这是由于远程对象的导出过程失败造成的。导出过程失败的原因有很多。以下异常堆栈跟踪(显示了部分跟踪)是由两次导出同一个远程对象引起的:
java.rmi.server.ExportException:
object already exported
at sun.rmi.transport.ObjectTable.putTarget(
ObjectTable.java:189)
at sun.rmi.transport.Transport.exportObject(
Transport.java:92)...
检查您的服务器程序,并确保您只导出远程对象一次。从UnicastRemoteObject类继承远程对象实现类并使用UnicastRemoteObject类的exportObject()方法导出远程对象是一个常见的错误。当您从UnicastRemoteObject类继承远程对象的实现类时,您创建的远程对象会被自动导出。如果您尝试使用exportObject()方法再次导出它,您将得到这个异常。在讨论远程接口实现类时,我已经多次强调了这一点。当你开发一个 RMI 应用程序时,记住这句话,“犯错的是程序员,惩罚的是 Java。”即使是 RMI 程序设置中的一个小错误也可能需要几个小时来检测和修复。
Java . security . accesscontrolexception
当您的 Java 策略文件没有运行 RMI 应用程序所必需的grant条目时,您会得到这个异常。下面是一个异常的部分堆栈跟踪,它是在您尝试运行服务器程序,并尝试将远程对象绑定到 RMI 注册表时导致的:
java.security.AccessControlException:
access denied (java.net.SocketPermission
127.0.0.1:1099 connect,resolve)...
注册中心、服务器和客户端之间的通信是使用套接字执行的。为了安全起见,您必须在 Java 策略文件中授予适当的套接字权限,以便 RMI 应用程序的三个组件能够进行通信。大多数与安全相关的异常可以通过在 Java 策略文件中授予适当的权限来修复。
Java . lang . class notfounindexception
当没有找到 Java 运行时需要的类文件时,您会得到一个ClassNotFoundException异常。到目前为止,您一定已经多次收到这个异常。大多数情况下,当CLASSPATH没有正确设置时,您会收到这个异常。在 RMI 应用程序中,这个异常可能是另一个异常的原因。以下堆栈跟踪显示抛出了java.rmi.ServerException异常,其原因在于ClassNotFoundException异常:
java.rmi.ServerException:
RemoteException occurred in server thread;
nested exception is:
java.rmi.UnmarshalException:
error unmarshalling arguments;
nested exception is:
java.lang.ClassNotFoundException:
com.jdojo.rmi.RemoteUtility
...
Caused by: java.lang.ClassNotFoundException:
com.jdojo.rmi.RemoteUtility
at java.net.URLClassLoader$1.run(
URLClassLoader.java:220)
at java.net.URLClassLoader$1.run(
URLClassLoader.java:209)
当您运行服务器或客户端应用程序时,java.rmi.server.codebase选项没有正确设置或根本没有设置,就会引发这种类型的异常。
当服务器程序在没有使用java.rmi.server.codebase选项的情况下启动,并且rmiregistry应用程序在没有设置CLASSPATH的情况下运行时,这个异常被抛出。当您试图将一个远程引用与一个rmiregistry应用程序绑定/重新绑定时,服务器应用程序会将该远程引用发送给rmiregistry应用程序。在 JVM 中将远程引用表示为 Java 对象之前,rmiregistry应用程序必须加载该类。此时,rmiregistry将尝试从服务器启动时使用java.rmi.server.codebase属性指定的位置下载所需的类文件。
如果在运行客户端程序时遇到这个异常,请确保在运行客户端程序时设置了java.rmi.server.codebase属性。
请在运行服务器和客户端程序时检查CLASSPATH和java.rmi.server.codebase属性,以避免此异常。
当您运行客户端程序时,您会得到一个ClassNotFoundException,因为服务器无法找到在服务器端解组客户端调用所需的一些类定义。异常的部分堆栈跟踪示例如下所示:
java.rmi.ServerException:
RemoteException occurred in server thread;
nested exception is:
java.rmi.UnmarshalException:
error unmarshalling arguments;
nested exception is: java.lang.
ClassNotFoundException:
com.jdojo.rmi.client.Square
at sun.rmi.server.UnicastServerRef.dispatch(
UnicastServerRef.java:336)
at sun.rmi.transport.Transport$1.run(
Transport.java:159)...
在远程接口中定义的远程方法可以接受参数,该参数可以是接口或类类型。客户端可以传递实现接口的类的对象或远程接口的方法签名中定义的类型的子类的对象。如果服务器上不存在该类定义,服务器将尝试使用客户端应用程序中设置的java.rmi.server.codebase属性下载该类。您需要确保出现该错误的类(异常堆栈跟踪显示类名为com.jdojo.rmi.client.Square)在服务器 JVM 的CLASSPATH中,或者在运行远程客户端时设置java.rmi.server.codebase属性,以便服务器可以下载该类。
调试 RMI 应用程序
通过将名为java.rmi.server.logCalls的 JVM 属性设置为true,可以为 RMI 服务器应用程序打开 RMI 日志记录。默认情况下,它被设置为false。以下命令启动您的RemoteServer应用程序,将java.rmi.server.logCalls属性设置为true:
C:\Java9APIsAndModules>java ^
--module-path ^
dist\jdojo.rmi.common.jar;dist\jdojo.rmi.server.jar ^
-Djava.rmi.server.logCalls=true ^
-Djava.security.policy=file:///C:/mypolicy/rmi.policy ^
-Djava.rmi.server.codebase=^
file:///C:/Java9APIsAndModules/dist/jdojo.rmi.common.jar ^
--module ^
jdojo.rmi.server/com.jdojo.rmi.server.RemoteServer
当服务器 JVM 的java.rmi.server.logCalls属性被设置为true时,对服务器的所有传入调用以及在传入调用执行期间抛出的任何异常的堆栈跟踪都被记录到标准错误中。
RMI 运行时还允许您将服务器应用程序中的传入调用记录到一个文件中,而不考虑为服务器 JVM 的java.rmi.server.logCalls属性设置的值。您可以使用java.rmi.server.RemoteServer类的setLog(OutputStream out)静态方法将所有来电细节记录到一个文件中。通常,您在服务器程序代码的开头设置用于日志记录的文件输出流,比如您的com.jdojo.rmi.server.RemoteServer类的main()方法中的第一条语句。下面的代码片段支持将远程服务器应用程序中的调用记录到一个C:\rmilogs\rmi.log文件中。您可以通过使用null作为setLog()方法中的OutputStream来禁用呼叫记录:
try {
java.io.OutputStream os =
new java.io.FileOutputStream(
"C:\\rmilogs\\rmi.log");
java.rmi.server.RemoteServer.setLog(os);
} catch (FileNotFoundException e) {
System.err.println(
"Could not enable incoming calls logging.");
e.printStackTrace();
}
当安全管理器安装在服务器上时,允许记录到文件的运行代码必须有一个权限目标为"control"的java.util.logging.LoggingPermission。Java 策略文件中的以下 grant 条目将授予该权限。您还必须在 Java 策略文件中授予日志文件的"write"权限(在本例中为C:\rmilogs\rmi.log):
grant {
permission java.io.FilePermission
"c:\\rmilogs\\rmi.log", "write";
permission java.util.logging.LoggingPermission
"control";
};
如果您想要获得关于 RMI 客户机应用程序的调试信息,那么在启动 RMI 客户机应用程序时,将一个非标准的sun.rmi.client.logCalls属性设置为true。它将显示关于标准错误的调试信息。由于该属性不是公共规范的一部分,因此在未来的版本中可能会被删除。关于调试选项的更多细节,您需要参考 RMI 规范。你可以在 https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/faq.html 找到 RMI 规范。
如果您在编译和运行 RMI 应用程序时仍有问题,您可以参考位于 https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/faq.html 的网页。这个网页提供了使用 RMI 应用程序时几个常见问题的答案。
动态类下载
JVM 在创建类的对象之前加载类定义。它使用一个类加载器在运行时加载一个类。类加载器是java.lang.ClassLoader类的一个实例。类装入器必须先定位类的字节码,然后才能将其定义装入 JVM。Java 类加载器能够从任何位置(比如本地文件系统和网络)加载类的字节码。一个 JVM 中可以有多个类装入器,它们可以是系统定义的,也可以是自定义的。
JVM 在启动时创建一个类加载器,称为引导类加载器。引导类加载器负责加载基本 JVM 功能所需的初始类。基于父子关系,类装入器被组织成树状结构。引导类装入器没有父类。所有其他类装入器都将引导类装入器作为它们的直接或间接父类。在一个典型的类加载过程中,当一个类加载器被要求加载一个类的字节码时,它要求它的父类加载这个类,这个类反过来又要求它的父类,以此类推,直到引导类加载器得到加载这个类的请求。如果没有一个父类装入器能够装入该类,收到装入该类的初始请求的类装入器将尝试装入该类。
RMI 运行时使用一个特殊的 RMI 类加载器,负责加载 RMI 应用程序中的类。当一个对象在 RMI 应用程序中从一个 JVM 传递到另一个 JVM 时,发送 JVM 必须序列化和封送该对象,接收 JVM 必须反序列化和解封送该对象。发送 JVM 将属性java.rmi.server.codebase的值添加到对象的序列化流中。当在另一端接收到对象流时,接收 JVM 必须使用类装入器装入对象的类定义,然后才能将对象流转换成 Java 对象。JVM 指示 RMI 类加载器加载对象的类定义,它已经以流的形式接收到了该定义。类装入器试图从它的 JVM CLASSPATH中装入类定义。如果使用CLASSPATH没有找到类定义,那么类加载器使用对象流中java.rmi.server.codebase属性的值来加载类定义。
注意,java.rmi.server.codebase属性是在一个 JVM 中设置的,它用于下载另一个 JVM 中的类定义。当您运行 RMI 服务器或客户端程序时,可以设置该属性。当一方(服务器或客户机)将一个对象传输到另一方时,另一方没有字节码来表示正在接收的对象的类定义,发送方必须在发送对象时设置java.rmi.server.codebase属性,以便接收端可以使用该属性下载类字节码。java.rmi.server.codebase属性的值是一个用空格分隔的 URL 列表。
从安全的角度来看,从 RMI 服务器下载代码到客户机可能没有问题。有时,从客户端下载代码到服务器可能被认为是不安全的。默认情况下,禁止从远程 JVM 下载类。RMI 允许您通过使用一个java.rmi.server.useCodebaseOnly属性来启用/禁用这个特性。默认设置为true。如果设置为true,JVM 的类装入器将只从本地CLASSPATH或本地设置的java.rmi.server.codebase属性装入类。也就是说,如果设置为true,类加载器将不会从接收到的对象流中读取java.rmi.server.codebase的值来下载类定义。相反,它会在它的 JVM CLASSPATH中寻找类定义,并使用设置为它自己的 JVM 的java.rmi.server.codebase属性值的 URL。也就是说,当java.rmi.server.useCodebaseOnly属性被设置为true时,RMI 类加载器忽略从发送 JVM 在对象流中发送的codebase的值。属性名useCodebaseOnly似乎用词不当。如果把它命名为useLocallySetCodebaseOnly,可能会更好地表达它的意思。以下是在运行 RMI 服务器时如何设置该属性:
java –Djava.rmi.server.codebase=^
"http://www.myurl.com/rmiclasses" ^
-Djava.rmi.server.useCodebaseOnly=true ^
<other-options> ^
com.jdojo.rmi.RemoteServer
Note
属性的默认值被设置为 true。这意味着,默认情况下,不允许应用程序从其他 JVM 下载类。
将java.rmi.server.useCodebaseOnly属性设置为true有两种含义:
-
如果服务器需要一个类作为来自客户端的远程调用的一部分,它将总是在它的
CLASSPATH中查找,或者它将使用您为服务器程序设置的java.rmi.server.codebase的值。在前面的例子中,服务器中的所有类都必须在它的CLASSPATH或 URLT3 中找到。 -
如果客户端需要在远程方法调用中使用新的类类型,服务器必须预先知道新的类类型,因为服务器永远不会使用客户端关于从哪里下载所需新类的指令(在客户端使用
java.rmi.server.codebase属性设置)。这意味着您必须在服务器的CLASSPATH中或者在指定为服务器的java.rmi.server.codebase属性的 URL 中提供远程客户端将使用的新类。当远程方法接受一个接口类型,并且客户端发送实现该接口的类的对象时,可能会出现这种情况。在这种情况下,服务器可能没有与客户端相同的接口新实现的定义。
如果您为运行 RMI 客户机应用程序的 JVM 将java.rmi.server.useCodebaseOnly属性设置为true,那么前面的论点同样适用于运行 RMI 客户机应用程序。如果客户端应用程序的这个属性被设置为true,那么您必须使所有必需的类对客户端可用,要么将它们放在它的CLASSPATH中,要么将它们放在 URL 中,并将 URL 设置为客户端的java.rmi.server.codebase属性的值。
远程对象的垃圾收集
在 RMI 应用程序中,远程对象是在服务器上的 JVM 中创建的。RMI 注册表和远程客户端保存远程对象的引用。远程对象会被垃圾收集吗?而且,如果它确实被垃圾收集了,它是什么时候发生的,又是如何发生的?本地对象的垃圾收集很容易。在同一个 JVM 中创建和引用了一个本地对象。对于垃圾收集器来说,确定一个本地对象在 JVM 中不再被引用是一件容易的事情。
在 RMI 应用程序中,您需要一个垃圾收集器来跟踪远程 JVM 中远程对象的引用。假设一个 RMI 服务器创建了一个RemoteUtilityImpl类的远程对象,五个客户机获得了它的远程引用。RMI 注册中心也是一个客户机,它在绑定/重新绑定过程中获取远程引用。服务器何时以及如何垃圾收集被五个客户端引用的RemoteUtilityImpl类的唯一对象?
拥有远程对象的服务器上的 JVM 和五个不同客户机上的五个 JVM 必须交互,因此当服务器 JVM 中的远程对象不再被任何远程客户机使用时,可以对其进行垃圾收集。在这个讨论中,让我们忽略服务器 JVM 中远程对象的本地引用。远程客户机和 RMI 服务器之间的交互依赖于许多不可靠的因素。例如,网络可能会中断,远程客户端可能无法与服务器通信。第二个要考虑的问题是谁发起远程客户机和服务器之间的交互?是服务器不断询问远程客户端是否有实时远程引用吗?是远程客户端一直告诉服务器它仍然有一个活动的远程引用吗?客户端和服务器之间的交互由双方共同承担。远程客户端需要向服务器更新其远程引用的有效性。如果服务器在一段时间内没有收到任何客户端的消息,它会单方面决定将远程对象作为未来垃圾收集的候选对象。
RMI 垃圾收集器是基于引用计数的。参考计数具有关联的租约。租约有有效期。当一个远程客户机(包括一个 RMI 注册中心)获得一个对远程对象的引用时,它向服务器上的 RMI 运行时发送一个消息,请求租用那个远程对象引用。服务器向该客户端授予特定时间段的租约。服务器将该远程对象的引用计数递增 1,并将租约发送回客户端。默认情况下,RMI 服务器授予远程对象十分钟的租约。现在,以下是一些可能性:
-
客户机可以在其从服务器获得租用的时间段内完成远程对象引用。
-
客户可能希望将租期延长一段时间。
-
客户端崩溃。服务器不接收来自客户端的任何消息,并且客户端获取的远程引用的租期到期。
让我们看看每一种可能性。客户端在三种不同的情况下向服务器发送消息。它在第一次收到远程引用时就发送一条消息。它告诉服务器它有一个远程对象的引用。第二次,当它想要更新远程引用的租约时,它向服务器发送一条消息。第三次,当完成远程引用时,它向服务器发送一条消息。事实上,当一个远程引用在客户机应用程序中被垃圾收集时,它会向服务器发送一条消息,表明它已经完成了对远程对象的处理。在内部,远程客户端发送给服务器的消息只有两种类型:脏的和干净的。发送脏消息以获得租约,发送干净消息以移除/取消租约。这两条消息使用java.rmi.dgc.DGC接口的dirty()和clean()方法从远程客户端发送到服务器。作为一名开发人员,除了可以自定义租期之外,您对这些消息(发送或接收)没有任何控制权。租用时间段控制这些消息发送到服务器的频率。
当一个客户机完成一个远程对象引用时,它向服务器发送一个消息,表明它已经完成了。当客户机的 JVM 中的远程引用被垃圾收集时,发送该消息。因此,重要的是,一旦使用完毕,就将客户端程序代码中的远程引用设置为null。否则,服务器将继续保留远程对象,即使远程客户端不再使用它。您无法控制该消息从远程客户端发送到服务器的时间。要加快这个消息的发送,您所能做的就是将客户机代码中的远程对象引用设置为null,这样垃圾收集器将尝试对它进行垃圾收集,并向服务器发送一个干净的消息。
RMI 运行时跟踪远程客户端 JVM 中远程引用的租约。当租约到期到一半时,远程客户端向服务器发送一个租约续订请求,并续订租约。当远程客户机的租约为远程引用续订时,服务器会跟踪租约到期时间,并且不会对远程对象进行垃圾收集。理解为远程引用设置租期的重要性是很重要的。如果太小,大量的网络带宽将用于频繁更新租约。如果它太大,服务器将保持远程对象活动更长时间,以防客户端完成其远程引用,并且它不会通知服务器取消租用。我将简要讨论如何在 RMI 应用程序中设置租期值。
如果服务器没有从远程客户机听到任何关于客户机已经获得的远程引用的租用的消息,则在租用期到期后,它简单地取消租用并将该远程对象的引用计数减 1。这种由服务器做出的单方面决定对于处理行为不良的远程客户端(没有告诉服务器它是通过远程引用完成的)或任何可能阻止远程客户端与服务器通信的网络/系统故障非常重要。
当所有客户机都完成了对一个远程对象的远程引用时,它在服务器中的引用计数将下降到零。当远程客户端的租约到期或者它已经向服务器发送了干净的消息时,远程客户端被认为完成了远程引用。在这种情况下,RMI 运行时将使用一个弱引用来引用远程对象,因此如果没有对远程对象的本地引用,它可能会被垃圾收集。
默认情况下,租期设置为十分钟。当您启动 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...
除了设置租用时间段之外,所有事情都由 RMI 运行时处理。RMI 运行时为您提供了关于远程对象垃圾收集的更多信息。它可以告诉你远程对象的引用计数何时降到零。如果一个远程对象持有一些资源,而您希望在没有远程客户端引用它时释放这些资源,那么得到这个通知是很重要的。要获得这个通知,您需要在您的远程对象实现类中实现java.rmi.server.Unreferenced接口。其声明如下:
public interface Unreferenced {
void unreferenced()
}
当远程对象的远程引用计数变为零时,调用unreferenced()方法。如果您想在您的示例中为RemoteUtility远程对象获得通知,您需要修改RemoteUtilityImpl类的声明,如清单 9-9 所示。
// 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());
}
}
Listing 9-9A Modified Version of the RemoteUtilityImpl Class That Implements the Unreferenced Interface
您可能会注意到,这一次,RemoteUtilityImpl类实现了Unreferenced接口并提供了unreferenced()方法的实现,该方法在其引用计数变为零时向标准输出打印一条消息。RMI 运行时将调用unreferenced()方法。为了测试是否调用了unreferenced()方法,您可以启动 RMI 注册表应用程序,然后启动 RMI 服务器应用程序。RMI 注册表将继续更新远程对象的租约。只要 RMI 注册中心还在运行,您就永远不会看到unreferenced()方法被调用。您需要关闭 RMI 注册表应用程序,并等待远程对象引用的租约到期,或者在您关闭它时被 RMI 注册表取消。RMI 注册表关闭后,您将在服务器程序的标准输出上看到由unreferenced()方法打印的消息。
RMI 注册表应该被用作启动远程客户端的引导工具。稍后,远程客户端可以接收远程对象的引用,作为对另一个远程对象的方法调用。如果远程客户机通过对远程对象的远程方法调用接收远程对象引用,则该远程对象的引用不需要在 RMI 注册表中注册。在这种情况下,在最后一个远程客户机完成远程引用后,当远程对象被绑定到 RMI 注册表时,服务器将对其进行垃圾收集,而不是将其保存在内存中。
摘要
Java 远程方法调用(RMI)允许在一个 JVM 中运行的程序调用在另一个 JVM 中运行的 Java 对象上的方法。RMI 提供了一个 API 来使用 Java 编程语言开发分布式应用程序。
一个 RMI 应用程序包含运行在三个 JVM 中的三个应用程序:rmiregistry应用程序、服务器应用程序和客户机应用程序。JDK 附带了rmiregistry应用程序。您负责开发服务器和客户端应用程序。服务器应用程序创建称为远程对象的 Java 对象,并将它们注册到rmiregistry中,供客户机以后查找名称。客户端应用程序使用逻辑名称在rmiregistry中查找远程对象,并取回远程对象的引用。客户端应用程序调用发送到服务器应用程序的远程对象引用上的方法,以便在远程对象上执行该方法。方法调用的结果从服务器应用程序发送回客户端应用程序。
RMI 应用程序必须遵循一些规则来开发远程通信中涉及的类和接口。您需要创建一个必须从Remote接口继承的接口(称为远程接口)。接口中的所有方法必须包含一个至少抛出RemoteException的throws子句。远程对象的类必须实现远程接口。服务器应用程序创建实现远程接口的类的对象,导出该对象以给出真实远程对象的状态,并将其注册到rmiregistry。客户端应用程序只需要远程接口。
如果这三个应用程序中的任何一个需要本地没有的类,它们可以在运行时动态下载。对于动态下载类的 JVM 来说,java.rmi.server.useCodebaseOnly属性必须设置为false。默认情况下,它被设置为true,这将禁止动态下载 JVM 中的类。除了远程对象引用,JVM 还接收一个名为java.rmi.server.codebase的属性值,这是一个 URL,JVM 可以从这个 URL 下载(如果它自己的java.rmi.server.useCodebaseOnly属性设置允许的话)使用远程对象引用所需的类。
RMI 应用程序中有几个组件协同工作,这使得调试变得很困难。通过在 JVM 属性java.rmi.server.logCalls设置为true的情况下运行 RMI 服务器,您可以记录对它的所有调用。所有对服务器的调用都将被记录为标准错误。您还可以将 RMI 服务器调用记录到文件中。
RMI 为运行在 RMI 服务器中的远程对象提供自动垃圾收集。远程对象的垃圾收集基于引用计数和租约。当客户机应用程序获得远程对象的引用时,它也从服务器应用程序获得远程对象的租约。租约在一段时间内有效。只要客户端应用程序保留远程对象引用,它就会定期更新租约。服务器应用程序跟踪远程对象的引用计数和租约。当客户机应用程序完成远程引用时,它向服务器应用程序发送一条消息,服务器应用程序将远程对象的引用计数减一。当远程对象的引用计数在服务器应用程序中减少到零时,远程对象被垃圾收集。
练习
练习 1
什么是 Java 远程方法调用?
练习 2
每个远程接口必须扩展的接口的全限定名称是什么?
运动 3
创建远程对象后,需要在 RMI 服务器程序中执行哪些步骤,以便客户端可以使用远程对象?
演习 4
什么是 RMI 注册表,它位于哪里?
锻炼 5
在 RMI 应用程序中,可以将 RMI 注册中心和 RMI 服务器部署到两台不同的机器上吗?如果你的答案是否定的,请解释原因。
锻炼 6
描述 RMI 客户端程序调用远程对象上的方法需要执行的典型步骤序列。
锻炼 7
RMI 应用程序包括三层应用程序:客户机、RMI 注册中心和服务器。这些应用程序必须按什么顺序运行?
运动 8
描述在运行 RMI 客户机和服务器应用程序时如何使用java.rmi.server.codebase命令行选项。
演习 9
运行 RMI 服务器程序时使用java.rmi.server.logCalls=true命令行选项会有什么影响?
运动 10
如何将 RMI 服务器应用程序中的远程调用记录到文件中?
演习 11
运行 RMI 应用程序时使用java.rmi.server.useCodebaseOnly=true命令行选项会有什么影响?
运动 12
简要解释远程对象是如何被垃圾收集的。
运动 13
描述当远程对象不再被引用时获得通知的步骤。
十、使用 Java 编写脚本
在本章中,您将学习:
-
什么是 Java 脚本
-
如何从 Java 执行脚本以及如何向脚本传递参数
-
如何在执行脚本时使用
ScriptContext -
如何在脚本中使用 Java 编程语言
-
如何实现脚本引擎
除非另有说明,本章中的所有示例程序都是清单 10-1 中声明的jdojo.script模块的成员。
// module-info.java
module jdojo.script {
requires java.scripting;
requires jdk.unsupported;
// <- needed for GraalVM JavaScript
exports com.jdojo.script;
}
Listing 10-1The Declaration of a jdojo.script Module
JDK 的脚本支持在java.scripting模块中。使用 Java 脚本 API 的模块需要像jdojo.script模块一样读取java.scripting模块。
Java 中的脚本是什么?
有人认为 Java 虚拟机(JVM)可以执行只用 Java 编程语言编写的程序,这是不正确的。JVM 执行语言无关的字节码。如果程序可以被编译成 Java 字节码,它可以执行用任何编程语言编写的程序。
脚本语言是一种编程语言,它提供了编写脚本的能力,这些脚本由一个叫做脚本引擎(或解释器)的运行时环境评估(或解释)。脚本是使用脚本语言的语法编写的字符序列,用作由解释器执行的程序的源。解释器解析脚本;产生中间代码,它是程序的内部表示;并执行中间代码。解释器将脚本中使用的变量存储在名为符号表的数据结构中。
通常,与编译编程语言不同,脚本语言中的源代码(称为脚本)不是编译的,而是在运行时解释的。然而,用一些脚本语言编写的脚本可以被编译成 Java 字节码,由 JVM 运行。
Java 已经包含了对 Java 平台的脚本支持,允许 Java 应用程序执行用 JavaScript、Groovy、Jython、JRuby 等脚本语言编写的脚本。支持双向通信。它还允许脚本访问由宿主应用程序创建的 Java 对象。Java 运行时和脚本语言运行时可以相互通信并使用彼此的功能。
Java 对脚本语言的支持来自 Java 脚本 API。Java 脚本 API 中的所有类和接口都在javax.script包中,这个包在java.scripting模块中。
在 Java 应用程序中使用脚本语言有几个好处:
-
大多数脚本语言都是动态类型的,这使得编写程序更加简单。
-
它们为开发和测试小型应用程序提供了一种更快捷的方式。
-
最终用户可以进行定制。
-
脚本语言可以提供 Java 中没有的特定领域的特性。
脚本语言也有一些缺点。例如,动态类型有利于编写更简单的代码;然而,当一个类型被错误地解释时,它就变成了一个缺点,你必须花很多时间去调试它。
Java 中的脚本支持让您可以利用两个世界的优势:它允许您使用 Java 编程语言来开发应用程序的静态类型、可伸缩和高性能部分,并使用适合特定领域需求的脚本语言来开发其他部分。
我在本章中经常使用术语脚本引擎。一个脚本引擎是一个执行用脚本语言编写的程序的软件组件。通常,但不一定,脚本引擎是脚本语言的解释器的实现。Java 已经实现了几种脚本语言的解释器。它们公开了编程接口,因此 Java 程序可以与它们进行交互。
JDK 曾经和一个叫做 Nashorn JavaScript 的脚本引擎捆绑在一起。不过在甲骨文的 JDK15 中已经去掉了 Nashorn,虽然在 OpenJDK 16 中你还能找到它。我们在这一章中不谈论纳松。
Java 可以执行任何为脚本引擎提供实现的脚本语言的脚本。比如 Java 可以执行用 GraalVM JavaScript、Groovy、Jython、JRuby 等编写的脚本。本章中的例子使用 Groovy 语言。
Note
作为 Nashorn JavaScript 引擎的替代品,您可以考虑使用 GraalVM 提供的 JavaScript 脚本引擎。不幸的是,这个不能很好地与 OpenJDK 17 配合使用。
在 Maven 中安装脚本引擎
如果您使用 Maven 作为构建工具,安装脚本引擎是很容易的。您所需要做的就是在您的pom.xml中添加一个非标准的存储库和某些依赖项:
<project xmlns:="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>your.project.group.id</groupId>
<artifactId>your.project.artifact.id</artifactId>
<version>your.project.version</version>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-jsr223</artifactId>
<version>3.0.8</version>
</dependency>
<!-- Other script engines:-->
<dependency>
<groupId>org.scijava</groupId>
<artifactId>scripting-jython</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.scijava</groupId>
<artifactId>scripting-jruby</artifactId>
<version>0.3.1</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>21.1.0</version>
</dependency>
<dependency>
<!-- needed for GraalVM.js -->
<groupId>org.graalvm.truffle</groupId>
<artifactId>truffle-api</artifactId>
<version>21.1.0</version>
</dependency>
...
</dependencies>
<repositories>
<repository>
<id>Maven Repo</id>
<url>
https://repo1.maven.org/maven2/
</url>
</repository>
<repository>
<id>Maven Repo 2</id>
<url>
http://maven.imagej.net/content/repositories/releases/
</url>
</repository>
</repositories>
...
</project>
当然,您可以注释掉或删除不需要的脚本引擎。
执行您的第一个脚本
在本节中,您将使用 Groovy 在标准输出中打印一条消息。使用任何其他脚本语言都可以使用相同的步骤来打印消息,只有一点不同:您需要使用特定于脚本语言的代码来打印消息。要用 Java 运行脚本,您需要执行以下三个步骤:
-
创建脚本引擎管理器。
-
从脚本引擎管理器获取脚本引擎的实例。
-
调用脚本引擎的
eval()方法执行脚本。
脚本引擎管理器是ScriptEngineManager类的一个实例:
// Create an script engine manager
ScriptEngineManager manager = new ScriptEngineManager();
接口的一个实例代表了 Java 程序中的一个脚本引擎。ScriptEngineManager的getEngineByName(String engineShortName)方法返回一个脚本引擎的实例。要获得 Groovy 引擎的实例,使用Groovy作为引擎的简称,如下所示:
// Get the reference of the Groovy engine
ScriptEngine engine =
manager.getEngineByName("Groovy");
Note
脚本引擎的简称区分大小写。有时,一个脚本引擎有多个简称。groovy 引擎有以下简称:Groovy,Groovy。您可以使用这些引擎的简称中的任何一个,通过使用ScriptEngineManager类的getEngineByName()方法来获得它的实例。请注意可能与其他脚本引擎的名称冲突。
在 Groovy 中,println()函数在标准输出中打印一条消息。Groovy 中的字符串是用单引号或双引号括起来的字符序列。下面的代码片段将一个 Groovy 脚本存储在一个 Java String对象中,该对象将Hello Scripting!打印到标准输出:
// Store a Groovy script in a String
String script = "println('Hello Scripting!')";
如果您想在 Groovy 中使用双引号将字符串括起来,该语句将如下所示:
// Store a Groovy script in a String
String script = "println(\"Hello Scripting!\")";
或者
// Store a Groovy script in a String
String script = """println("Hello Scripting!")""";
要执行脚本,需要将脚本传递给脚本引擎的eval()方法。脚本引擎在运行脚本时可能会抛出一个ScriptException。因此,当您调用ScriptEngine的eval()方法时,您需要处理这个异常。以下代码片段执行存储在script变量中的脚本:
try {
engine.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
清单 10-2 包含程序在标准输出上打印信息的完整代码。
// HelloScripting.java
package com.jdojo.script;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class HelloScripting {
public static void main(String[] args) {
// Create a script engine manager
ScriptEngineManager manager =
new ScriptEngineManager();
// Obtain a Groovy script engine from the manager
ScriptEngine engine =
manager.getEngineByName("Groovy");
// Store the Groovy script in a String
String script = """
println('Hello Scripting!')
""";
try {
// Execute the script
engine.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Hello Scripting!
Listing 10-2Printing a Message on the Standard Output Using Groovy
使用其他脚本语言
在 Java 程序中使用除 Groovy 之外的脚本语言非常简单。在使用脚本引擎之前,您只需要执行一项任务:在应用程序模块路径中包含特定脚本引擎的 JAR 文件。脚本引擎的实现者提供这些 JAR 文件。
Java 的服务提供者机制将列出其模块化 JAR 或 JAR 文件已经包含在应用程序的模块路径中的所有脚本引擎。接口的一个实例用于创建和描述一个脚本引擎。脚本引擎的提供者为ScriptEngineFactory接口提供了一个实现。ScriptEngineManager的getEngineFactories()方法返回所有可用脚本引擎工厂的List<ScriptEngineFactory>。ScriptEngineFactory的getScriptEngine()方法返回ScriptEngine的一个实例。工厂的其他几个方法返回关于引擎的元数据。
清单 10-3 展示了如何打印所有可用脚本引擎的细节。输出显示 Groovy 的脚本引擎是可用的。它之所以可用,是因为我已经向 Maven 项目添加了org.codehaus.groovy:groovy-jsr223:3.0.8工件,这导致在我的机器上包含了模块路径所需的所有 jar。当您在模块路径中包含了一个脚本引擎,并且想知道脚本引擎的简称时,这个程序会很有帮助。运行该程序时,您可能会得到不同的输出。
// ListingAllEngines.java
package com.jdojo.script;
import java.util.List;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
public class ListingAllEngines {
public static void main(String[] args) {
ScriptEngineManager manager =
new ScriptEngineManager();
// Get the list of all available engines
List<ScriptEngineFactory> list =
manager.getEngineFactories();
// Print the details of each engine
for (ScriptEngineFactory f : list) {
System.out.println("Engine Name:" +
f.getEngineName());
System.out.println("Engine Version:" +
f.getEngineVersion());
System.out.println("Language Name:" +
f.getLanguageName());
System.out.println("Language Version:" +
f.getLanguageVersion());
System.out.println("Engine Short Names:" +
f.getNames());
System.out.println("Mime Types:" +
f.getMimeTypes());
System.out.println(
"----------------------------");
}
}
}
ScriptEngineFactory Info
Script Engine: Groovy Scripting Engine (2.0)
Engine Alias: groovy
Engine Alias: Groovy
Language: Groovy (3.0.8)
Listing 10-3Listing All Available Script Engines
清单 10-4 展示了如何使用 JavaScript、Groovy、Jython 和 JRuby 在标准输出中打印消息。如果脚本引擎不可用,程序会打印一条消息说明这一点。您可能会得到不同的输出。
// HelloEngines.java
package com.jdojo.script;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class HelloEngines {
public static void main(String[] args) {
// Get the script engine manager
ScriptEngineManager manager =
new ScriptEngineManager();
// Try executing scripts in JavaScript, Groovy,
// Jython, and JRuby
execute(manager, "JavaScript",
"print('Hello JavaScript')");
execute(manager, "Groovy",
"println('Hello Groovy')");
execute(manager, "jython",
"print 'Hello Jython'");
execute(manager, "jruby",
"puts('Hello JRuby')");
}
public static void
execute(ScriptEngineManager manager, String engineName,
String script) {
// Try getting the engine
ScriptEngine engine =
manager.getEngineByName(engineName);
if (engine == null) {
System.out.println(engineName +
" is not available.");
return;
}
// If we get here, it means we have the engine
// installed. So, run the script
try {
engine.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
JavaScript is not available.
Hello Groovy
jython is not available.
jruby is not available.
Listing 10-4Printing
a Message on the Standard Output Using Different Scripting Languages
探索 javax.script 包
Java 中的 Java 脚本 API 由少量的类和接口组成。它们在java.scripting模块的javax.script包中。本节包含对这个包中的类和接口的简要描述。我将在后续章节中讨论它们的用法。
ScriptEngine 和 ScriptEngineFactory 接口
ScriptEngine接口是 Java 脚本 API 中的主要接口,它的实例促进了以特定脚本语言编写的脚本的执行。
ScriptEngine接口的实现者也提供了ScriptEngineFactory接口的实现。一辆ScriptEngineFactory执行两项任务:
-
它创建脚本引擎的实例。
-
它提供关于脚本引擎的信息,如引擎名称、版本、语言等。
AbstractScriptEngine 类
AbstractScriptEngine是一个抽象类。它为ScriptEngine接口提供了部分实现。除非实现脚本引擎,否则不能直接使用该类。
ScriptEngineManager 类
ScriptEngineManager类为脚本引擎提供了发现和实例化机制。它还维护一个键-值对的映射,作为存储状态的Bindings接口的一个实例,由它创建的所有脚本引擎共享。
可编译接口和 CompiledScript 类
可选地,可以通过脚本引擎来实现Compilable接口,该脚本引擎允许编译脚本以便重复执行,而无需重新编译。
CompiledScript类被声明为抽象的。它是由脚本引擎的提供者扩展的。它以编译形式存储脚本,无需重新编译即可重复执行。请注意,使用ScriptEngine重复执行脚本会导致脚本每次都要重新编译,从而降低性能。支持脚本编译不需要脚本引擎。如果支持脚本编译,它必须实现Compilable接口。
可调用的接口
可选地,Invocable接口可以由脚本引擎实现,该脚本引擎可以允许调用先前已经编译过的脚本中的过程、函数和方法。
绑定接口和简单绑定类
实现Bindings接口的类的一个实例是一个键-值对的映射,有一个限制,即一个键必须是非 null、非空的String。它扩展了java.util.Map接口。SimpleBindings类是Bindings接口的一个实现。
ScriptContext 接口和 SimpleScriptContext 类
接口的一个实例充当 Java 主机应用程序和脚本引擎之间的桥梁。它用于将 Java 主机应用程序的执行上下文传递给脚本引擎。脚本引擎可以在执行脚本时使用上下文信息。脚本引擎可以将其状态存储在实现ScriptContext接口的类的实例中,Java 主机应用程序可以访问该接口。
SimpleScriptContext类是ScriptContext接口的一个实现。
ScriptException 类
ScriptException类是一个异常类。如果在脚本的执行、编译或调用过程中出现错误,脚本引擎会抛出一个ScriptException。该类包含三个有用的方法,分别叫做getLineNumber()、getColumnNumber()和getFileName()。这些方法报告发生错误的脚本的行号、列号和文件名。ScriptException类覆盖了Throwable类的getMessage()方法,并在它返回的消息中包含行号、列号和文件名。
发现和实例化脚本引擎
您可以使用ScriptEngineFactory或ScriptEngineManager创建脚本引擎。谁真正负责创建一个脚本引擎:ScriptEngineFactory、ScriptEngineManager,或者两者都有?简单的回答是,ScriptEngineFactory总是负责创建脚本引擎的实例。下一个问题是“a ScriptEngineManager的作用是什么?”
A ScriptEngineManager使用服务提供者机制来定位所有可用的脚本引擎工厂。服务提供者机制已经在本书的第七章中讨论过了。
一个ScriptEngineManager定位并实例化所有可用的ScriptEngineFactory类。您可以使用ScriptEngineManager类的getEngineFactories()方法获得所有工厂类的实例列表。当您调用管理器的一个方法来获得一个基于某个标准的脚本引擎时,例如通过名称获得引擎的getEngineByName(String shortName)方法,管理器搜索该标准的所有工厂并返回匹配的脚本引擎引用。如果没有工厂能够提供匹配的引擎,经理返回null。请参考清单 10-3 了解更多关于列出所有可用工厂和描述它们可以创建的脚本引擎的详细信息。
现在你知道了ScriptEngineManager并不创建脚本引擎的实例。相反,它查询所有可用的工厂,并将工厂创建的脚本引擎的引用传递回调用者。
为了使讨论完整,让我们添加一个创建脚本引擎的方法。您可以通过三种方式创建脚本引擎的实例:
-
直接实例化脚本引擎类。
-
直接实例化脚本引擎工厂类,调用其
getScriptEngine()方法。 -
使用
ScriptEngineManager类的getEngineByXxx()方法之一。
建议使用ScriptEngineManager类来获取脚本引擎的实例。这个方法允许由同一个管理器创建的所有引擎共享一个状态,这个状态是作为Bindings接口的一个实例存储的一组键-值对。ScriptEngineManager实例存储这个状态。使用此方法还会使您的代码不知道实际的脚本引擎/工厂实现类。
Note
一个应用程序中可能有多个ScriptEngineManager类的实例。在这种情况下,每个ScriptEngineManager实例维护一个它创建的所有引擎共有的状态。也就是说,如果两个引擎是由ScriptEngineManager类的两个不同实例获得的,那么这些引擎将不会共享由它们的管理器维护的一个公共状态,除非您以编程方式实现这一点。
执行脚本
一个ScriptEngine可以执行一个String和一个Reader中的脚本。使用Reader,您可以执行存储在网络或文件中的脚本。ScriptEngine的eval()方法的以下版本之一用于执行脚本:
-
Object eval(String script) -
Object eval(Reader reader) -
Object eval(String script, Bindings bindings) -
Object eval(Reader reader, Bindings bindings) -
Object eval(String script, ScriptContext context) -
Object eval(Reader reader, ScriptContext context)
eval()方法的第一个参数是脚本的源。第二个参数允许您将信息从宿主应用程序传递到脚本引擎,这些信息可以在脚本执行期间使用。
在清单 10-2 中,您看到了如何使用第一个版本的eval()方法使用String来执行脚本。在本节中,您将把您的脚本存储在一个文件中,并使用一个Reader对象作为脚本的源,它将使用第二个版本的eval()方法。下一节将讨论eval()方法的其他四个版本。通常,脚本文件会被赋予一个.js扩展名。
清单 10-5 显示了名为helloscript.groovy的文件的内容。它在 Groovy 中只包含一个在标准输出中打印消息的语句。
// Print a message
println('Hello from Groovy!')
Listing 10-5The Contents of the helloscript.groovy File
清单 10-6 有执行保存在helloscript.groovy文件中脚本的 Java 程序,该文件应该保存在当前目录下的scripts子目录中。如果没有找到脚本文件,程序会在需要的地方打印出helloscript.js文件的完整路径。如果您在执行脚本文件时遇到问题,请尝试在main()方法中使用绝对路径,例如 Windows 上的C:\scripts\helloscript.js,假设helloscript.js文件保存在C:\scripts目录中。本章示例中使用的所有脚本都在源代码中的Java9APIsAndModules\scripts目录下提供。
// ReaderAsSource.java
package com.jdojo.script;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class ReaderAsSource {
public static void main(String[] args) {
// Construct the script file path
String scriptFileName =
"scripts/helloscript.groovy";
Path scriptPath = Paths.get(scriptFileName);
// Make sure the script file exists. If not,
// print the full path of the script file and
// terminate the program.
if (!Files.exists(scriptPath)) {
System.out.println(
scriptPath.toAbsolutePath() +
" does not exist.");
return;
}
// Get the Groovy script engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
try {
// Get a Reader for the script file
Reader scriptReader = Files.newBufferedReader(
scriptPath);
// Execute the script in the file
engine.eval(scriptReader);
} catch (IOException | ScriptException e) {
e.printStackTrace();
}
}
}
Hello from Groovy!
Listing 10-6Executing a Script Stored in a File
在实际应用程序中,您应该将所有脚本存储在允许修改脚本而无需修改和重新编译 Java 代码的文件中。在本章的大部分例子中,你不会遵循这个规则;您将把您的脚本存储在String对象中,以保持代码简短。
传递参数
Java 脚本 API 允许您将参数从主机环境(Java 应用程序)传递到脚本引擎,反之亦然。在本节中,您将看到宿主应用程序和脚本引擎之间的参数传递机制的技术细节。
从 Java 代码向脚本传递参数
Java 程序可以向脚本传递参数。Java 程序也可以在脚本执行后访问脚本中声明的全局变量。让我们讨论一个简单的例子,Java 程序向脚本传递一个参数。考虑清单 10-7 中向脚本传递参数的程序。
// PassingParam.java
package com.jdojo.script;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class PassingParam {
public static void main(String[] args) {
// Get the Groovy engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Store the script in a String. Here, msg is a
// variable that we have not declared in the script
String script = "println(msg)";
try {
// Store a parameter named msg in the engine
engine.put("msg",
"Hello from the Java program");
// Execute the script
engine.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Hello from the Java program
Listing 10-7Passing Parameters from a Java Program to Scripts
程序在String中存储一个脚本,如下所示:
// Store the script in a String
String script = "println(msg)";
在语句中,脚本引擎将执行的脚本是
println(msg)
注意msg是在println()函数调用中使用的变量。脚本没有声明msg变量,也没有给它赋值。如果您试图在不告诉引擎什么是msg变量的情况下执行这个脚本,引擎将抛出一个异常,声明它不理解名为msg的变量的含义。这就是将参数从 Java 程序传递到脚本引擎的概念发挥作用的地方。
可以通过几种方式将参数传递给脚本引擎。最简单的方法是使用脚本引擎的put(String paramName, Object paramValue)方法,它接受两个参数:
-
第一个参数是参数的名称,它需要与脚本中变量的名称相匹配。
-
第二个参数是参数的值。
在您的例子中,您希望将一个名为msg的参数传递给脚本引擎,它的值是一个String。调用put()的方法是
// Store the value of the msg parameter in the engine
engine.put("msg", "Hello from Java program");
注意,在调用eval()方法之前,必须先调用引擎的put()方法。在您的例子中,当引擎试图执行print(msg)时,它将使用您传递给引擎的msg参数的值。
大多数脚本引擎允许您使用传递给它的参数名作为脚本中的变量名。当您传递名为msg的参数值并在清单 10-7 的脚本中将它用作变量名时,您看到了这种例子。脚本引擎可能要求在脚本中声明变量,例如,PHP 中的变量名必须以前缀$开头,JRuby 中的全局变量名必须包含前缀$。如果您想将名为msg的参数传递给 JRuby 中的脚本,您的代码如下所示:
// Get the JRuby script engine
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("jruby");
// Must use the $ prefix in JRuby script
String script = "puts($msg)";
// No $ prefix used in passing the msg parameter to the
// JRuby engine
engine.put("msg", "Hello from Java");
// Execute the script
engine.eval(script);
传递给脚本的 Java 对象的属性和方法可以在脚本中访问,就像在 Java 代码中访问一样。不同的脚本语言使用不同的语法来访问脚本中的 Java 对象。例如,您可以在清单 10-7 所示的示例中使用表达式msg.toString(),输出将是相同的。在这种情况下,您正在调用变量msg的toString()方法。将清单 10-7 中赋值给script变量的语句改为如下,并运行程序,程序将产生相同的输出:
String script = "println(msg.toString())";
从脚本向 Java 代码传递参数
脚本引擎可以使其全局范围内的变量对 Java 代码可用。ScriptEngine的get(String variableName)方法用于访问 Java 代码中的那些变量。它返回一个 Java Object。全局变量的声明依赖于脚本语言。以下代码片段声明了一个全局变量,并在 Groovy 中为其赋值:
// Declare a variable named year in Groovy
// Note the missing of the 'def' in front of it. If you
// don't prepend 'def', Groovy puts the variable in a
// script-wide global scope.
year = 1969;
清单 10-8 包含了一个程序,展示了如何从 Java 代码访问 Groovy 中的一个全局变量。
// AccessingScriptVariable.java
package com.jdojo.script;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class AccessingScriptVariable {
public static void main(String[] args) {
// Get the Groovy engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Write a script that declares a global variable
// named year and assign it a value of 1969.
String script = "year = 1969";
try {
// Execute the script
engine.eval(script);
// Get the year global variable from the
// engine
Object year = engine.get("year");
// Print the class name and the value of the
// variable year
System.out.println("year's class: " +
year.getClass().getName());
System.out.println("year's value: " +
year);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
year's class: java.lang.Integer
year's value: 1969
Listing 10-8Accessing Script Global Variables in Java Code
程序在脚本中声明了一个全局变量year,并给它赋值1969,如下所示:
String script = "year = 1969";
当脚本执行时,引擎将year变量添加到它的状态中。在 Java 代码中,引擎的get()方法用于检索year变量的值,如下所示:
Object year = engine.get("year");
当脚本中声明了year变量时,您没有指定它的数据类型。脚本变量值到适当 Java 对象的转换是自动执行的。在这种情况下,值1969被评估为一个Integer。
高级参数传递技术
为了理解参数传递机制的细节,必须清楚地理解三个术语:绑定、范围和上下文。这些术语起初令人困惑。本节使用以下步骤解释参数传递机制:
-
首先,它定义了这些术语。
-
其次,它定义了这些术语之间的关系。
-
第三,它解释了如何在 Java 代码中使用它们。
粘合剂
一个Bindings是一组键-值对,其中所有键必须是非空的、非空字符串。在 Java 代码中,Bindings是Bindings接口的一个实例。SimpleBindings类是Bindings接口的一个实现。脚本引擎可以提供自己的Bindings接口实现。
Note
如果你熟悉java.util.Map界面,就很容易理解Bindings。Bindings接口继承自Map<String,Object>接口。因此,Bindings只是一个Map,它的键必须是非空的非空字符串。
清单 10-9 展示了如何使用Bindings。它创建一个SimpleBindings的实例,添加一些键值对,检索键值,删除键值对,等等。Bindings接口的get()方法返回null,如果键不存在或者键存在且其值为null。如果你想测试一个键是否存在,你需要调用它的contains()方法。
// BindingsTest.java
package com.jdojo.script;
import javax.script.Bindings;
import javax.script.SimpleBindings;
public class BindingsTest {
public static void main(String[] args) {
// Create a Bindings instance
Bindings params = new SimpleBindings();
// Add some key-value pairs
params.put("msg", "Hello");
params.put("year", 1969);
// Get values
Object msg = params.get("msg");
Object year = params.get("year");
System.out.println("msg = " + msg);
System.out.println("year = " + year);
// Remove year from Bindings
params.remove("year");
year = params.get("year");
boolean containsYear = params.containsKey("year");
System.out.println("year = " + year);
System.out.println("params contains year = " +
containsYear);
}
}
msg = Hello
year = 1969
year = null
params contains year = false
Listing 10-9Using Bindings Objects
你不能单独使用一个Bindings。通常,您会使用它将参数从 Java 代码传递到脚本引擎。ScriptEngine接口包含一个返回Bindings接口实例的createBindings()方法。这个方法给脚本引擎一个机会来返回一个Bindings接口的特殊实现的实例。您可以使用如下所示的方法:
// Get the Groovy engine
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Do not instantiate SimpleBindings class directly.
// Use the createBindings() method of the engine to create
// a Bindings.
Bindings params = engine.createBindings();
// Work with params as usual
范围
让我们转到下一个术语,即范围。范围用于绑定。绑定的范围决定了它的键值对的可见性。在多个作用域中可以有多个绑定。但是,一个绑定只能出现在一个范围内。如何指定绑定的范围?我很快会谈到这一点。
使用Bindings的作用域可以让您按照层次顺序为脚本引擎定义参数变量。如果在引擎状态下搜索变量名,首先搜索优先级较高的Bindings,然后是优先级较低的Bindings。返回找到的第一个变量的值。Java 脚本 API 定义了两个范围。它们在ScriptContext接口中被定义为两个int常量。他们是
-
ScriptContext.ENGINE_SCOPE -
ScriptContext.GLOBAL_SCOPE
引擎范围的优先级高于全局范围。如果向两个Bindings添加具有相同键的两个键-值对,一个在引擎范围内,一个在全局范围内,那么每当需要解析与键同名的变量时,都会使用引擎范围内的键-值对。
理解作用域对于一个Bindings的作用是如此重要,以至于我通过另一个类比来解释它。考虑一个有两组变量的 Java 类:一组包含类中的所有实例变量,另一组包含方法中的所有局部变量。这两组变量及其值是两个Bindings。这些Bindings中的变量类型定义了作用域。为了便于讨论,我定义了两个范围:实例范围和本地范围。当执行一个方法时,首先在局部范围Bindings中查找变量名,因为局部变量优先于实例变量。如果在本地作用域Bindings中没有找到变量名,就在实例作用域Bindings中查找。当一个脚本被执行时,Bindings和它们的作用域扮演着相似的角色。
定义脚本上下文
脚本引擎在上下文中执行脚本。您可以将上下文视为脚本执行的环境。Java 宿主应用程序为脚本引擎提供了两样东西:脚本和脚本需要执行的上下文。接口的一个实例代表一个脚本的上下文。SimpleScriptContext类是ScriptContext接口的一个实现。脚本上下文由四部分组成:
-
一组
Bindings,其中每个Bindings与一个不同的作用域相关联 -
脚本引擎用来读取输入的
Reader -
脚本引擎用来写输出的一个
Writer -
脚本引擎用来写入错误输出的错误
Writer
上下文中的一组Bindings用于向脚本传递参数。上下文中的读取器和写入器分别控制脚本的输入源和输出目的地。例如,通过将文件编写器设置为编写器,可以将脚本的所有输出发送到文件。
每个脚本引擎都维护一个默认的脚本上下文,用于执行脚本。到目前为止,您已经在没有提供脚本上下文的情况下执行了几个脚本。在这些情况下,脚本引擎使用它们的默认脚本上下文来执行脚本。在这一节中,我将介绍如何单独使用ScriptContext。在下一节中,我将介绍在脚本执行期间如何将一个ScriptContext传递给一个ScriptEngine。
您可以使用SimpleScriptContext类创建一个ScriptContext接口的实例:
// Create a script context
ScriptContext ctx = new SimpleScriptContext();
一个SimpleScriptContext类的实例维护两个Bindings实例:一个用于引擎范围,一个用于全局范围。当您创建SimpleScriptContext的实例时,就会创建引擎范围内的Bindings。要使用全局范围Bindings,您需要创建一个Bindings接口的实例。
默认情况下,SimpleScriptContext类将上下文的输入读取器、输出写入器和错误写入器分别初始化为标准输入System.in、标准输出System.out和标准错误输出System.err。您可以使用ScriptContext接口的getReader()、getWriter()和getErrorWriter()方法分别从ScriptContext中获取阅读器、编写器和错误编写器的引用。还提供了 Setter 方法来设置读取器和编写器。下面的代码片段显示了如何获取阅读器和编写器。它还展示了如何将 writer 设置为FileWriter以将脚本输出写入文件:
// Get the reader and writers from the script context
Reader inputReader = ctx.getReader();
Writer outputWriter = ctx.getWriter();
Writer errWriter = ctx.getErrorWriter();
// Write all script outputs to an out.txt file
Writer fileWriter = new FileWriter("out.txt");
ctx.setWriter(fileWriter);
在创建了SimpleScriptContext之后,您可以开始在引擎范围Bindings中存储键值对,因为当您创建SimpleScriptContext对象时,在引擎范围中创建了一个空的Bindings。setAttribute()方法用于向Bindings添加一个键值对。您必须为Bindings提供键名、值和范围。以下代码片段添加了三个键值对:
// Add three key-value pairs to the engine scope bindings
ctx.setAttribute("year", 1969, ScriptContext.ENGINE_SCOPE);
ctx.setAttribute("month", 9, ScriptContext.ENGINE_SCOPE);
ctx.setAttribute("day", 19, ScriptContext.ENGINE_SCOPE);
如果您想在全局范围内将键值对添加到Bindings中,您需要首先创建并设置Bindings,如下所示:
// Add a global scope Bindings to the context
Bindings globalBindings = new SimpleBindings();
ctx.setBindings(globalBindings,
ScriptContext.GLOBAL_SCOPE);
现在,您可以使用setAttribute()方法在全局范围内向Bindings添加键值对,如下所示:
// Add two key-value pairs to the global scope bindings
ctx.setAttribute("year", 1982,
ScriptContext.GLOBAL_SCOPE);
ctx.setAttribute("name", "Boni",
ScriptContext.GLOBAL_SCOPE);
此时,您可以可视化ScriptContext实例的状态,如图 10-1 所示。
图 10-1
SimpleScriptContext 类实例的图示视图
您可以在ScriptContext上执行多项操作。您可以使用setAttribute(String name, Object value, int scope)方法为已存储的密钥设置不同的值。对于指定的键和范围,可以使用removeAttribute(String name, int scope)方法移除键-值对。您可以使用getAttribute(String name, int scope)方法获得指定范围内的键值。
使用ScriptContext可以做的最有趣的事情是检索一个键值,而不用使用它的getAttribute(String name)方法指定它的作用域。一个ScriptContext首先在引擎范围Bindings中搜索关键字。如果在引擎范围内没有找到,则在全局范围内搜索Bindings。如果在这些范围中找到该键,则返回首先找到该键的范围中的相应值。如果两个范围都不包含该键,则返回null。
在您的示例中,您已经在引擎范围和全局范围中存储了名为year的键。当首先搜索引擎范围时,下面的代码片段从引擎范围返回关键字year的1969。getAttribute()方法的返回类型是Object:
// Get the value of the key year without specifying the
// scope. It returns 1969 from the Bindings in the engine
// scope.
int yearValue = (Integer) ctx.getAttribute("year");
您只在全局范围内存储了名为name的键。如果尝试检索其值,将首先搜索引擎范围,这不会返回匹配项。随后,搜索全局范围,并返回值"Boni",如下所示:
// Get the value of the key named name without specifying
// the scope.
// It returns "Boni" from the Bindings in the global scope.
String nameValue = (String) ctx.getAttribute("name");
您还可以检索特定范围内的键值。以下代码片段从引擎范围和全局范围中检索关键字"year"的值:
// Assigns 1969 to engineScopeYear and 1982 to
// globalScopeYear
int engineScopeYear = (Integer) ctx.getAttribute(
"year", ScriptContext.ENGINE_SCOPE);
int globalScopeYear = (Integer) ctx.getAttribute(
"year", ScriptContext.GLOBAL_SCOPE);
Note
Java 脚本 API 只定义了两个作用域:引擎和全局。ScriptContext接口的子接口可以定义额外的作用域。ScriptContext接口的getScopes()方法返回一个支持范围的列表作为List<Integer>。请注意,作用域表示为整数。在ScriptContext界面中的两个常量ENGINE_SCOPE和GLOBAL_SCOPE分别被赋值为 100 和 200。当在出现在多个范围中的多个Bindings中搜索一个键时,首先搜索具有较小整数值的范围。因为引擎范围的值 100 小于全局范围的值 200,所以当您不指定范围时,首先在引擎范围中搜索一个键。
清单 10-10 展示了如何使用实现ScriptContext接口的类的实例。请注意,您不能在应用程序中单独使用ScriptContext。它由脚本引擎在脚本执行期间使用。最常见的是,你通过一个ScriptEngine和一个ScriptEngineManager间接地操纵一个ScriptContext,这将在下一节详细讨论。
// ScriptContextTest.java
package com.jdojo.script;
import java.util.List;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.SimpleBindings;
import javax.script.SimpleScriptContext;
import static javax.script.ScriptContext.ENGINE_SCOPE;
import static javax.script.ScriptContext.GLOBAL_SCOPE;
public class ScriptContextTest {
public static void main(String[] args) {
// Create a script context
ScriptContext ctx = new SimpleScriptContext();
// Get the list of scopes supported by the script
// context
List<Integer> scopes = ctx.getScopes();
System.out.println("Supported Scopes: " + scopes);
// Add three key-value pairs to the engine scope
// bindings
ctx.setAttribute("year", 1969, ENGINE_SCOPE);
ctx.setAttribute("month", 9, ENGINE_SCOPE);
ctx.setAttribute("day", 19, ENGINE_SCOPE);
// Add a global scope Bindings to the context
Bindings globalBindings = new SimpleBindings();
ctx.setBindings(globalBindings, GLOBAL_SCOPE);
// Add two key-value pairs to the global scope
// bindings
ctx.setAttribute("year", 1982, GLOBAL_SCOPE);
ctx.setAttribute("name", "Boni", GLOBAL_SCOPE);
// Get the value of year without specifying the
// scope
int yearValue =
(Integer) ctx.getAttribute("year");
System.out.println("yearValue = " + yearValue);
// Get the value of name
String nameValue =
(String) ctx.getAttribute("name");
System.out.println("nameValue = " + nameValue);
// Get the value of year from engine and global
// scopes
int engineScopeYear = (Integer) ctx.
getAttribute("year", ENGINE_SCOPE);
int globalScopeYear = (Integer) ctx.
getAttribute("year", GLOBAL_SCOPE);
System.out.println("engineScopeYear = " +
engineScopeYear);
System.out.println("globalScopeYear = " +
globalScopeYear);
}
}
Supported Scopes: [100, 200]
yearValue = 1969
nameValue = Boni
engineScopeYear = 1969
globalScopeYear = 1982
Listing 10-10Using an Instance of the ScriptContext Interface
把它们放在一起
在这一节中,我将向您展示Bindings的实例及其作用域、ScriptContext、ScriptEngine、ScriptEngineManager和宿主应用程序是如何协同工作的。重点是如何使用一个ScriptEngine和一个ScriptEngineManager在不同的范围内操作存储在Bindings中的键值对。
一个ScriptEngineManager在一个Bindings中维护一组键值对。它允许您使用以下方法处理这些键值对:
-
void put(String key, Object value) -
Object get(String key) -
void setBindings(Bindings bindings) -
Bindings getBindings()
put()方法向Bindings添加一个键值对。get()方法返回指定键的值;如果没有找到密钥,它返回null。使用setBindings()方法可以替换发动机管理器的Bindings。getBindings()方法返回ScriptEngineManager的Bindings的引用。
默认情况下,每个ScriptEngine都有一个被称为默认上下文的ScriptContext。回想一下,除了读者和作者,一个ScriptContext有两个Bindings:一个在引擎范围内,一个在全局范围内。当一个ScriptEngine被创建时,它的引擎作用域Bindings为空,它的全局作用域Bindings引用创建它的ScriptEngineManager的Bindings。
默认情况下,由ScriptEngineManager创建的ScriptEngine的所有实例共享ScriptEngineManager的Bindings。在同一个 Java 应用程序中可能有多个ScriptEngineManager实例。在这种情况下,由同一个ScriptEngineManager创建的ScriptEngine的所有实例共享ScriptEngineManager的Bindings作为它们默认上下文的全局作用域Bindings。
下面的代码片段创建了一个ScriptEngineManager,用于创建ScriptEngine的三个实例:
// Create a ScriptEngineManager
ScriptEngineManager manager = new ScriptEngineManager();
// Create three ScriptEngines using the same
// ScriptEngineManager
ScriptEngine engine1 = manager.getEngineByName(
"Groovy");
ScriptEngine engine2 = manager.getEngineByName(
"Groovy");
ScriptEngine engine3 = manager.getEngineByName(
"Groovy");
现在,让我们给ScriptEngineManager的Bindings添加三个键值对,给每个ScriptEngine的引擎范围Bindings添加两个键值对:
// Add three key-value pairs to the Bindings
// of the manager
manager.put("K1", "V1");
manager.put("K2", "V2");
manager.put("K3", "V3");
// Add two key-value pairs to each engine
engine1.put("KE11", "VE11");
engine1.put("KE12", "VE12");
engine2.put("KE21", "VE21");
engine2.put("KE22", "VE22");
engine3.put("KE31", "VE31");
engine3.put("KE32", "VE32");
图 10-2 显示了前一段代码执行后ScriptEngineManager和三个ScriptEngine的状态。从图中可以明显看出,所有ScriptEngine的默认上下文共享ScriptEngineManager的Bindings作为它们的全局作用域Bindings。
图 10-2
由 ScriptEngineManager 创建的三个 ScriptEngines 的图示视图
ScriptEngineManager中的Bindings可以通过以下方式修改:
-
通过使用
ScriptEngineManager的put()方法 -
通过使用
ScriptEngineManager的getBindings()方法获取Bindings的引用,然后在Bindings上使用put()和remove()方法 -
通过使用
getBindings()方法在ScriptEngine的默认上下文的全局范围内获取Bindings的引用,然后在Bindings上使用put()和remove()方法
当一个ScriptEngineManager中的Bindings被修改时,由这个ScriptEngineManager创建的所有ScriptEngine的默认上下文中的全局作用域Bindings被修改,因为它们共享同一个Bindings。
每个ScriptEngine的默认上下文分别维护一个引擎范围Bindings。要将一个键-值对添加到一个ScriptEngine的引擎作用域Bindings,使用它的put()方法,如下所示:
ScriptEngine engine1 = null; // get an engine
// Add an "engineName" key with its value as "Engine-1"
// to the engine scope Bindings of the default context
// of engine1
engine1.put("engineName", "Engine-1");
ScriptEngine的get(String key)方法从其引擎作用域Bindings返回指定的key的值。下面的语句返回"Engine-1",它是engineName键的值:
String eName = (String) engine1.get("engineName");
在默认的ScriptEngine上下文中,获得全局作用域Bindings的键值对需要两个步骤。首先,您需要使用它的getBindings()方法获取全局作用域Bindings的引用,如下所示:
Bindings e1Global =
engine1.getBindings(ScriptContext.GLOBAL_SCOPE);
现在,您可以使用e1Global引用来修改引擎的全局范围Bindings。下面的语句向e1Global Bindings添加了一个键值对:
e1Global.put("id", 89999);
因为所有的ScriptEngine共享一个ScriptEngine的全局作用域Bindings,这段代码将把键id及其值添加到所有ScriptEngine的默认上下文的全局作用域Bindings中,这些默认上下文是由创建engine1的同一ScriptEngineManager创建的。不建议使用之前的代码修改ScriptEngineManager中的Bindings。您应该改为使用ScriptEngineManager引用来修改Bindings,这使得代码的读者可以更清楚地理解逻辑。
清单 10-11 展示了本节讨论的概念。
ScriptEngineManager
// GlobalBindings.java
package com.jdojo.script;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class GlobalBindings {
public static void main(String[] args) {
ScriptEngineManager manager =
new ScriptEngineManager();
// Add two numbers to the Bindings of the
// manager - shared by all its engines
manager.put("n1", 100);
manager.put("n2", 200);
// Create two JavaScript engines and add the name
// of the engine in the engine scope of the default
// context of the engines
ScriptEngine engine1 = manager.getEngineByName(
"Groovy");
engine1.put("engineName", "Engine-1");
ScriptEngine engine2 = manager.getEngineByName(
"Groovy");
engine2.put("engineName", "Engine-2");
// Execute a script that adds two numbers and
// prints the result
String script = """
def sum = n1 + n2
println(engineName + ' - Sum = ' + sum)
""";
try {
// Execute the script in two engines
engine1.eval(script);
engine2.eval(script);
// Now add a different value for n2 for each
// engine
engine1.put("n2", 1000);
engine2.put("n2", 2000);
// Execute the script in two engines again
engine1.eval(script);
engine2.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Engine-1 - Sum = 300
Engine-2 - Sum = 300
Engine-1 - Sum = 1100
Engine-2 - Sum = 2100
Listing 10-11Using Global and Engine Scope Bindings of Engines Created by the Same
A ScriptEngineManager向它的Bindings添加两个键-值对,键为n1和n2。创造了两个ScriptEngine;他们在引擎范围Bindings中添加了一个名为engineName的键。当脚本被执行时,脚本中的engineName变量的值从ScriptEngine的引擎范围中被使用。脚本中变量n1和n2的值是从ScriptEngine的全局作用域Bindings中获取的。在第一次执行该脚本后,每个ScriptEngine向它们的引擎范围Bindings添加一个名为n2的键,该键具有不同的值。当您第二次执行脚本时,变量n1的值从引擎的全局作用域Bindings中检索,而变量n2的值从引擎作用域Bindings中检索,如输出所示。
由一个ScriptEngineManager创建的所有ScriptEngines共享的全局范围Bindings的故事还没有结束。这是最复杂、最令人困惑的事情!现在重点将放在使用ScriptEngineManager类的setBindings()方法和ScriptEngine接口的效果上。考虑以下代码片段:
// Create a ScriptEngineManager and two ScriptEngines
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine1 = manager.getEngineByName(
"Groovy");
ScriptEngine engine2 = manager.getEngineByName(
"Groovy");
// Add two key-value pairs to the manager
manager.put("n1", 100);
manager.put("n2", 200);
图 10-3 显示了该脚本执行后引擎管理器及其引擎的状态。此时,ScriptEngineManager中只存储了一个Bindings,两个ScriptEngine正在将其作为自己的全局作用域Bindings进行引用。
图 10-3
ScriptEngineManager 和两个 ScriptEngines 的初始状态
让我们创建一个新的Bindings,并使用setBindings()方法将其设置为ScriptEngineManager的Bindings,如下所示:
// Create a Bindings, add two key-value pairs to it, and
// set it as the new Bindings for the manager
Bindings newGlobal = new SimpleBindings();
newGlobal.put("n3", 300);
newGlobal.put("n4", 400);
manager.setBindings(newGlobal);
图 10-4 显示了前一段代码执行后ScriptEngineManager和两个ScriptEngine的状态。请注意,ScriptEngineManager有了新的Bindings,而两个ScriptEngine仍然将旧的Bindings称为它们的全球范围Bindings。
图 10-4
设置新绑定后 ScriptEngineManager 和两个 ScriptEngines 的状态
此时,对ScriptEngineManager的Bindings所做的任何更改都不会反映在两个ScriptEngine的全局作用域Bindings中
您仍然可以对两个ScriptEngine共享的Bindings进行更改,两个ScriptEngine都将看到其中一个所做的更改。
让我们创建一个新的ScriptEngine,如图所示:
// Create a new ScriptEngine
ScriptEngine engine3 = manager.getEngineByName(
"Groovy");
回想一下,ScriptEngine在创建时获得了全局作用域Bindings,Bindings与ScriptEngineManager的Bindings相同。前一条语句执行后,ScriptEngineManager和三个ScriptEngine的状态如图 10-5 所示。
图 10-5
创建第三个 ScriptEngine 后 ScriptEngineManager 和三个 script engine 的状态
这里是对所谓的ScriptEngine s 的全局范围的“全球性”的另一种扭曲。这一次,您将使用一个ScriptEngine的setBindings()方法来设置它的全局范围Bindings:
// Set a new Bindings for the global scope of engine1
Bindings newGlobalEngine1 = new SimpleBindings();
newGlobalEngine1.put("n5", 500);
newGlobalEngine1.put("n6", 600);
engine1.setBindings(newGlobalEngine1,
ScriptContext.GLOBAL_SCOPE);
图 10-6 显示了前一段代码执行后ScriptEngineManager和三个脚本引擎的状态。
图 10-6
设置新的全局范围绑定后 ScriptEngineManager 和三个 ScriptEngines 的状态
Note
默认情况下,a ScriptEngineManager创建的所有ScriptEngine共享其Bindings作为它们的全局作用域Bindings。如果你使用一个ScriptEngine的setBindings()方法来设置它的全局作用域Bindings,或者如果你使用一个ScriptEngineManager的setBindings()方法来设置它的Bindings,你就打破了“全局”链,如本节所讨论的。为了保持“全局”链的完整性,您应该总是使用ScriptEngineManager的put()方法将键值对添加到它的Bindings中。要从由ScriptEngineManager创建的所有ScriptEngine的全局范围中删除一个键值对,您需要使用ScriptEngineManager的getBindings()方法获取Bindings的引用,并在Bindings上使用remove()方法。
使用自定义脚本上下文
在上一节中,您看到每个ScriptEngine都有一个默认的脚本上下文。ScriptEngine的get()、put()、getBindings()和setBindings()方法在默认ScriptContext下运行。当ScriptEngine的eval()方法没有指定ScriptContext时,使用引擎的默认上下文。ScriptEngine的eval()方法的以下两个版本使用其默认上下文来执行脚本:
-
Object eval(String script) -
Object eval(Reader reader)
您可以将一个Bindings传递给下面两个版本的eval()方法:
-
Object eval(String script, Bindings bindings) -
Object eval(Reader reader, Bindings bindings)
这些版本的eval()方法不使用默认的ScriptEngine上下文。他们使用一个新的ScriptContext,其引擎范围Bindings是传递给这些方法的那个,全局范围Bindings与引擎的默认上下文相同。注意,eval()方法的这两个版本保持了ScriptEngine的默认上下文不变。
您可以将一个ScriptContext传递给下面两个版本的eval()方法:
-
Object eval(String script, ScriptContext context) -
Object eval(Reader reader, ScriptContext context)
这些版本的eval()方法使用指定的上下文来执行脚本。它们保持ScriptEngine的默认上下文不变。
三组eval()方法允许您使用不同的隔离级别执行脚本:
-
第一组让所有脚本共享默认上下文。
-
第二组让脚本使用不同的引擎作用域
Bindings并共享全局作用域Bindings。 -
第三组让脚本在隔离的
ScriptContext中执行。
清单 10-12 展示了如何使用不同版本的eval()方法在不同的隔离级别执行脚本。
// CustomContext.java
package com.jdojo.script;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import static javax.script.SimpleScriptContext.
ENGINE_SCOPE;
import static javax.script.SimpleScriptContext.
GLOBAL_SCOPE;
public class CustomContext {
public static void
main(String[] args) throws ScriptException {
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Add n1 to Bindings of the manager, which will
// be shared by all engines as their global scope
// Bindings
manager.put("n1", 100);
// Prepare the script
String script = """
def sum = n1 + n2
println(msg + ' n1=' + n1 + ', n2=' + n2 +
', sum=' + sum)
""";
// Add n2 to the engine scope of the default
// context of the engine
engine.put("n2", 200);
engine.put("msg", "Using the default context:");
engine.eval(script);
// Use a Bindings to execute the script
Bindings bindings = engine.createBindings();
bindings.put("n2", 300);
bindings.put("msg", "Using a Bindings:");
engine.eval(script, bindings);
// Use a ScriptContext to execute the script
ScriptContext ctx = new SimpleScriptContext();
Bindings ctxGlobalBindings =
engine.createBindings();
ctx.setBindings(ctxGlobalBindings, GLOBAL_SCOPE);
ctx.setAttribute("n1", 400, GLOBAL_SCOPE);
ctx.setAttribute("n2", 500, ENGINE_SCOPE);
ctx.setAttribute("msg", "Using a ScriptContext:",
ENGINE_SCOPE);
engine.eval(script, ctx);
// Execute the script again using the default
// context to prove that the default context is
// unaffected.
engine.eval(script);
}
}
Using the default context: n1=100, n2=200, sum=300
Using a Bindings: n1=100, n2=300, sum=400
Using a ScriptContext: n1=400, n2=500, sum=900
Using the default context: n1=100, n2=200, sum=300
Listing 10-12Using Different Isolation Levels for Executing Scripts
该程序使用三个变量,称为msg、n1和n2。它显示存储在msg变量中的值。将n1和n2的值相加,并显示总和。该脚本打印出在计算总和时使用了什么值的n1和n2。n1的值存储在由所有ScriptEngine的默认上下文共享的ScriptEngineManager的Bindings中。n2的值存储在默认上下文和自定义上下文的引擎范围中。该脚本使用引擎的默认上下文执行两次,一次在开始,一次在结束,以证明在eval()方法中使用自定义Bindings或ScriptContext不会影响ScriptEngine的默认上下文中的Bindings。该程序在其main()方法中声明了一个throws子句,以使代码更短。
eval()方法的返回值
ScriptEngine的eval()方法返回一个Object,这是脚本中的最后一个值。如果脚本中没有最后一个值,它将返回null。依赖脚本中的最后一个值容易出错,同时也令人困惑。下面的代码片段展示了一些为 Groovy 使用eval()方法返回值的例子。代码中的注释表示从eval()方法返回的值:
Object result = null;
// Assigns 3 to result
result = engine.eval("1 + 2");
// Assigns 7 to result
result = engine.eval("1 + 2; 3 + 4");
// Assigns 6 to result
result = engine.eval("""1 + 2; 3 + 4;
def v = 5; v = 6""");
// Assigns 5 to result
result = engine.eval("""1 + 2; 3 + 4;
def v = 5""");
// Assigns null to result
result = engine.eval("println(1 + 2)");
最好不要依赖于eval()方法的返回值。您应该将一个 Java 对象作为参数传递给脚本,并让脚本将脚本的返回值存储在该对象中。在执行了eval()方法之后,您可以查询这个 Java 对象的返回值。
清单 10-13 包含包装整数的Result类的代码。您将向脚本传递一个Result类的对象,脚本将在其中存储返回值。脚本完成后,您可以在 Java 代码中读取存储在Result对象中的整数值。需要将Result声明为公共的,这样脚本引擎就可以访问它。
// Result.java
package com.jdojo.script;
public class Result {
public int val = -1;
}
Listing 10-13A Result Class That Wraps an Integer
清单 10-14 中的程序展示了如何将一个Result对象传递给一个用值填充Result对象的脚本。该程序在main()方法的声明中包含一个throws子句,以保持代码简短。
// ResultBearingScript.java
package com.jdojo.script;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class ResultBearingScript {
public static void
main(String[] args) throws ScriptException {
// Get the Groovy engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Pass a Result object to the script. The script
// will store the result of the script in the
// result object
Result result = new Result();
engine.put("result", result);
// Store the script in a String
String script = "3 + 4; result.val = 101";
// Execute the script, which uses the passed in
// Result object to return a value
engine.eval(script);
// Use the result object to get the returned value
// from the script
int returnedValue = result.val; // -> 101
System.out.println("Returned value is " +
returnedValue);
}
}
Returned value is 101
Listing 10-14Collecting the Return Value of a Script in a Result Object
引擎范围绑定的保留键
通常,引擎范围Bindings中的一个键代表一个脚本变量。有些键是保留的,它们有特殊的含义。它们的值可以通过引擎的实现传递给引擎。一个实现可以定义附加的保留密钥。
表 10-1 包含所有保留密钥的列表。这些键在ScriptEngine接口中也被声明为常量。脚本引擎的实现不需要在引擎范围绑定中将所有这些键传递给引擎。作为开发人员,您不应该使用这些键将参数从 Java 应用程序传递到脚本引擎。
表 10-1
引擎范围绑定的保留键
|钥匙
|
ScriptEngine 接口中的常数
|
键值的含义
|
| --- | --- | --- |
| "javax.script.argv" | ScriptEngine.ARGV | 用来传递一个数组Object来传递一组位置参数。 |
| "javax.script.engine" | ScriptEngine.ENGINE | 脚本引擎的名称。 |
| "javax.script.engine_version" | ScriptEngine.ENGINE_VERSION | 脚本引擎的版本。 |
| "javax.script.filename" | ScriptEngine.FILENAME | 用于传递作为脚本源的文件或资源的名称。 |
| "javax.script.language" | ScriptEngine.LANGUAGE | 脚本引擎支持的语言的名称。 |
| "javax.script.language_version" | ScriptEngine.LANGUAGE_VERSION | 引擎支持的脚本语言版本。 |
| "javax.script.name" | ScriptEngine.NAME | 脚本语言的简称。 |
更改默认脚本上下文
您可以分别使用getContext()和setContext()方法来获取和设置ScriptEngine的默认上下文,如下所示:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Get the default context of the ScriptEngine
ScriptContext defaultCtx = engine.getContext();
// Work with defaultCtx here
// Create a new context
ScriptContext ctx = new SimpleScriptContext();
// Configure ctx here
// Set ctx as the new default context for the engine
engine.setContext(ctx);
注意,为一个ScriptEngine设置一个新的默认上下文不会使用ScriptEngineManager的Bindings作为它的全局作用域Bindings。如果您希望新的默认上下文使用ScriptEngineManager的Bindings,您需要显式设置它,如下所示:
// Create a new context
ScriptContext ctx = new SimpleScriptContext();
// Set the global scope Bindings for ctx the same as the
// Bindings for the manager
ctx.setBindings(manager.getBindings(),
ScriptContext.GLOBAL_SCOPE);
// Set ctx as the new default context for the engine
engine.setContext(ctx);
将脚本输出发送到文件
您可以自定义脚本执行的输入源、输出目标和错误输出目标。您需要为用于执行脚本的ScriptContext设置适当的读取器和写入器。下面的代码片段将把脚本输出写到当前目录中名为output.txt的文件中:
// Create a FileWriter
FileWriter writer = new FileWriter("output.txt");
// Get the default context of the engine
ScriptContext defaultCtx = engine.getContext();
// Set the output writer for the default context of the
// engine
defaultCtx.setWriter(writer);
该代码为ScriptEngine的默认上下文设置了一个自定义输出编写器,在使用默认上下文的脚本执行过程中将会用到这个编写器。如果您想使用定制的输出编写器来执行特定的脚本,您需要使用一个定制的ScriptContext并设置它的编写器。
Note
为ScriptContext设置自定义输出编写器不会影响 Java 应用程序标准输出的目的地。要重定向 Java 应用程序的标准输出,您需要使用System.setOut()方法。
清单 10-15 向您展示了如何将脚本执行的输出写到名为output.txt的文件中。该程序在标准输出中打印输出文件的完整路径。运行该程序时,您可能会得到不同的输出。您需要在文本编辑器中打开输出文件来查看脚本的输出。
// CustomScriptOutput.java
package com.jdojo.script;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class CustomScriptOutput {
public static void main(String[] args) {
// Get the Groovy engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Print the absolute path of the output file
File outputFile = new File("output.txt");
System.out.println(
"Script output will be written to "
+ outputFile.getAbsolutePath());
try (FileWriter writer =
new FileWriter(outputFile)) {
// Set a custom output writer for the engine
ScriptContext defaultCtx =
engine.getContext();
defaultCtx.setWriter(writer);
// Execute a script
String script =
"println('Hello custom output writer')";
engine.eval(script);
} catch (IOException | ScriptException e) {
e.printStackTrace();
}
}
}
Listing 10-15Writing the Output of Scripts to a File
脚本输出将被写入当前工作目录中的文件output.txt。
在脚本中调用过程
脚本语言可以允许创建过程、函数和方法。Java 脚本 API 允许您从 Java 应用程序中调用这样的过程、函数和方法。在本节中,我使用术语“过程”来表示过程、函数和方法。当讨论的上下文需要时,我使用特定的术语。
并非所有脚本引擎都需要支持过程调用。Groovy 引擎支持过程调用。如果有脚本引擎支持,那么脚本引擎类的实现必须实现Invocable接口。在调用过程之前,检查脚本引擎是否实现了Invocable接口是开发人员的责任。调用过程包括四个步骤:
-
检查脚本引擎是否支持过程调用。
-
将发动机参考转换为
Invocable类型。 -
评估包含该过程源代码的脚本。
-
使用
Invocable接口的invokeFunction()方法调用过程和函数。使用invokeMethod()方法来调用在脚本语言中创建的对象的方法。
以下代码片段检查脚本引擎实现类是否实现了Invocable接口:
// Get the Groovy engine
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Make sure the script engine implements the Invocable
// interface
if (engine instanceof Invocable) {
System.out.println(
"Invoking procedures is supported.");
} else {
System.out.println(
"Invoking procedures is not supported.");
}
第二步是将引擎引用转换为Invocable接口类型:
Invocable inv = (Invocable) engine;
第三步是评估脚本,因此脚本引擎编译并存储过程的编译形式,供以后调用。以下代码片段执行此步骤:
// Declare a function named add that adds two numbers
String script = "def add(n1, n2) { n1 + n2 }";
// Evaluate the function. Call to eval() does not invoke
// the function. It just compiles it.
engine.eval(script);
最后一步是调用过程或函数:
// Invoke the add function with 30 and 40 as the function's
// arguments. It is as if you called add(30, 40) in the
// script.
Object result = inv.invokeFunction("add", 30, 40);
invokeFunction()的第一个参数是过程或函数的名称。第二个参数是 varargs,用于指定过程或函数的参数。invokeFunction()方法返回过程或函数返回的值。
清单 10-16 显示了如何调用一个函数。它调用用 Groovy 编写的函数。
// InvokeFunction.java
package com.jdojo.script;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class InvokeFunction {
public static void main(String[] args) {
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Make sure the script engine implements the
// Invocable interface
if (!(engine instanceof Invocable)) {
System.out.println(
"Invoking procedures is not supported.");
return;
}
// Cast the engine reference to the Invocable type
Invocable inv = (Invocable) engine;
try {
String script =
"def add(n1, n2) { n1 + n2 }";
// Evaluate the script first
engine.eval(script);
// Invoke the add function twice
Object result1 = inv.invokeFunction(
"add", 30, 40);
System.out.println("Result1 = " + result1);
Object result2 = inv.invokeFunction(
"add", 10, 20);
System.out.println("Result2 = " + result2);
} catch (ScriptException |
NoSuchMethodException e) {
e.printStackTrace();
}
}
}
Result1 = 70
Result2 = 30
Listing 10-16Invoking a Function Written in Groovy
面向对象或基于对象的脚本语言可以让您定义对象及其方法。您可以使用Invocable接口的invokeMethod()方法调用这些对象的方法,声明如下:
Object invokeMethod(Object objectRef, String name,
Object... args)
第一个参数是对象的引用,第二个参数是要在对象上调用的方法的名称,第三个参数是 varargs 参数,用于将参数传递给被调用的方法。
清单 10-17 展示了在 Groovy 中创建的对象上的方法调用。注意,该对象是在 Groovy 脚本中创建的。要从 Java 调用对象的方法,需要通过脚本引擎获取对象的引用。该程序评估使用 add()方法创建对象的脚本,并将其引用存储在名为 calculator 的变量中。engine.get("calculator ")方法返回 calculator 对象对 Java 代码的引用。
// InvokeMethod.java
package com.jdojo.script;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class InvokeMethod {
public static void main(String[] args) {
// Get the Groovy engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Make sure the script engine implements the
// Invocable interface
if (!(engine instanceof Invocable)) {
System.out.println(
"Invoking methods is not supported.");
return;
}
// Cast the engine reference to the Invocable type
Invocable inv = (Invocable) engine;
try {
// Declare a global object with an add() method
String script = """
class Calculator {
def add(int n1, int n2){n1 + n2}
}
calculator = new Calculator()
""";
// Evaluate the script first
engine.eval(script);
// Get the calculator object reference created
// in the script
Object calculator = engine.get("calculator");
// Invoke the add() method on the calculator
// object
Object result = inv.invokeMethod(calculator,
"add", 30, 40);
System.out.println("Result = " + result);
} catch (ScriptException |
NoSuchMethodException e) {
e.printStackTrace();
}
}
}
Result = 70
Listing 10-17Invoking a Method on an Object Created in Groovy JavaScript
Note
使用 Invocable 接口重复执行过程、函数和方法。脚本的评估包含过程、函数和方法,将中间代码存储在引擎中,从而在重复执行时提高性能。
在脚本中实现 Java 接口
Java 脚本 API 允许您用脚本语言实现 Java 接口。Java 接口的方法可以使用顶层过程或对象的实例方法在脚本中实现。用脚本语言实现 Java 接口的优点是,您可以用 Java 代码使用接口的实例,就好像接口是用 Java 实现的一样。您可以将接口的实例作为参数传递给 Java 方法。Invocable接口的getInterface()方法用于获取在脚本中实现的 Java 接口的实例。该方法有两个版本:
-
<T> T getInterface(Class<T> cls) -
<T> T getInterface(Object obj, Class<T> cls)
第一个版本用于获取 Java 接口的实例,该接口的方法在脚本中作为顶级过程实现。接口类型作为参数传递给该方法。假设你有一个Calculator接口,如清单 10-18 所示,它有两个方法叫做add()和subtract()。
// Calculator.java
package com.jdojo.script;
public interface Calculator {
int add (int n1, int n2);
int subtract (int n1, int n2);
}
Listing 10-18A Calculator Interface
考虑以下两个用 Groovy 编写的顶级函数:
def add(n1, n2) {
n1 + n2
}
def subtract(n1, n2) {
n1 -n2
}
这两个函数为Calculator接口的两个方法提供了实现。在 Groovy 脚本引擎编译了这些函数之后,您可以获得一个Calculator接口的实例,如下所示:
// Cast the engine reference to the Invocable type
Invocable inv = (Invocable) engine;
// Get the reference of the Calculator interface
Calculator calc = inv.getInterface(Calculator.class);
if (calc == null) {
System.err.println(
"Calculator interface implementation not found.");
} else {
// Use calc to call add() and subtract() methods
}
您可以添加两个数字,如下所示:
int sum = calc.add(15, 10);
清单 10-19 展示了如何使用 Groovy 中的顶级过程实现 Java 接口。请查阅脚本语言的文档,了解它是如何支持这一功能的。
// UsingInterfaces.java
package com.jdojo.script;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class UsingInterfaces {
public static void main(String[] args) {
// Get the Groovy engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Make sure the script engine implements
// Invocable interface
if (!(engine instanceof Invocable)) {
System.out.println(
"""Interface implementation in script
is not supported.""");
return;
}
// Cast the engine reference to the Invocable
// type
Invocable inv = (Invocable) engine;
// Create the script for add() and subtract()
// functions
String script = """
def add(n1, n2) { n1 + n2 }
def subtract(n1, n2) { n1 - n2 }
""";
try {
// Compile the script that will be stored in
// the engine
engine.eval(script);
// Get the interface implementation
Calculator calc = inv.getInterface(
Calculator.class);
if (calc == null) {
System.err.println(
"""Calculator interface implementation
not found.""");
return;
}
int result1 = calc.add(15, 10);
System.out.println(
"add(15, 10) = " + result1);
int result2 = calc.subtract(15, 10);
System.out.println(
"subtract(15, 10) = " + result2);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
add(15, 10) = 25
subtract(15, 10) = 5
Listing 10-19Implementing a Java Interface Using Top-Level Functions in a Script
第二个版本的getInterface()方法用于获得一个 Java 接口的实例,该接口的方法被实现为一个对象的实例方法。它的第一个参数是用脚本语言创建的对象的引用。对象的实例方法实现作为第二个参数传入的接口类型。Groovy 中的以下代码创建了一个对象,该对象的实例方法实现了Calculator接口:
class GCalculator {
def add(int n1, int n2){n1 + n2}
def subtract(int n1, int n2){n1 + n2}
}
calculator = new GCalculator()
当脚本对象的实例方法实现 Java 接口的方法时,您需要执行一个额外的步骤。在获取接口的实例之前,需要获取脚本对象的引用,如下所示:
// Get the reference of the global script object calc
Object calc = engine.get("calculator");
// Get the implementation of the Calculator interface
Calculator calculator =
inv.getInterface(calc, Calculator.class);
清单 10-20 展示了如何使用 Groovy 将 Java 接口的方法实现为对象的实例方法。
// ScriptObjectImplInterface.java
package com.jdojo.script;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class ScriptObjectImplInterface {
public static void main(String[] args) {
// Get the Groovy engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
// Make sure the engine implements the Invocable
// interface
if (!(engine instanceof Invocable)) {
System.out.println(
"""Interface implementation in script is
not supported.""");
return;
}
// Cast the engine reference to the Invocable type
Invocable inv = (Invocable) engine;
String script = """
class GCalculator {
def add(int n1, int n2){n1 + n2}
def subtract(int n1, int n2){n1 + n2}
}
calculator = new GCalculator()
""";
try {
// Compile and store the script in the engine
engine.eval(script);
// Get the reference of the global script
// object calc
Object calc = engine.get("calculator");
// Get the implementation of the Calculator
// interface
Calculator calculator =
inv.getInterface(calc, Calculator.class);
if (calculator == null) {
System.err.println(
"""Calculator interface implementation
not found.""");
return;
}
int result1 = calculator.add(15, 10);
System.out.println(
"add(15, 10) = " + result1);
int result2 = calculator.subtract(15, 10);
System.out.println(
"subtract(15, 10) = " + result2);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
add(15, 10) = 25
subtract(15, 10) = 5
Listing 10-20Implementing Methods of a Java Interface As Instance Methods of an Object in a Script
使用编译的脚本
脚本引擎可以允许编译脚本并重复执行它。执行编译后的脚本可以提高应用程序的性能。脚本引擎可以以 Java 类、Java 类文件的形式或特定于语言的形式编译和存储脚本。
并非所有脚本引擎都需要支持脚本编译。支持脚本编译的脚本引擎必须实现Compilable接口。Groovy 引擎支持脚本编译。以下代码片段检查脚本引擎是否实现了Compilable接口:
// Get the script engine reference
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"YOUR_ENGINE_NAME");
if (engine instanceof Compilable) {
System.out.println(
"Script compilation is supported.");
} else {
System.out.println(
"Script compilation is not supported.");
}
一旦您知道脚本引擎实现了Compilable接口,您就可以将其引用转换为Compilable类型
// Cast the engine reference to the Compilable type
Compilable comp = (Compilable) engine;
Compilable接口包含两个方法:
-
CompiledScript compile(String script) throws ScriptException -
CompiledScript compile(Reader script) throws ScriptException
该方法的两个版本仅在脚本源的类型上有所不同。第一个版本接受脚本作为String,第二个版本接受脚本作为Reader。
compile()方法返回一个CompiledScript类的对象。CompiledScript是一个抽象类。脚本引擎的提供者提供了这个类的具体实现。一个CompiledScript与创建它的ScriptEngine相关联。CompiledScript类的getEngine()方法返回与其关联的ScriptEngine的引用。
要执行编译后的脚本,您需要调用CompiledScript类的以下eval()方法之一:
-
Object eval() throws ScriptException -
Object eval(Bindings bindings) throws ScriptException -
Object eval(ScriptContext context) throws ScriptException
没有任何参数的eval()方法使用脚本引擎的默认脚本上下文来执行编译后的脚本。当你向另外两个版本传递一个Bindings或一个ScriptContext时,它们的工作方式与ScriptEngine接口的eval()方法相同。
清单 10-21 展示了如何编译并执行一个脚本。它使用不同的参数将相同的编译脚本执行两次。
// CompilableTest .java
package com.jdojo.script;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class CompilableTest {
public static void main(String[] args) {
// Get the Groovy engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"Groovy");
if (!(engine instanceof Compilable)) {
System.out.println(
"Script compilation not supported.");
return;
}
// Cast the engine reference to the Compilable
// type
Compilable comp = (Compilable) engine;
try {
// Compile a script
String script = "println(n1 + n2)";
CompiledScript cScript = comp.compile(script);
// Store n1 and n2 script variables in a
// Bindings
Bindings scriptParams =
engine.createBindings();
scriptParams.put("n1", 2);
scriptParams.put("n2", 3);
cScript.eval(scriptParams);
// Execute the script again with different
// values for n1 and n2
scriptParams.put("n1", 9);
scriptParams.put("n2", 7);
cScript.eval(scriptParams);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
5
16
Listing 10-21Using Compiled Scripts
在脚本语言中使用 Java
脚本语言允许在脚本中使用 Java 类库。每种脚本语言都有自己的使用 Java 类的语法。讨论所有脚本语言的语法是不可能的,也超出了本书的范围。在这一节中,我将讨论在 Groovy 中使用一些 Java 结构的语法。关于 Groovy 的完整报道,请参考网站 www.groovy-lang.org/ 。
声明变量
在脚本语言中声明变量并不一定与 Java 相关。通常,脚本语言允许您在不声明变量的情况下给变量赋值。然后在运行时基于变量存储的值的类型来确定变量的类型。
在 Groovy 中,关键字def用于声明一个变量。如果您决定在变量声明中省略关键字def,那么该变量在整个脚本中都是可访问的,尽管不是在您在脚本中声明的类中,并且在脚本被处理后,可以从 Java 中访问该值。以下代码片段声明了两个变量,并为它们赋值:
// Declare a variable named msg using the def keyword
def msg = "Hello";
// Declare a variable named greeting without using the
// keyword def. We can later use
// Object greeting = engine.get("greeting");
// in Java to get the value.
greeting = "Hello";
导入 Java 类
Groovy 位于 JVM 之上,所以您可以像导入 Java 类文件一样,将 Java 类从标准库中导入到 Groovy 脚本中。这同样适用于项目中包含的库提供的类以及项目中定义的类:
// A class from the standard library
import java.text.SimpleDateFormat
// A class defined elsewhere in the project
import java17.script.SomeJavaClass
// Some library class. Must be inside the classpath.
import com.foo.superlib.Foo
def obj = new SomeJavaClass(8)
def sdf = new SimpleDateFormat("yyyy-MM-dd")
def foo = new Foo()
...
其他脚本语言定义或者不定义它们自己导入 Java 类的方式。有关详细信息,请参考他们的文档。
实现脚本引擎
实现一个成熟的脚本引擎不是一件简单的任务,它超出了本书的范围。本节旨在为您提供实现脚本引擎所需的设置的简要但完整的概述。在本节中,您将实现一个简单的脚本引擎,称为JKScript引擎。它将使用以下规则计算算术表达式:
-
它将计算由两个操作数和一个运算符组成的算术表达式。
-
表达式可能有两个数字文字、两个变量,或者一个数字文字和一个变量作为操作数。数字文字必须是十进制格式。不支持十六进制、八进制和二进制数字文本。
-
表达式中的算术运算仅限于加、减、乘和除。
-
它会将
+、-、*和/识别为算术运算符。 -
引擎将返回一个
Double对象作为表达式的结果。 -
可以使用引擎的全局范围或引擎范围绑定将表达式中的操作数传递给引擎。
-
它应该允许从一个
String对象和一个java.io.Reader对象执行脚本。然而,一个Reader应该只有一个表达式作为其内容。 -
它不会实现
Invocable和Compilable接口。
使用这些规则,脚本引擎的一些有效表达式如下:
-
10 + 90 -
10.7 + 89.0 -
+10 + +90 -
num1 + num2 -
num1 * num2 -
78.0 / 7.5
脚本 API 使用服务提供者机制来发现脚本引擎。服务类型是javax.script.ScriptEngineFactory接口。您的脚本引擎必须为此服务类型提供实现。你将把你的脚本引擎打包在一个名为jdojo.jkscript的独立模块中,如清单 10-22 中所声明的。
// module-info.java
module jdojo.jkscript {
requires java.scripting;
provides javax.script.ScriptEngineFactory
with com.jdojo.jkscript.JKScriptEngineFactory;
}
Listing 10-22The Declaration of a jdojo.jkscript Module
该模块读取java.scripting模块,因为它需要使用该模块中的类型。该模块提供了javax.script.ScriptEngineFactory服务接口的实现,它是com.jdojo.jkscript.JKScriptEngineFactory类。您不需要导出您的模块的任何包,因为没有其他模块应该直接从该模块访问任何类型。
作为JKScript脚本引擎实现的一部分,你将开发表 10-2 中列出的三个类。在随后的部分中,您将开发这些类。
表 10-2
要为 jscript 脚本引擎开发的类
|班级
|
描述
|
| --- | --- |
| Expression | Expression类是脚本引擎的核心。它执行解析和评估算术表达式的工作。它在JKScriptEngine类的eval()方法中使用。 |
| JKScriptEngine | 接口的一个实现。它扩展了实现ScriptEngine接口的AbstractScriptEngine类。AbstractScriptEngine类为ScriptEngine接口的eval()方法的几个版本提供了标准实现。您需要实现下面两个版本的eval()方法:Object eval(String, ScriptContext)和Object eval(Reader, ScriptContext) |
| JKScriptEngineFactory | 接口的一个实现。这是javax.script.ScriptEngineFactory服务接口的服务提供者。 |
表达式类
Expression类包含解析和评估算术表达式的主要逻辑。清单 10-23 包含了Expression类的完整代码。
// Expression.java
package com.jdojo.jkscript;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.script.ScriptContext;
public class Expression {
private String exp;
private ScriptContext context;
private String op1;
private char op1Sign = '+';
private String op2;
private char op2Sign = '+';
private char operation;
private boolean parsed;
public Expression(String exp, ScriptContext context) {
if (exp == null || exp.trim().equals("")) {
throw new IllegalArgumentException(
this.getErrorString());
}
this.exp = exp.trim();
if (context == null) {
throw new IllegalArgumentException(
"ScriptContext cannot be null.");
}
this.context = context;
}
public String getExpression() {
return exp;
}
public ScriptContext getScriptContext() {
return context;
}
public Double eval() {
// Parse the expression
if (!parsed) {
this.parse();
this.parsed = true;
}
// Extract the values for the operand
double op1Value = getOperandValue(op1Sign, op1);
double op2Value = getOperandValue(op2Sign, op2);
// Evaluate the expression
Double result = null;
switch (operation) {
case '+':
result = op1Value + op2Value;
break;
case '-':
result = op1Value - op2Value;
break;
case '*':
result = op1Value * op2Value;
break;
case '/':
result = op1Value / op2Value;
break;
default:
throw new RuntimeException(
"Invalid operation:" + operation);
}
return result;
}
private double
getOperandValue(char sign, String operand) {
// Check if operand is a double
double value;
try {
value = Double.parseDouble(operand);
return sign == '-' ? -value : value;
} catch (NumberFormatException e) {
// Ignore it. Operand is not in a format that
// can be converted to a double value.
}
// Check if operand is a bind variable
Object bindValue = context.getAttribute(operand);
if (bindValue == null) {
throw new RuntimeException(operand +
" is not found in the script context.");
}
if (bindValue instanceof Number) {
value = ((Number) bindValue).doubleValue();
return sign == '-' ? -value : value;
} else {
throw new RuntimeException(operand +
" must be bound to a number.");
}
}
public void parse() {
// Supported expressions are of the form v1 op v2,
// where v1 and v2 are variable names or numbers,
// and op could be +, -, *, or /
// Prepare the pattern for the expected expression
String operandSignPattern = "([+-]?)";
String operandPattern = "([\\p{Alnum}\\p{Sc}_.]+)";
String whileSpacePattern = "([\\s]*)";
String operationPattern = "([+*/-])";
String pattern = "^" + operandSignPattern
+ operandPattern
+ whileSpacePattern + operationPattern
+ whileSpacePattern
+ operandSignPattern + operandPattern
+ "$";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(exp);
if (!m.matches()) {
// The expression is not in the expected format
throw new IllegalArgumentException(
this.getErrorString());
}
// Get operand-1
String temp = m.group(1);
if (temp != null && !temp.equals("")) {
this.op1Sign = temp.charAt(0);
}
this.op1 = m.group(2);
// Get operation
temp = m.group(4);
if (temp != null && !temp.equals("")) {
this.operation = temp.charAt(0);
}
// Get operand-2
temp = m.group(6);
if (temp != null && !temp.equals("")) {
this.op2Sign = temp.charAt(0);
}
this.op2 = m.group(7);
}
private String getErrorString() {
return "Invalid expression[" + exp + "]"
+ "\nSupported expression syntax is: "
+ "op1 operation op2"
+ "\n where op1 and op2 can be a number "
+ " or a bind variable"
+ " , and operation can be"
+ " +, -, *, and /.";
}
@Override
public String toString() {
return "Expression: " + this.exp + ", op1 Sign = "
+ op1Sign + ", op1 = " + op1
+ ", op2 Sign = " + op2Sign
+ ", op2 = " + op2
+ ", operation = " + operation;
}
}
Listing 10-23The Expression Class That Parses and Evaluates an Arithmetic Expression
Expression类被设计用来解析和评估以下形式的算术表达式
op1 operation op2
这里,op1和op2是两个操作数,可以是十进制格式的数字或变量,operation可以是+、-、*或/。
建议使用的Expression类是
Expression exp = new Expression(expression, scriptContext);
Double value = exp.eval();
让我们详细讨论一下Expression类的重要组件。实例变量exp和context分别是表达式和对表达式求值的ScriptContext。它们被传递给这个类的构造函数。
实例变量op1和op2分别表示表达式中的第一个和第二个操作数。实例变量op1Sign和op2Sign分别代表表达式中第一个和第二个操作数的符号,可以是+或-。当使用parse()方法解析表达式时,操作数及其符号被填充。
实例变量operation表示要对操作数执行的算术运算(+、-、*或/)。
实例变量parsed用于跟踪表达式是否已经被解析。parse()方法将其设置为true。
构造函数接受一个表达式和一个ScriptContext,确保它们不是null,并将它们存储在实例变量中。在将表达式存储到实例变量exp中之前,它会从表达式中删除开头和结尾的空白。
parse()方法将表达式解析成操作数和操作。它使用正则表达式来解析表达式文本。正则表达式要求表达式文本采用以下形式:
-
第一个操作数的可选符号
+或- -
第一个操作数,可以由字母数字、货币符号、下划线和小数点的组合组成
-
任何数量的空白
-
可能是
+、-、*或/的操作标志 -
第二个操作数的可选符号
+或- -
第二个操作数,可以由字母数字、货币符号、下划线和小数点的组合组成
正则表达式([+-]?)将匹配操作数的可选符号。正则表达式([\\pAlnum\\pSc_.]+)会匹配一个操作数,可能是十进制数,也可能是名字。正则表达式([\\s]*)将匹配任意数量的空格。正则表达式([+*/-])将匹配一个操作符。所有正则表达式都用括号括起来形成组,这样就可以捕获表达式的匹配部分。
如果一个表达式匹配正则表达式,parse()方法将匹配存储到各自的实例变量中。
注意,匹配操作数的正则表达式并不完美。它将允许几种无效的情况,比如一个操作数有多个小数点,等等。然而,对于这个演示目的,它将做。
在表达式被解析后,在表达式求值期间使用getOperandValue()方法。如果操作数是一个double数,它通过应用操作数的符号返回值。否则,它会在ScriptContext中查找操作数的名称。如果在ScriptContext中没有找到操作数的名称,它抛出一个RuntimeException。如果在ScriptContext中找到操作数的名称,它将检查该值是否为数字。如果该值是一个数字,则在将符号应用于该值后返回该值;否则抛出一个RuntimeException。
getOperandValue()方法不支持十六进制、八进制和二进制格式的操作数。例如,像“0x2A + 0b1011”这样的表达式将不会被视为具有两个带int文字的操作数的表达式。读者可以增强这种方法,以支持十六进制、八进制和二进制格式的数字文字。
eval()方法计算表达式并返回一个double值。首先,如果表达式还没有被解析,它就解析它。注意,多次调用eval()只会解析表达式一次。它获取两个操作数的值,执行运算,并返回表达式的值。
JKScriptEngine 类
清单 10-24 包含了JKScript脚本引擎的实现。它的eval(String, ScriptContext)方法包含主要逻辑:
Expression exp = new Expression(script, context); Object result = exp.eval();
它创建了一个Expression类的对象。它调用评估表达式并返回结果的Expression对象的eval()方法。
eval(Reader,ScriptContext)方法从Reader中读取所有行,将它们连接起来,并将结果String传递给eval(String, ScriptContext)方法来计算表达式。注意一个Reader必须只有一个表达式。一个表达式可以拆分成多行。Reader中的空白被忽略。
// JKScriptEngine.java
package com.jdojo.jkscript;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
public class JKScriptEngine extends AbstractScriptEngine {
private final ScriptEngineFactory factory;
public JKScriptEngine(ScriptEngineFactory factory) {
this.factory = factory;
}
@Override
public Object
eval(String script, ScriptContext context)
throws ScriptException {
try {
Expression exp =
new Expression(script, context);
Object result = exp.eval();
return result;
} catch (Exception e) {
throw new ScriptException(e.getMessage());
}
}
@Override
public Object
eval(Reader reader, ScriptContext context)
throws ScriptException {
// Read all lines from the Reader
BufferedReader br = new BufferedReader(reader);
String script = "";
try {
String str;
while ((str = br.readLine()) != null) {
script = script + str;
}
} catch (IOException e) {
throw new ScriptException(e);
}
// Use the String version of eval()
return eval(script, context);
}
@Override
public Bindings createBindings() {
return new SimpleBindings();
}
@Override
Public ScriptEngineFactory getFactory() {
return factory;
}
}
Listing 10-24An Implementation of the JKScript Script Engine
JKScriptEngineFactory 类
清单 10-25 包含了JKScript引擎的ScriptEngineFactory接口的实现。它的一些方法返回一个"Not Implemented"字符串,因为你不支持这些方法公开的特性。JKScriptEngineFactory类中的代码是不言自明的。使用ScriptEngineManager可以获得一个JKScript引擎的实例,其名称为jks、JKScript或jkscript,如getNames()方法中编码的那样。
// JKScriptEngineFactory.java
package com.jdojo.jkscript;
import java.util.List;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
public class JKScriptEngineFactory
implements ScriptEngineFactory {
@Override
public String getEngineName() {
return "JKScript Engine";
}
@Override
public String getEngineVersion() {
return "1.0";
}
@Override
public List<String> getExtensions() {
return List.of("jks");
}
@Override
public List<String> getMimeTypes() {
return List.of("text/jkscript");
}
@Override
public List<String> getNames() {
return List.of("jks", "JKScript", "jkscript");
}
@Override
public String getLanguageName() {
return "JKScript";
}
@Override
public String getLanguageVersion() {
return "1.0";
}
@Override
public Object getParameter(String key) {
switch (key) {
case ScriptEngine.ENGINE:
return getEngineName();
case ScriptEngine.ENGINE_VERSION:
return getEngineVersion();
case ScriptEngine.NAME:
return getEngineName();
case ScriptEngine.LANGUAGE:
return getLanguageName();
case ScriptEngine.LANGUAGE_VERSION:
return getLanguageVersion();
case "THREADING":
return "MULTITHREADED";
default:
return null;
}
}
@Override
public String
getMethodCallSyntax(String obj, String m, String[] p) {
return "Not implemented";
}
@Override
public String
getOutputStatement(String toDisplay) {
return "Not implemented";
}
@Override
public String
getProgram(String[] statements) {
return "Not implemented";
}
@Override
public ScriptEngine
getScriptEngine() {
return new JKScriptEngine(this);
}
}
Listing 10-25A ScriptEngineFactory Implementation for the JKScript Script Engine
打包 jscript 文件
要让其他人使用您的 JKScript 引擎,您需要做的就是为jdojo.jkscript模块提供模块化 JAR。
使用 jscript 脚本引擎
是时候测试您的 JKScript 脚本引擎了。第一步也是最重要的一步是将您在上一节中创建的jdojo.jkscript.jar包含到应用程序的模块路径中。之后,使用 JKScript 脚本引擎与使用任何其他脚本引擎没有什么不同。
下面的代码片段使用 JKScript 作为其名称来创建 JKScript 脚本引擎的实例。您也可以使用它的其他名称,jks和jkscript:
// Create the JKScript engine
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JKScript");
if (engine == null) {
System.out.println(
"JKScript engine is not available. ");
System.out.println(
"Add jkscript.jar to CLASSPATH.");
else {
// Evaluate your JKScript
}
清单 10-26 包含一个使用 JKScript 脚本引擎评估不同类型表达式的程序。执行存储在String对象和文件中的表达式。一些表达式使用数字文字和一些绑定变量,它们的值在引擎范围和引擎的默认ScriptContext的全局范围中的绑定中传递。注意,这个程序期望在当前目录中有一个名为jkscript.txt的文件,其中包含一个可以被JKScript脚本引擎理解的算术表达式。如果脚本文件不存在,程序将在标准输出中打印一条消息,其中包含预期脚本文件的路径。您可能会在最后一行得到不同的输出。
// JKScriptTest.java
package com.jdojo.script;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class JKScriptTest {
public static void
main(String[] args)
throws FileNotFoundException, IOException {
// Create JKScript engine
ScriptEngineManager manager =
new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"JKScript");
if (engine == null) {
System.out.println(
"JKScript engine is not available. ");
System.out.println(
"Add jkscript.jar to CLASSPATH.");
return;
}
// Test scripts as String
testString(manager, engine);
// Test scripts as a Reader
testReader(manager, engine);
}
public static void
testString(ScriptEngineManager manager,
ScriptEngine engine) {
try {
// Use simple expressions with numeric literals
String script = "12.8 + 15.2";
Object result = engine.eval(script);
System.out.println(script + " = " + result);
script = "-90.0 - -10.5";
result = engine.eval(script);
System.out.println(script + " = " + result);
script = "5 * 12";
result = engine.eval(script);
System.out.println(script + " = " + result);
script = "56.0 / -7.0";
result = engine.eval(script);
System.out.println(script + " = " + result);
// Use global scope bindings variables
manager.put("num1", 10.0);
manager.put("num2", 20.0);
script = "num1 + num2";
result = engine.eval(script);
System.out.println(script + " = " + result);
// Use global and engine scopes bindings.
// num1 from engine scope and num2 from
// global scope will be used.
engine.put("num1", 70.0);
script = "num1 + num2";
result = engine.eval(script);
System.out.println(script + " = " + result);
// Try mixture of number literal and bindings.
// num1 from the engine scope bindings will be
// used
script = "10 + num1";
result = engine.eval(script);
System.out.println(script + " = " + result);
} catch (ScriptException e) {
e.printStackTrace();
}
}
public static void
testReader(ScriptEngineManager manager,
ScriptEngine engine) {
try {
Path scriptPath = Paths.get("jkscript.txt").
toAbsolutePath();
if (!Files.exists(scriptPath)) {
System.out.println(scriptPath +
" script file does not exist.");
return;
}
try (Reader reader = Files.
newBufferedReader(scriptPath);) {
Object result = engine.eval(reader);
System.out.println("Result of " +
scriptPath + " = " + result);
}
} catch (ScriptException | IOException e) {
e.printStackTrace();
}
}
}
12.8 + 15.2 = 28.0
-90.0 - -10.5 = -79.5
5 * 12 = 60.0
56.0 / -7.0 = -8.0
num1 + num2 = 30.0
num1 + num2 = 90.0
10 + num1 = 80.0
Result of C:\Java9APIsAndModules\jkscript.txt = 88.0
Listing 10-26Using the JKScript Script Engine
Groovy 中的 JavaFX
我们可以使用脚本来加速 JavaFX 的开发。事实上,混合 Java 代码和脚本有助于分离前端和后端逻辑,并且因为脚本比 Java 代码更简洁,所以可以节省一些开发时间。
清单 10-27 包含一个简单的 HelloWorld 风格的 JavaFX 应用程序。
package com.jdojo.groovyfx;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javafx.application.Application;
import javafx.stage.Stage;
public class HelloGroovyFX extends Application {
private Invocable inv;
public static void main(String[] args) {
launch(args);
}
@Override
public void init() {
// Create a script engine manager
ScriptEngineManager manager =
new ScriptEngineManager();
// Obtain a Groovy script engine from the manager
ScriptEngine engine =
manager.getEngineByName("Groovy");
// Store the Groovy script in a String
String script = """
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.scene.layout.StackPane
import javafx.beans.property.SimpleStringProperty as SP
def go(def primaryStage) {
primaryStage.setTitle "Hello World!"
Button btn = new Button()
btn.text = "Say 'Hello World'"
btn.onAction = { def event ->
println("Hello World!")
}
StackPane root = new StackPane()
root.children.add(btn)
primaryStage.scene = new Scene(root, 300, 250)
primaryStage.show()
}
""";
try {
// Execute the script
engine.eval(script);
inv = (Invocable) engine;
} catch (ScriptException e) {
e.printStackTrace();
}
}
@Override
public void start(Stage primaryStage) {
try {
inv.invokeFunction("go", primaryStage);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Listing 10-27A JavaFX Application Using a Groovy Script
为此,您必须添加 JavaFX 库。对于 Maven 项目来说,这很容易。只是补充
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>16</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>16</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>16</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>16</version>
</dependency>
在您的pom.xml文件的<dependencies>部分中。
与 Java 相比,Groovy 版本的前端代码要简单一些。在脚本中,您可以使用 Java 类的属性来调用它们的方法。例如,不要用 Java 编写:
btn.setText("Say 'Hello World'");
你可以用 Groovy 写这个:
btn.text = "Say 'Hello World'"
此规则的一个例外是
primaryStage.setTitle "Hello World!"
因为在Stage类中,title字段的类型不同于String。
为按钮添加事件处理程序也更容易。您可以使用 Groovy 闭包作为按钮的事件处理程序。请注意,您也可以使用onAction属性来设置事件处理程序,而不是调用Button类的setOnAction()方法。以下代码片段显示了如何为按钮设置ActionEvent处理程序:
btn.onAction = { def event ->
println("Hello World!")
}
图 10-7 显示了正在运行的 JavaFX 应用程序。
图 10-7
带有 Groovy 脚本的 JavaFX 应用程序
摘要
脚本语言是一种编程语言,它使您能够编写由运行时环境评估(或解释)的脚本,运行时环境称为脚本引擎(或解释器)。脚本是使用脚本语言的语法编写的字符序列,用作由解释器执行的程序的源。Java 脚本 API 允许您执行用任何脚本语言编写的脚本,这些脚本可以从 Java 应用程序编译成 Java 字节码。
使用脚本引擎执行脚本,脚本引擎是ScriptEngine接口的一个实例。ScriptEngine接口的实现者也提供了ScriptEngineFactory接口的实现,其工作是创建脚本引擎的实例并提供关于脚本引擎的细节。ScriptEngineManager类为脚本引擎提供了发现和实例化机制。一个ScriptManager维护一个键值对的映射,作为一个由它创建的所有脚本引擎共享的Bindings接口的实例。
您可以执行包含在String或Reader中的脚本。ScriptEngine的eval()方法用于执行脚本。您可以使用ScriptContext向脚本传递参数。传递的参数可以是脚本引擎的本地参数,脚本执行的本地参数,或者由ScriptManager创建的所有脚本引擎的全局参数。使用 Java 脚本 API,您还可以执行用脚本语言编写的过程和函数。如果脚本引擎支持,您还可以预编译脚本,并执行从 Java 重复的脚本以获得更好的性能。
您可以使用 Java 脚本 API 实现您的脚本引擎。您需要为ScriptEngine和ScriptEngineFactory接口提供实现。你需要以某种方式打包你的脚本引擎代码,这样引擎就可以在运行时被ScriptManager发现。
练习
练习 1
什么是脚本语言?
练习 2
哪个 JDK 模块包含脚本 API?
运动 3
简述以下类和接口的使用:ScriptEngineFactory、ScriptEngine、ScriptEngineManager、Compilable、Invocable、Bindings、ScriptContext、ScriptException。
演习 4
一个ScriptEngine的eval()方法有什么用?
锻炼 5
编写一个程序,在其中使用SimpleScriptContext类创建一个ScriptContext接口的实例。在引擎范围和全局范围中存储一些属性,检索相同的属性,并打印它们的值。
锻炼 6
如何向全局范围和引擎范围添加属性?
锻炼 7
如何将由ScriptEngine执行的脚本输出发送到一个文件中?
运动 8
编写一段代码来检查ScriptEngine是否支持编译脚本。
演习 9
使用java.util.List接口的of()方法创建一个不可修改的两个字符串的列表,并打印列表中的值。使用 Groovy 脚本编写代码。
运动 10
如果您想要推出自己的脚本引擎,那么您必须提供其实现的服务接口的名称是什么?