Java8-API-入门手册-六-

94 阅读1小时+

Java8 API 入门手册(六)

原文:Beginning Java 8 APIs

协议:CC BY-NC-SA 4.0

6# 七、Java 远程方法调用

在本章中,您将学习

  • 什么是 Java 远程方法调用(RMI)和 RMI 架构
  • 如何开发和打包 RMI 服务器和客户机应用
  • 如何启动rmiregistry、RMI 服务器和客户端应用
  • 如何对 RMI 应用进行故障排除和调试
  • RMI 应用中的动态类下载
  • RMI 应用中远程对象的垃圾收集

什么是 Java 远程方法调用?

Java 支持各种应用架构,这些架构决定了应用代码如何以及在哪里部署和执行。在最简单的应用架构中,所有的 Java 代码都驻留在一台机器上,一个 JVM 管理所有的 Java 对象以及它们之间的交互。这是一个独立应用的例子,其中所需要的只是一台可以启动 JVM 的机器。Java 还支持分布式应用架构,其中应用的代码和执行可以分布在多台机器上。

在第四章中,你看到了 Java Applet,其中 Java 类被部署在 web 服务器上。web 浏览器将 applet 类下载到客户机上,并在客户机上运行的 JVM 中执行。在 applet 的情况下,Java 代码仍然在一个 JVM 中执行。在第五章中,您学习了 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 体系结构

图 7-1 以简化的形式显示了 RMI 架构。图中的矩形框表示 RMI 应用中的一个组件。箭头线显示了沿箭头方向从一个组件发送到另一个组件的消息。显示从 1 到 11 的数字的椭圆表示在典型的 RMI 应用中发生的步骤序列。我将在本节详细讨论所有步骤。

A978-1-4302-6662-4_7_Fig1_HTML.jpg

图 7-1。

The RMI architecture

让我们假设您已经开发了运行 RMI 应用所需的所有 Java 类和接口。在这一节中,您将浏览运行 RMI 应用时涉及的所有步骤。在接下来的几节中,您将开发每一步所需的 Java 代码。

RMI 应用的第一步是在服务器中创建一个 Java 对象。该对象将被用作远程对象。要使普通的 Java 对象成为远程对象,还需要执行一个额外的步骤。这一步被称为导出远程对象。当一个普通的 Java 对象作为远程对象导出时,它就可以接收/处理来自远程客户机的调用了。导出过程产生一个远程对象引用(也称为存根)。远程引用知道导出对象的细节,比如它的位置和可以远程调用的方法。该步骤在图 7-1 中未标注。它发生在服务器程序内部。当这一步完成时,远程对象已经在服务器中创建好了,并准备好接收远程方法调用。

下一步由服务器执行,向 RMI 注册中心注册(或绑定)远程引用。服务器为它在 RMI 注册表中注册的每个远程引用选择一个惟一的名称。远程客户端需要使用相同的名称在 RMI 注册表中查找远程引用。这在图 7-1 中标记为#1。当这一步完成时,RMI 注册中心已经注册了远程对象引用,并且对调用远程对象上的方法感兴趣的客户机可以从 RMI 注册中心请求它的引用。

Tip

出于安全原因,RMI 注册中心和服务器必须运行在同一台机器上,以便服务器可以向 RMI 注册中心注册远程引用。如果没有施加这种限制,黑客可能会从他的机器上向您的 RMI 注册表注册他自己的有害 Java 对象。

这一步包括客户机和 RMI 注册中心之间的交互。通常,客户机和 RMI 注册中心运行在两台不同的机器上。客户机向 RMI 注册中心发送一个远程引用的查找请求。客户端使用名称在 RMI 注册表中查找远程引用。该名称与步骤#1 中服务器用来绑定 RMI 注册表中的远程引用的名称相同。在图 7-1 中,查找步骤被标记为#2。RMI 注册中心将远程引用(或存根)返回给图 7-1 中标记为步骤#3 的客户端。如果远程引用没有在 RMI 注册表中与客户机在查找请求中使用的名称绑定,RMI 注册表将抛出一个NotBoundException。如果这一步成功完成,客户机就收到了运行在服务器上的远程对象的远程引用(或存根)。

在这一步中,客户机调用存根上的一个方法。如图 7-1 中步骤#4 所示。此时,存根连接到服务器并传输调用远程对象上的方法所需的信息,例如方法的名称、方法的参数等。存根知道服务器的位置以及如何联系服务器上的远程对象的细节。该步骤在图 7-1 中标为步骤#5。网络层的许多不同层参与了从存根到服务器的信息传输。

骨架是客户端存根的服务器端副本。它的工作是接收存根发送的数据。这如图 7-1 中的步骤#6 所示。在一个框架收到数据后,它将数据重组为更有意义的格式,并调用远程对象上的方法,如图 7-1 中的步骤 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接口。Remote接口是一个没有声明任何方法的标记接口。
  • 远程接口中的所有方法都必须抛出一个RemoteException或异常,即它的超类,如IOExceptionExceptionRemoteException是被检查的异常。远程方法还可以抛出任意数量的其他特定于应用的异常。
  • 远程方法可以接受远程对象的引用作为参数。它也可以返回远程对象的引用作为它的返回值。如果远程接口中的方法接受或返回远程对象引用,则参数或返回类型必须声明为类型Remote,而不是实现Remote接口的类的类型。
  • 远程接口只能在其方法的参数或返回值中使用三种数据类型。它可以是基本类型、远程对象或可序列化的非远程对象。远程对象通过引用传递,而非远程可序列化对象通过复制传递。如果一个对象的类实现了java.io.Serializable接口,那么这个对象就是可序列化的。

您将您的远程接口命名为RemoteUtility。清单 7-1 包含了RemoteUtility远程接口的代码。它包含三个方法,分别叫做echo()getServerTime()add(),它们提供了你想要的三个功能。

清单 7-1。RemoteUtility 接口

// RemoteUtility.java

package com.jdojo.rmi;

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;

}

实现远程接口

这一步包括创建一个实现远程接口的类。你将把这个类命名为RemoteUtilityImpl。它将实现RemoteUtility远程接口,并将提供三种方法的实现:echo()getServerTime()add()。这个类中可以有任意数量的其他方法。您必须做的唯一一件事就是为在RemoteUtility远程接口中定义的所有方法提供实现。远程客户端将只能调用该类的远程方法。如果在这个类中定义的方法不同于远程接口中定义的方法,那么这些方法对于远程方法调用是不可用的。但是,您可以使用其他方法来实现远程方法。清单 7-2 包含了RemoteUtilityImpl类的代码。

清单 7-2。RemoteUtility 远程接口的实现类

// RemoteUtilityImpl.java

package com.jdojo.rmi;

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;

}

}

远程对象实现类非常简单。它实现了RemoteUtility接口,并为该接口的三个方法提供了实现。注意,RemoteUtilityImpl类中的这些方法没有声明它们抛出了一个RemoteException。声明所有远程方法抛出一个RemoteException的要求是针对远程接口的,而不是实现远程接口的类。

有两种方法可以为远程接口编写实现类。一种方法是从java.rmi.server.UnicastRemoteObject类继承它。另一种方法是不从任何类或者从除了UnicastRemoteObject类之外的任何类继承它。清单 7-2 采用了后一种方法。它没有从任何类继承RemoteUtilityImpl类。

如果远程接口的实现类继承自UnicastRemoteObject类或其他类,会有什么不同呢?远程接口的实现类用于创建远程对象,远程对象的方法被远程调用。这个类的对象必须经过一个导出过程,这使得它适合于远程方法调用。UnicastRemoteObject类的构造函数自动为您导出对象。所以,如果你的实现类继承自UnicastRemoteObject类,它将为你以后的整个过程节省一步。有时你的实现类必须从另一个类继承,这将迫使你不要从UnicastRemoteObject类继承它。你需要注意的一点是,UnicastRemoteObject类的构造函数抛出一个RemoteException.如果你从UnicastRemoteObject类继承远程对象实现类,实现类的构造函数必须在其声明中抛出一个RemoteException。清单 7-3 通过继承UnicastRemoteObject类重写了RemoteUtilityImpl类。这个实现中有两个新东西——它在类声明中使用了extends子句,在构造函数声明中使用了throws子句。其他一切都保持不变。当你在本章的后面编写服务器程序时,我将讨论使用清单 7-2 和清单 7-3 中所示的RemoteUtilityImpl类的实现的区别。

清单 7-3。通过从 UnicastRemoteObject 类继承 RemoteUtilityImpl 类来重写它

// RemoteUtilityImpl.java

package com.jdojo.rmi;

import java.rmi.RemoteException;

import java.rmi.server.UnicastRemoteObject;

import java.time.ZonedDateTime;

public class RemoteUtilityImpl extends UnicastRemoteObject implements RemoteUtility {

// Must throw 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;

}

}

编写 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()方法来绑定远程引用。您也可以使用它的方法,如果指定名称的旧绑定已经存在,该方法将替换旧绑定。用的名字是一个String。您将使用名称MyRemoteUtility作为您的远程引用的名称。最好遵循 RMI 注册表中绑定引用对象的命名约定,以避免名称冲突。

Registry registry = LocateRegistry.getRegistry("localhost", 1099);

String name = "MyRemoteUtility";

registry.rebind(name, remoteUtilityStub);

这就是编写服务器程序所需的全部内容。清单 7-4 包含了 RMI 服务器的完整代码。它假设RemoteUtilityImpl类不继承清单 7-2 中列出的UnicastRemoteObject类。

清单 7-4。一个 RMI 远程服务器程序

// RemoteServer.java

package com.jdojo.rmi;

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();

}

}

}

如果您使用清单 7-3 中列出的RemoteUtilityImpl类的实现,您将需要修改清单 7-4 中的代码。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 客户端程序执行以下操作:

  • 它确保它在安全管理器下运行。

SecurityManager secManager = System.getSecurityManager();

if (secManager == null) {

System.setSecurityManager(new SecurityManager());

}

它定位远程引用被服务器绑定的注册表。您必须知道机器名或 IP 地址,以及运行 RMI 注册表的端口号。在真实的 RMI 程序中,您不会在客户端程序中使用localhost来定位注册表。相反,RMI 注册中心将在一台单独的机器上运行。对于您的例子,您将在同一台机器上运行所有三个程序——RMI 注册表、服务器和客户机。

// Locate the registry

Registry registry = LocateRegistry.getRegistry("localhost", 1099);

  • 它使用Registry接口的lookup()方法在注册表中执行查找。它将绑定的远程引用的名称传递给lookup()方法,并获取远程引用(或存根)。注意,lookup()方法必须使用与服务器绑定/重新绑定远程引用相同的名称。lookup()方法返回一个Remote对象。您必须将其转换为远程接口的类型。以下代码片段将从lookup()方法返回的远程引用转换为RemoteUtility接口类型:

String name = "MyRemoteUtility";

RemoteUtility remoteUtilStub = (RemoteUtility)registry.lookup(name);

  • 它调用远程引用(或存根)上的方法。客户端程序将remoteUtilStub引用视为对本地对象的引用。对它进行的任何方法调用都被发送到服务器执行。所有远程方法都抛出一个RemoteException。当您调用任何远程方法时,您必须处理RemoteException

// Call the echo() method

String reply = remoteUtilStub.echo("Hello from the RMI client.");

清单 7-5 包含了你的客户端程序的完整代码。暂时不要运行这个程序。在接下来的几节中,您将一步一步地运行您的 RMI 应用。您可能会注意到,编写 RMI 代码并不复杂。RMI 中不同组件的管道是复杂的。

清单 7-5。一个 RMI 远程客户程序

// RemoteClient.java

package com.jdojo.rmi;

import java.rmi.NotBoundException;

import java.rmi.RemoteException;

import java.rmi.registry.LocateRegistry;

import java.rmi.registry.Registry;

import java.time.ZonedDateTime;

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);

// Echo a message from the server

String msg = "Hello";

String reply = remoteUtilStub.echo(msg);

System.out.println("Echo Message: " + msg +

", Echo reply: " + reply);

// Get the server date and time with the zone info

ZonedDateTime serverTime = remoteUtilStub.getServerTime();

System.out.println("Server Time: " + serverTime);

// Add two integers

int n1 = 101;

int n2 = 207;

int sum = remoteUtilStub.add(n1, n2);

System.out.println(n1 + " + " + n2 + " = " + sum);

}

catch (RemoteException | NotBoundException e) {

e.printStackTrace();

}

}

}

分离服务器和客户端代码

在 RMI 应用中,将服务器和客户机程序的代码分开是很重要的。服务器程序需要有以下三个组件:

  • 远程接口
  • 远程接口的实现类
  • 服务器程序

客户端程序需要有以下两个组件。

  • 远程接口
  • 客户端程序

客户端程序不应该知道实现远程接口的实现类。让客户机程序可以访问这个类违背了开发 RMI 应用的目的。您可以拥有运行服务器和客户端程序所需的附加类。

对于您的示例,您可以将服务器和客户机类文件分开,或者放在两个目录结构中,或者放在两个 JAR 文件中。您将在utilserver.jarutilclient.jar中分别打包服务器和客户端程序的类文件。

utilserver.jar文件中的文件有

  • RemoteUtility.class
  • RemoteUtilityImpl.class
  • RemoteServer.class

utilclient.jar文件中的文件有

  • RemoteUtility.class
  • RemoteClient.class

生成存根和框架

当使用UnicastRemoteObject类导出远程对象时,RMI 需要一个存根类。您可以执行以下两项操作之一:

  • 您可以使用UnicastRemoteObject类来继承您的远程接口实现类,它将自动导出您的远程对象。
  • 您可以使用UnicastRemoteObject类的exportObject()方法显式导出远程对象。

在这两种情况下,当导出一个远程对象时,RMI 需要一个存根类。在 Java 5 之前,您需要执行一个额外的步骤来为远程接口实现类生成存根类。这是通过使用 JDK 安装文件夹的bin子目录中的rmic命令来完成的。运行该命令,传递远程接口实现类的完全限定名,如下所示:

rmic com.jdojo.rmi.RemoteUtilityImpl

您可能需要适当地设置CLASSPATH环境变量,以便rmic能够找到您指定作为其参数的类。上面的命令将在RemoteUtilityImpl.class文件所在的同一个文件夹中生成以下两个类文件。

  • RemoteUtilityImpl_Stub.class
  • RemoteUtilityImpl_Skel.class

您需要在utilserver.jar文件中包含这两个类文件。请注意,只有在使用 Java 5 之前的 Java 版本时,才需要这一步。如果您的客户端程序运行的是 Java 5 之前的 Java 版本,而您的服务器运行的是 Java 5 或更高版本,您也需要执行此步骤。如果您有兴趣查看为这两个类文件生成的 Java 源代码,您可以使用带有rmic命令的–keep(或-keepgenerated)选项,这将为这些类生成 Java 源代码。以下命令将生成四个文件,两个.class文件和两个.java文件。

rmic –keep com.jdojo.chapter5.RemoteUtilityImpl

运行 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:\文件夹中。

java -Djava.security.policy=file:/c:/rmi.policy <other-options>

这种设置 Java 策略文件的方法是临时的。它应该仅用于学习目的。您需要在生产环境中设置细粒度的安全性。

运行 RMI 注册表

RMI 注册表应用是随 JDK/JRE 安装一起提供的。它被复制到相应安装主文件夹的bin子文件夹中。在 Windows 平台上,它是rmiregistry.exe可执行文件。您可以通过使用命令提示符启动rmiregistry应用来运行 RMI 注册表。它接受将在其上运行的端口号。默认情况下,它运行在端口 1099 上。以下命令在 Windows 上使用命令提示符在端口 1099 启动它:

C:\java8\bin> rmiregistry

以下命令在端口 8967 启动 RMI 注册表:

C:\java8\bin> rmiregistry 8967

rmiregistry应用不在提示符下打印任何启动信息。通常,它是作为后台进程启动的。

很可能,该命令在您的机器上不起作用。使用该命令,您将能够成功启动rmiregistry。然而,当您在下一节运行 RMI 服务器应用时,您将得到ClassNotFoundExceptionrmiregistry应用需要访问 RMI 服务器应用中使用的一些类(已注册的类)。有三种方法可以让rmiregistry使用这些类:

  • 适当地设置类路径。
  • java.rmi.server.codebase JVM 属性设置为包含rmiregistry所需类的 URL。
  • 将名为java.rmi.server.useCodebaseOnly的 JVM 属性设置为false。在 JDK 7u21(也在 JDK 6u45 和 JDK 5u45)中,此属性默认设置为 true。之前默认设置为false。如果该属性设置为falsermiregistry可以从服务器下载需要的类文件。

在启动rmiregistry之前,以下命令将serverutil.jar文件添加到CLASSPATH:

C:\java8\bin> SET CLASSPATH=C:\utilserver.jar

C:\java8\bin> rmiregistry

除了设置CLASSPATH使类对 rmiregistry 可用之外,还可以设置java.rmi.server.codebase JVM 属性,这是一个用空格分隔的 URL 列表,如下所示:

rmiregistry -J-Djava.rmi.server.codebase=file:/C:/utilserver.jar

下面的命令重置CLASSPATH并将 JVM 的java.rmi.server.useCodebaseOnly属性设置为false,这样rmiregistry将从 RMI 服务器下载任何需要的类文件。您的示例将使用以下命令:

C:\java8\bin> SET CLASSPATH=

C:\java8\bin> rmiregistry -J-Djava.rmi.server.useCodebaseOnly=false

运行 RMI 服务器

在运行 RMI 服务器之前,必须运行 RMI 注册表。回想一下,服务器在一个安全管理器下运行,该安全管理器要求您授予在 Java 策略文件中执行某些操作的权限。确保您已经在策略文件中输入了所需的授权。您可以使用以下命令来运行服务器程序。命令文本在一行中输入;为了清楚起见,已经用多行显示了它。命令文本中的每个部分都应该用空格分隔,而不是换行。在命令中,您需要更改 JAR 和策略文件的路径,以反映它们在您的机器上的路径。

java -cp C:\utilserver.jar

-Djava.rmi.server.codebase=file:/C:/utilserver.jar

-Djava.security.policy=file:/c:/rmi.policy

com.jdojo.rmi.RemoteServer

–cp选项将CLASSPATH设置为utilserver.jar文件。如果您不想使用 JAR 文件来打包与服务器相关的类文件,您可以使用任何其他的CLASSPATH设置,以便服务器程序可以运行。如果您已经设置了适当的CLASSPATH,您可以从命令文本中删除–cp选项和CLASSPATH值。

你需要设置一个java.rmi.server.codebase属性。如果 RMI 注册表和客户机程序需要下载它们没有的类文件,它们就会使用这个方法。该属性的值是一个 URL,它可以指向本地文件系统、web 服务器、FTP 服务器或任何其他资源。URL 可以指向一个 JAR 文件,就像本例中一样,也可以指向一个目录。如果它指向一个目录,URL 必须以正斜杠结束。以下命令使用文件夹作为其基本代码。如果 RMI 注册中心和客户机需要任何类文件,它们将尝试从 URL 文件/C:/myrmi/classes/下载类文件。

java -cp C:\utilserver.jar

-Djava.rmi.server.codebase=file:/C:/myrmi/classes/

com.jdojo.rmi.RemoteServer

您还可以设置一个java.rmi.server.codebase属性来指向一个 web 服务器,在那里您可以存储您需要的类文件,如下所示:

java -cp C:\utilserver.jar

-Djava.rmi.server.codebase=http://www.jdojo.com/rmi/classes/

com.jdojo.rmi.RemoteServer

如果将类文件存储在多个位置,可以指定所有位置,用空格分隔,如下所示:

java -cp C:\utilserver.jar

-Djava.rmi.server.codebase="http://www.jdojo.com/rmi/classes/

com.jdojo.rmi.RemoteServer

它将一个位置指定为目录,将另一个位置指定为 JAR 文件。一个使用http协议,另一个使用ftp协议。这两个值由一个空格隔开,它们在一行上,而不是如图所示的两行。当您运行服务器或客户端程序时,可能会出现ClassNotFoundException,这很可能是由于java.rmi.server.codebase属性的设置不正确,或者根本没有设置该属性。

运行 RMI 客户端程序

成功启动 RMI 注册表和服务器应用之后,就该启动 RMI 客户机应用了。您可以使用以下命令来运行客户端程序:

java -cp C:\utilclient.jar

-Djava.rmi.server.codebase=file:/C:/utilclient.jar

-Djava.security.policy=file:/c:/rmi.policy

com.jdojo.rmi.RemoteClient

当您运行上面的命令时,您不必包含一个java.rmi.server.codebase选项。客户端程序也可以使用以下命令运行。当客户端程序成功运行时,您应该能够在控制台上看到输出。运行该程序时,您可能会得到不同的输出,因为它会打印当前日期和时间以及运行服务器应用的服务器的时区信息。

java -cp C:\utilclient.jar

-Djava.security.policy=file:/c:/rmi.policy

com.jdojo.rmi.RemoteClient

Echo Message: Hello, Echo reply: Hello

Server Time: 2014-06-22T13:11:31.790-05:00[America/Chicago]

101 + 207 = 308

RMI 应用故障排除

在第一次运行 RMI 应用之前,很可能会出现许多异常。本节将列出一些您可能会收到的例外情况。它还将列出这些异常的一些可能原因和一些可能的解决方案。当您试图运行 RMI 应用时,不可能列出所有可能的错误。通过查看异常的栈输出,您应该能够找出大多数错误。

Java . RMI . stubnotfounindexception

当你试图运行一个服务器程序时,你得到一个StubNotFoundException。异常栈跟踪类似于以下内容:

java.rmi.StubNotFoundException: Stub class not found: com.jdojo.rmi.RemoteUtilityImpl_Stub; nested exception is:

java.lang.ClassNotFoundException: com.jdojo.rmi.RemoteUtilityImpl_Stub

at sun.rmi.server.Util.createStub(Util.java:292)...

这种异常可能由于多种原因而发生。以下是您可以寻找并解决的一些原因:

  • 您可能正在使用 Java 5 之前的 Java 版本运行服务器程序。您必须使用rmic命令创建存根和框架,并在运行服务器程序时让 JVM 可以访问它们。更多细节请参考“生成存根和框架”一节。
  • 当您导出远程对象并且不传递端口号时,可能会出现此错误:

RemoteUtility remoteUtilityStub =

(RemoteUtility)UnicastRemoteObject.exportObject(remoteUtility);

  • 如果您没有向UnicastRemoteObject类的exportObject()方法传递端口号来导出远程对象,您必须首先使用rmic命令生成存根和框架。更多细节请参考“生成存根和框架”一节。解决这个问题的另一种方法是向exportObject()方法传递一个端口号。端口号 0(零)表示匿名端口。

RemoteUtility remoteUtilityStub =

(RemoteUtility)UnicastRemoteObject.exportObject(remoteUtility,0);

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 的应用,并在端口 1009 启动rmiregistry应用。
  • 您可以在 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属性。

请在运行服务器和客户端程序时检查CLASSPATHjava.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.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.Square)在服务器 JVM 的CLASSPATH中,或者在运行远程客户端时设置java.rmi.server.codebase属性,以便服务器可以下载该类。

调试 RMI 应用

通过将名为java.rmi.server.logCalls的 JVM 属性设置为true,可以为 RMI 服务器应用打开 RMI 日志记录。默认情况下,它被设置为false。以下命令启动您的RemoteServer应用,将java.rmi.server.logCalls属性设置为true:

java -cp C:\utilserver.jar

-Djava.rmi.server.logCalls=true

–Djava.rmi.server.codebase="http://www.myurl.com/rmiclasses

com.jdojo.rmi.RemoteServer

当服务器 JVM 的java.rmi.server.logCalls属性被设置为true时,对服务器的所有传入调用以及在传入调用执行期间抛出的任何异常的栈跟踪都被记录到标准错误中。

RMI 运行时还允许您将服务器应用中的传入调用记录到一个文件中,而不考虑为服务器 JVM 的java.rmi.server.logCalls属性设置的值。您可以使用java.rmi.server.RemoteServer类的setLog(OutputStream out)静态方法将所有来电细节记录到一个文件中。通常,您在服务器程序代码的开头设置用于日志记录的文件输出流,比如您的com.jdojo.rmi.RemoteServer类的main()方法中的第一条语句。下面的代码片段支持将远程服务器应用中的调用记录到 C :\rmi.log文件中。您可以通过使用null作为setLog()方法中的OutputStream来禁用呼叫记录。

try {

java.io.OutputStream os = new java.io.FileOutputStream("C:\\rmi.log");

java.rmi.server.RemoteServer.setLog(os);

}

catch (FileNotFoundException e) {

System.err.println("Could not enable incoming calls logging.");

e.printStackTrace();

}

当安全管理器安装在服务器上时,允许记录到文件的运行代码必须有一个java.util.logging.LoggingPermission,其权限目标为“control”。Java 策略文件中的以下 grant 条目将授予该权限。您还必须在 Java 策略文件中授予日志文件(本例中为 C :\rmi.log)的“写”权限。

grant {

permission java.io.FilePermission "c:\\rmi.log", "write";

permission java.util.logging.LoggingPermission "control";

};

如果您想要获得关于 RMI 客户机应用的调试信息,那么在启动 RMI 客户机应用时,将一个非标准的sun.rmi.client.logCalls属性设置为true。它将显示关于标准错误的调试信息。由于该属性不是公共规范的一部分,因此在未来的版本中可能会被删除。关于调试选项的更多细节,您需要参考 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 -cp C:\utilserver.jar

–Djava.rmi.server.codebase="http://www.myurl.com/rmiclasses

-Djava.rmi.server.useCodebaseOnly=true

com.jdojo.rmi.RemoteServer

Tip

从 JDK 7u21 开始(也在 JDK 6u45 和 JDK 5u45 中),java.rmi.server.codebase属性的默认值被设置为true。它的默认值曾经是false。这意味着,默认情况下,不允许应用从其他 JVM 下载类。

java.rmi.server.useCodebaseOnly属性设置为true有两种含义:

  • 如果服务器需要一个类作为来自客户端的远程调用的一部分,它将总是在它的CLASSPATH中查找,或者它将使用您为服务器程序设置的java.rmi.server.codebase的值。在上面的例子中,服务器中的所有类都必须在它的CLASSPATH中或者在 URL http://www.myurl.com/rmiclasses 中找到。
  • 如果客户端需要在远程方法调用中使用新的类类型,新的类类型必须预先为服务器所知,因为服务器永远不会使用客户端关于从何处下载所需新类的指令(在客户端使用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 服务器授予远程对象 10 分钟的租约。现在,以下是一些可能性:

  • 客户机可以在其从服务器获得租用的时间段内完成远程对象引用。
  • 客户可能希望将租期延长一段时间。
  • 客户端崩溃。服务器不接收来自客户端的任何消息,并且客户端获取的远程引用的租期到期。

让我们看看每一种可能性。客户端在三种不同的情况下向服务器发送消息。它在第一次收到远程引用时就发送一条消息。它告诉服务器它有一个远程对象的引用。第二次,当它想要更新远程引用的租约时,它向服务器发送一条消息。第三次,当完成远程引用时,它向服务器发送一条消息。事实上,当一个远程引用在客户机应用中被垃圾收集时,它会向服务器发送一条消息,表明它已经完成了对远程对象的处理。在内部,远程客户端发送给服务器的消息只有两种类型:脏的和干净的。发送脏消息以获得租约,发送干净消息以移除/取消租约。这两条消息使用java.rmi.dgc.DGC接口的dirty()clean()方法从远程客户端发送到服务器。作为一名开发人员,除了可以自定义租期之外,您对这些消息(发送或接收)没有任何控制权。租用时间段控制这些消息发送到服务器的频率。

当一个客户机完成一个远程对象引用时,它向服务器发送一个消息,表明它已经完成了。当客户机的 JVM 中的远程引用被垃圾收集时,发送该消息。因此,重要的是,一旦使用完毕,就将客户端程序代码中的远程引用设置为null。否则,服务器将继续保留远程对象,即使远程客户端不再使用它。您无法控制该消息从远程客户端发送到服务器的时间。要加快这个消息的发送,您所能做的就是将客户机代码中的远程对象引用设置为null,这样 garage collector 将尝试对它进行垃圾收集,并向服务器发送一个干净的消息。

RMI 运行时跟踪远程客户端 JVM 中远程引用的租约。当租约到期到一半时,远程客户端会向服务器发送一个租约续订请求,并续订租约。当远程客户机的租约为远程引用续订时,服务器会跟踪租约到期时间,并且不会对远程对象进行垃圾收集。理解为远程引用设置租期的重要性是很重要的。如果太小,大量的网络带宽将用于频繁更新租约。如果它太大,服务器将保持远程对象存活更长时间,以防客户端完成其远程引用,并且不通知服务器取消租用。我将简要讨论如何在 RMI 应用中设置租期值。

如果服务器没有从远程客户机听到任何关于客户机已经获得的远程引用的租用的消息,则在租用期到期后,它简单地取消租用并将该远程对象的引用计数减 1。这种由服务器做出的单方面决定对于处理行为不良的远程客户端(没有告诉服务器它是通过远程引用完成的)或任何可能阻止远程客户端与服务器通信的网络/系统故障非常重要。

当所有客户机都完成了对一个远程对象的远程引用时,它在服务器中的引用计数将下降到零。当远程客户端的租约到期或者它已经向服务器发送了干净的消息时,远程客户端被认为完成了远程引用。在这种情况下,RMI 运行时将使用弱引用来引用远程对象,因此如果没有对远程对象的本地引用,它可能会被垃圾收集。

默认情况下,租期设置为 10 分钟。当您启动 RMI 服务器时,您可以使用java.rmi.dgc.leaseValue属性来设置租期。租期的值以毫秒为单位指定。以下命令启动服务器程序,租期设置为 5 分钟(300000 毫秒)。命令文本在一行中输入,两部分用空格分开,而不是像所示的那样用换行符分开;为了清楚起见,我使用了换行符来分隔命令的各个部分。

java -cp C:/utilserver.jar

-Djava.rmi.dgc.leaseValue=300000

-Djava.rmi.server.codebase=file:/C:/utilserver.jar

com.jdojo.rmi.RemoteServer

除了设置租用时间段之外,所有事情都由 RMI 运行时处理。RMI 运行时为您提供了关于远程对象垃圾收集的更多信息。它可以告诉你远程对象的引用计数何时降到零。如果一个远程对象持有一些资源,而您希望在没有远程客户端引用它时释放这些资源,那么得到这个通知是很重要的。很容易得到这个通知。你所要做的就是在你的远程对象实现类中实现java.rmi.server.Unreferenced接口。其声明如下:

public interface Unreferenced {

void unreferenced()

|

当远程对象的远程引用计数变为零时,调用unreferenced()方法。如果你想在你的例子中为RemoteUtility远程对象得到一个通知,你需要修改RemoteUtilityImpl类的声明,如清单 7-6 所示。

清单 7-6。RemoteUtilityImpl 类的修改版本,它实现未引用的接口

// RemoteUtilityImpl.java

package com.jdojo.rmi;

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());

}

}

您可能会注意到,这一次,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接口继承的接口(称为远程接口)。接口中的所有方法必须包含一个至少抛出RemoteExceptionthrows子句。远程对象的类必须实现远程接口。服务器应用创建实现远程接口的类的对象,导出该对象以给出真实远程对象的状态,并将其注册到rmiregistry。客户端应用只需要远程接口。

如果这三个应用中的任何一个需要本地没有的类,它们可以在运行时动态下载。对于动态下载类的 JVM 来说,java.rmi.server.useCodebaseOnly属性必须设置为false。默认情况下,它被设置为true,这将禁止动态下载 JVM 中的类。除了远程对象引用,JVM 还接收名为java.rmi.server.codebase的属性值,这是 JVM 可以下载(如果它自己的java.rmi.server.useCodebaseOnly属性设置允许的话)使用远程对象引用所需的类的 URL。

RMI 应用中有几个组件协同工作,这使得调试变得很困难。通过在 JVM 属性java.rmi.server.logCalls设置为true的情况下运行 RMI 服务器,您可以记录对它的所有调用。所有对服务器的调用都将被记录为标准错误。您还可以将 RMI 服务器调用记录到文件中。

RMI 为运行在 RMI 服务器中的远程对象提供自动垃圾收集。远程对象的垃圾收集基于引用计数和租约。当客户机应用获得远程对象的引用时,它也从服务器应用获得远程对象的租约。租约在一段时间内有效。只要客户端应用保留远程对象引用,它就会定期更新租约。服务器应用跟踪远程对象的引用计数和租约。当客户机应用完成远程引用时,它向服务器应用发送一条消息,服务器应用将远程对象的引用计数减 1。当远程对象的引用计数在服务器应用中减少到零时,远程对象被垃圾收集。

八、Java 本地接口

在本章中,您将学习

  • 什么是 Java 本地接口(JNI)
  • 如何编写使用本地方法的 Java 程序
  • 如何编写 C++程序来实现本机方法
  • 如何在 Windows 和 Linux 上为 Java 中使用的方法的本地实现创建一个共享库
  • Java 类型和 JNI 类型之间的数据类型映射
  • 如何在本机代码中使用 Java 字符串和数组
  • 如何创建 Java 对象,并在本机代码中访问这些对象的字段和方法
  • 本机代码中的异常处理
  • 如何在本机代码中嵌入 JVM
  • 如何在本机代码中使用 JNI 处理线程同步

什么是 Java 原生接口?

Java 本地接口(Java Native Interface,JNI)是一种编程接口,它有助于 Java 代码与用 C、C++、FORTRAN 等本地语言编写的代码之间的交互。JNI 支持直接从 Java 调用 C 和 C++函数。如果需要使用用任何其他语言(如 FORTRAN)编写的本机代码,可以使用 C/C++包装函数从 Java 中调用它。互动可以双向进行。Java 代码可以调用本机代码,反之亦然,如图 8-1 所示。

A978-1-4302-6662-4_8_Fig1_HTML.jpg

图 8-1。

The JNI architecture

Java 使用本机方法调用本机代码。Java 上下文中的本地方法是用 Java 声明并以本地语言(如 C/C++)实现的方法。本机方法实现被编译成一个共享库,由 JVM 加载。共享库在 Windows 上称为动态链接库(DLL ),在 UNIX 上称为共享对象(SO)。在 Java 代码中,以同样的方式调用 Java 方法和本机方法。Java 程序被编译成一种与平台无关的格式,称为字节码。本机代码被编译成依赖于平台的格式。因此,如果一个 Java 应用使用本机代码,它就不能再移植到其他平台,除非您在所有平台上开发相同的共享库。有时,您可能会访问本机代码中特定于平台的特性,这些特性是从 Java 应用中使用的;在这种情况下,您应该知道您的 Java 应用不能在其他平台上运行。

当 Java 通过其类库提供了丰富的特性时,为什么还会有人使用 JNI 呢?出于以下原因,可能有必要使用 JNI 来访问 Java 中的本机代码:

  • 如果一个 Java 应用需要实现一些特定于平台的特性,而这些特性使用 Java APIs 是不可能实现的。
  • 您可能已经有了用本地语言编写的遗留代码,并且希望在 Java 应用中重用它。
  • 您正在开发一个时间关键的 Java 应用,其中 Java 代码的执行速度没有预期的快。您可以将 Java 代码中时间关键的部分转移到本机代码中。

您应该考虑在 Java 应用中使用 JNI 作为最后的手段。您必须探索使用 Java APIs 实现所需特性的所有可能性。使用 JNI 还会改变开发应用所需的技能。从事 Java 应用开发的开发人员要么接受本地语言(C/C++)的培训,要么让懂本地语言的新开发人员加入团队。在 Java 应用中使用本机代码会降低应用的稳定性,并且容易出现安全风险,因为本机代码是在 JVM 之外运行的。

在本章中,我将使用 C++来实现本地方法。你可以用 C 语言来代替。本章中列出的所有 C++代码示例只需稍加修改就可以移植到 C 语言中。每当您需要对 C++代码进行更改以将其转换为 C 时,我都会详细说明 C++代码和 C 代码之间的区别。

系统需求

你需要一个能创建共享库的 C 或 C++编译器。您还需要在计算机上安装一个 JDK 来生成 C/C++头文件。本章中引用的本机代码是使用 NetBeans 8.0 和 Cygwin 作为 Windows 平台上的 C++编译器开发的。Java 8 用于编译和运行 Java 代码。然而,使用 Cygwin 作为 C++编译器并不是运行任何示例的必要条件。您可以使用任何其他 C/C++编译器在您的平台上创建共享库。请访问 https://netbeans.org/kb/trails/cnd.html 了解更多关于如何配置 NetBeans 使用 C++的详细信息。

JNI 入门

开发使用 JNI 的 Java 应用包括以下步骤:

  • 编写 Java 程序
  • 编译 Java 程序
  • 创建 C/C++头文件
  • 编写 C/C++程序
  • 创建共享库
  • 运行 Java 程序

后续部分将详细讨论每个步骤。

编写 Java 程序

使用 JNI 的 Java 程序与纯 Java 程序的区别仅在于两个方面:

  • 加载共享库
  • 声明本机方法

包含本机方法实现的共享库必须在 Java 调用本机方法之前加载。使用java.lang.System类的loadLibrary(String libraryNameWithoutExtension)静态方法加载共享库,如下所示:

// Load a shared library named beginningjava

System.loadLibrary("beginningjava");

您还可以使用java.lang.Runtime类的loadLibrary()方法加载共享库。在内部,System类的loadLibrary()方法调用了Runtime类的loadLibrary()方法。上述代码可以重写如下:

// Load the shared library

Runtime.getRuntime().loadLibrary("beginningjava");

注意,您需要向loadLibrary()方法传递一个共享库名,不带任何特定于平台的前缀和文件扩展名。例如,如果您的共享库文件名在 Windows 上是beginningjava.dll或者在 UNIX 上是beginningjava.so,那么您需要使用beginningjava作为共享库名。loadLibrary()方法将添加文件扩展名来查找共享库。这样,您不需要更改 Java 代码,如果您打算在不同的平台上运行相同的 Java 代码,Java 代码会加载共享库。

你也可以使用SystemRuntime类的load()方法加载一个共享库。load()方法接受带有文件扩展名的共享库的绝对路径。如果 Windows 平台上的beginningjava.dll文件在C:\myjni目录中,那么对load()方法的调用将如下所示:

// Load the shared library

System.load("C:\\myjni\\beginningjava.dll");

注意,使用load()方法迫使您使用共享库的绝对路径和文件扩展名,这使得您的 Java 代码不可移植到其他平台。在本章的例子中,你将使用System类的loadLibrary()方法来加载共享库。如果无法加载特定的库,load()loadLibrary()方法会抛出一个java.lang.UnsatisfiedLinkError

loadLibrary()方法如何在只知道库名的情况下找到文件系统中的共享库文件?有两种方法可以让 JVM 知道共享库的位置:

  • 将包含共享库的目录包含到 Windows 上的PATH环境变量和 UNIX 上的LD_LIBRARY_PATH环境变量中。
  • 使用java.library.path JVM 属性作为命令行选项,指定包含共享库的目录(或多个目录,用分号分隔)。以下命令假设beginningjava共享库位于C:\myjni\lib目录中:java -Djava.library.path=C:\myjni\lib your-class-name-to-run

Java 中使用的本机方法没有用 Java 编写的主体,因为它的实现存在于本机代码中。但是,在使用本机方法之前,需要用 Java 声明它。它是使用native关键字声明的。Java 代码中的native方法声明以分号结束。下面的代码片段声明了一个名为hello()native方法,它没有参数并返回void

public class Test {

// Declare a native method called hello()

public``native

}

用 Java 代码调用本机方法与调用任何其他 Java 方法是一样的。

Test test = new Test();

test.hello();

您可以声明一个本机方法具有publicprivateprotected或包级范围。一个native方法可以被声明为static或非静态的。一个 Java 类中可以有任意多的native方法。

不能将native方法声明为abstract。这意味着一个接口不能有native方法,因为在一个接口中声明的所有方法如果没有声明为staticdefault,那么它们都是abstract。一个abstract方法意味着该方法的实现缺失,它将在 Java 中实现,而native方法意味着该方法的实现缺失,它在本机代码中实现。将一个方法同时声明为nativeabstract将会混淆在哪里寻找该方法的实现——在 Java 代码中还是在本地代码中。这就是方法声明不能使用两个修饰符abstractnative组合的原因。

关键字native只能用于声明方法。您不能将字段声明为native。下面的代码片段声明了两个名为WillCompileWontCompile的类。类WillCompile包含了native关键字的有效用法,而类WontCompile展示了native关键字的无效用法。

public class WillCompile {

public native void m1();

private native void m2();

protected native void m3();

native void m4();

public static native void m5();

public native int m6(String str);

// A non-native method (Java-only method)

public int add(int a, int b) {

return a + b;

}

}

// Sample of Illegal use of native keyword in a Java class

public class WontCompile {

// A field cannot be native

private native String name;

// A method cannot be abstract as well as native

public abstract native String getName();

}

现在,您已经准备好编写 Java 代码来调用您的第一个本机方法。您将把您的native方法命名为hello()。它不接受任何参数,也不返回任何值。稍后您将在 C++中实现它,它将在标准输出中打印一条消息Hello JNI。清单 8-1 给出了HelloJNI类的完整代码。

清单 8-1。使用名为 hello()的本机方法的 HelloJNI 类

// HelloJNI.java

package com.jdojo.jni;

public class HelloJNI {

static {

// Load the shared library using its name only

System.loadLibrary("beginningjava");

}

// Declare the native method

public native void hello();

public static void main(String[] args) {

// Create a HelloJNI object

HelloJNI helloJNI = new HelloJNI();

// Call the native method

helloJNI.hello();

}

}

HelloJNI类执行三件事情:

  • 它在静态初始化器中加载一个beginningjava共享库(Windows 上的beginningjava.dll和类 UNIX 操作系统上的beginningjava.so)。注意,当你编写和编译HelloJNI类时,你不需要有beginningjava共享库。运行HelloJNI类时需要共享库。static { System.loadLibrary("beginningjava "); }
  • 它声明了一个名为hello()native方法,稍后将在 C++代码中实现。public native void hello();Java 编译器会用hello() native方法声明编译HelloJNI类,而不需要实现该方法的本机代码。在运行时调用该方法时,将需要该方法的实现。
  • 它在main()方法中创建了一个HelloJNI类的对象,并对该对象调用了hello()方法。HelloJNI helloJNI = new HelloJNI(); helloJNI.hello();

HelloJNI类的代码很简单。使用native方法并不需要在 Java 代码中做什么特别的事情。您还不能运行这个类,因为当您运行它的时候,它会寻找一个包含hello()方法本机代码的beginningjava共享库,您还没有编写这个库。

编译 Java 程序

编译使用本机方法的 Java 程序与编译任何其他 Java 程序是一样的。在编译HelloJNI类时,没有什么特殊的设置需要应用。您可以使用javac命令来编译它,就像这样:

javac HelloJNI.java

该命令将生成一个HelloJNI.class文件,该文件将包含HelloJNI类的类定义,其完全限定名为com.jdojo.jni.HelloJNI。确保您有可用的HelloJNI.class文件,因为它是执行下一步所必需的。

创建 C/C++头文件

在开始用 C/C++编写本机方法的代码之前,您需要生成一个头文件,其中包含您的 C/C++方法声明。当您编写您的hello() native方法的实现时,您将使用这个头文件。Java 和 C/C++中的hello()方法的方法签名差别很大。

您不需要担心如何在 C/C++中编写方法签名的细节,Java 代码将使用这些签名。JDK 提供了一个名为javah的工具,可以为您生成所有需要的头文件。javah工具位于JDK_HOME\bin文件夹中,其中JDK_HOME是 JDK 的安装文件夹。例如,如果你在 Windows 的C:\java8目录中安装了 JDK,那么javah工具就在C:\java8\bin中。该工具接受 Java 类的完全限定类名,并生成一个扩展名为.h的头文件,其中包含指定类中声明的所有native方法的方法签名。以下命令将为HelloJNI类中的所有native方法声明生成一个 C/C++头文件:

javah com.jdojo.jni.HelloJNI

javah工具将在CLASSPATH中寻找HelloJNI类。如果不在CLASSPATH中,可以使用–classpath或- cp命令行选项指定CLASSPATH,如下所示:

javah –cp C:\myclasses com.jdojo.jni.HelloJNI

该命令将在当前目录下生成一个名为com_jdojo_jni_HelloJNI.h的头文件。默认情况下,生成的文件名基于该类的完全限定名。类名中的点被替换为下划线,文件的扩展名为.h。您还可以通过使用一个–o选项来指定javah命令将生成的头文件名称。您可以通过执行javah –help命令来查看javah命令支持的其他选项。清单 8-2 显示了com_jdojo_jni_HelloJNI.h文件的内容。

清单 8-2。com_jdojo_jni_HelloJNI.h 文件的内容

/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

/* Header for class com_jdojo_jni_HelloJNI */

#ifndef _Included_com_jdojo_jni_HelloJNI

#define _Included_com_jdojo_jni_HelloJNI

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class:    com_jdojo_jni_HelloJNI

* Method:    hello

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_com_jdojo_jni_HelloJNI_hello

(JNIEnv *, jobject);

#ifdef __cplusplus

}

#endif

#endif

您不需要担心头文件中的细节。您只需要为您的native hello()方法生成的方法签名。Java 代码中的方法签名void hello()已经被翻译成 C/C++代码的如下方法签名:

JNIEXPORT void JNICALL Java_com_jdojo_jni_HelloJNI_hello (JNIEnv *, jobject);

JNIEXPORTJNICALL是两个宏。关键字void表示native方法不返回任何值。javah命令使用一个规则在头文件中生成native方法的名称。在这种情况下,方法名是Java_com_jdojo_jni_HelloJNI_hello。稍后我将讨论javah工具使用的命名规则的细节。虽然 Java 代码中的hello()方法的方法声明不接受任何参数,但是头文件中的native方法声明接受两个参数。将本地语言中的所有本地方法声明接受比 Java 代码中声明的参数数量多两个的参数作为一条规则。附加参数作为本地语言中的方法的第一和第二参数被添加。第一个参数是一个指向JNIEnv类型对象的指针,这是一个函数指针表,便于本地环境和 Java 环境之间的交互。第二个参数的类型为jobjectjclass。如果native方法在 Java 代码中被声明为非静态的,那么第二个参数的类型是jobject,它是对调用本机方法的 Java 对象的引用。它类似于 Java 中每个非静态方法内部都有的this引用。由于 Java 中的本机hello()方法已经被声明为非静态的,所以第二个参数类型是类型jobject。如果本地方法在 Java 中被声明为 static,第二个参数将是类型jclass,它将是对 JVM 中调用native方法的类对象的引用。

在这一步的最后,您应该有一个名为com_jdojo_jni_HelloJNI.h的头文件,其内容如清单 8-2 所示。

编写 C/C++程序

清单 8-3 显示了您需要为hello()本地方法编写的 C/C++代码。下一节将介绍使用 NetBeans IDE 设置项目和编写 C++代码的分步过程。C++的源代码文件被命名为hellojni.cpp。在这种情况下,如果您选择使用 C 语言,代码将是相同的。注意,hello是 Java 代码中本地方法的名称,而在 C/C++中它被命名为Java_com_jdojo_jni_HelloJNI_hello

清单 8-3。hello()本机方法的 C/C++实现

// hellojni.cpp

#include <stdio.h>

#include <jni.h>

#include "com_jdojo_jni_HelloJNI.h"

JNIEXPORT void JNICALL Java_com_jdojo_jni_HelloJNI_hello(JNIEnv *env, jobject obj) {

printf("Hello JNI\n");

return;

}

这是这个程序做的事情。它使用三个 C/C++编译器预处理器include指令来包含三个头文件:stdio.hjni.hcom_jdojo_jni_HelloJNI.h。它包括使用标准输入/输出功能的stdio.h,使用 JNI 相关功能的jni.h,以及包含与您的hello() native方法相关的功能的com_jdojo_jni_HelloJNI.h

安装 JDK 时,jni.h文件被复制到JDK_HOME\include目录。例如,如果你在C:\java8安装了 JDK,那么jni.h文件将会在C:\java8\include目录下。在JDK_HOME\include目录下有一个子目录。子目录名称取决于平台。它在 Windows 上被命名为win32,在 Linux 上被命名为linux,等等..在编译hellojni.cpp文件时,需要使用以下两个目录作为包含路径选项:

  • C:\java8\include
  • C:\java8\include\win32

这些内含路径是给窗户用的。请根据您的平台进行更改。

您可以将com_jdojo_jni_HelloJNI.h文件放在机器上的任何目录中。在编译hellojni.cpp文件时,您需要在 include-path 选项中包含包含该文件的目录。

函数签名是从com_jdojo_jni_HelloJNI.h头文件中复制的。您将这两个参数命名为envobj。在代码中为这些参数使用什么名称并不重要。

JNIEXPORT void JNICALL Java_com_jdojo_jni_HelloJNI_hello

(JNIEnv *env, jobject obj)

您已经通过添加两个语句为native方法提供了实现。第一条语句使用printf()函数在标准输出上打印消息Hello JNI,第二条语句从函数返回,如下所示:

printf("Hello JNI\n");

return;

创建共享库

在本节中,您将把hellojni.cpp文件编译成一个名为beginningjava的共享库。共享库在 Windows 上是一个名为beginningjava.dll的文件,在类 UNIX 操作系统上是一个名为beginningjava.so的文件。您的操作系统可能对共享库使用不同的文件扩展名。有许多编译器可用于从 C/C++代码创建共享库。本节说明如何在上创建共享库

  • Windows 使用名为g++的 GNU C++编译器,被称为 MinGW 编译器(Minimalist GNU for Windows)。
  • 在 Fedora Linux 上使用名为g++.的 GNU C++编译器

要创建共享库,您可以在命令提示符或 IDE(如 Windows 上的 Microsoft Visual Studio 或 Windows 和 Linux 上的 NetBeans)上使用 C/C++编译器。请注意,NetBeans 没有附带 C/C++编译器。要使用 NetBeans IDE 创建共享库,您需要下载一个编译器,如 MinGW 或 Cygwin。

在 Windows 上创建共享库

以下部分描述了如何在 Windows 上安装名为g++的 MinGW C++编译器,以及如何通过命令提示符使用它来创建名为beginningjava.dll的共享库。

安装 MinGW C/C++编译器

按照以下步骤安装 MinGW 编译器:

  • http://sourceforge.net/projects/mingw 下载 MinGW 编译器并安装在你的机器上。
  • 假设您已经在C:\MinGW目录中安装了 MinGW。您需要安装 MinGW 的以下软件包:mingw-developer-toolkit、migw32-base、mingw32-gcc-g++和 msys-base。如果您已将 MinGW 安装在另一个目录中,请在本节的以下讨论中将此目录路径替换为您的安装目录路径。
  • C:\MinGW\bin目录添加到系统PATH环境变量中。如果不设置系统 PATH 环境变量,则可以通过在命令提示符下设置 PATH 环境变量来使用 MinGW。
  • 验证C:\MinGw\bin\g++.exe文件存在于您的机器上。g++C++编译器,gcc是 MinGW 使用的 C 编译器。你将使用本章中的 C++代码和 g++编译器来编译 C++代码。
使用 g++命令

您需要使用g++命令来创建一个共享库。创建共享库需要两种类型的文件:

  • 包含 C++代码的 C++源文件。在这种情况下,您将其命名为hellojin.cpp,如清单 8-3 所示。
  • 清单 8-2 所示的com_jdojo_jni_HelloJNI.h头文件。
  • 与 JNI 相关的头文件位于JDK_HOME\include和 J DK_HOME\include\win32目录中,其中JDK_HOME是您安装 JDK 的目录。

您可以向g++编译器传递几个选项。以下命令显示了创建共享库所需的最少选项:

g++ -Wl,--kill-at -shared –I<include-dir> -o <output-file> <source-files>

这里,

  • -Wl,<option>用于将选项传递给链接器。<option>是一个逗号分隔的链接器选项列表。在这个命令中,您将--kill-at选项传递给链接器,以便在导出符号之前从符号中去除标准调用后缀(@nn)。如果您没有指定这个选项,当您运行使用共享库的 Java 程序时,您将得到一个java.lang.UnsatisfiedLinkError
  • –shared选项表示您想要创建一个共享库。
  • –I<include-dir>选项用于传递包含头文件的目录(。h 文件)。您可以对每个目录重复此选项一次。
  • –o <output-file>选项指定输出文件名。在您的例子中,您将使用名为beginningjava.dll的输出文件。
  • <source-files>是用空格分隔的 C++源文件列表。

为了简化生成共享库的命令语法,我假设您的计算机上存在以下目录和文件:

  • C:\dll\hellojni.cpp
  • C:\dll\com_jdojo_jni_HelloJNI.h
  • C:\java8\include
  • C:\java8\include\win32

以下命令将在C:\dll目录中生成beginingjava.dll文件。为了清楚起见,命令的每个部分都显示在单独的行中;您将在一行中输入整个命令。

C:\> g++ -Wl,--kill-at -shared

-IC:/java8/include -IC:/java8/include/win32 -IC:/dll

-o C:/dll/beginningjava.dll

C:/dll/hellojni.cpp

请注意文件路径中正斜杠的使用。对于 Windows 上的 g++命令,您可以使用正斜杠或反斜杠作为路径分隔符。请更改命令中的路径,以匹配您计算机上这些文件和目录的路径。

如果您没有将 PATH 环境变量设置为C:\MinGW\bin目录,当您运行g++命令时,您可能会得到以下错误:

'g++' is not recognized as an internal or external command,operable program or batch file

Note

在 Windows 上,如果您想将 NetBeans IDE 与 MinGW 一起使用,请参考以下链接获取设置说明: https://netbeans.org/community/releases/80/cpp-setup-instructions.html .

在 Linux 上创建共享库

以下部分描述了如何在 Fedora Linux 上安装名为g++的 GNU C++编译器,以及如何在终端上使用它来创建名为beginningjava.so的共享库。

安装 MinGW C/C++编译器

在 Linux 上安装g++编译器很容易。在 Linux 终端上运行以下命令将安装g++编译器:

$ yum install gcc-c++

运行该命令时,您可能会收到以下消息:

$ yum install gcc-c++

You need to be root to perform this command.

$

如果您收到此消息,您需要以 root 用户身份登录来安装编译器。使用su –命令以 root 身份登录,在出现提示时输入 root 密码,然后运行yum命令。

$ su –

Password: Enter Your Password Here

# yum install gcc-c++

在安装过程中,yum命令会多次提示您确认编译器安装文件的下载。当你得到这些提示时,你需要回答是。如果您的机器上已经安装了 g++编译器,那么yum命令将会打印一条相应的消息。

这就是在 Linux 上安装 g+=编译器的全部工作。

使用 g++命令

您需要使用g++命令来创建一个共享库。创建共享库需要两种类型的文件:

  • 包含 C++代码的 C++源文件。在这种情况下,您将其命名为hellojni.cpp,如清单 8-3 所示。
  • 清单 8-2 所示的com_jdojo_jni_HelloJNI.h头文件。
  • 与 JNI 相关的头文件位于JDK_HOME/include和 J DK_HOME/include/win32目录中,其中JDK_HOME是您安装 JDK 的目录。

您可以向g++编译器传递几个选项。以下命令显示了创建共享库所需的最少选项:

g++ -shared –I<include-dir> -o <output-file> <source-files>

在这里,

  • –shared选项表示您想要创建一个共享库。
  • –I<include-dir>选项用于传递包含头文件的目录(。h文件)。您可以对每个目录重复此选项一次。
  • –o <output-file>选项指定输出文件名。在您的例子中,您将使用输出文件名beginningjava.so
  • <source-files>是用空格分隔的 C++源文件列表。

为了简化生成共享库的命令语法,我假设您的计算机上存在以下目录和文件:

  • /home/ksharan/slib/hellojni.cpp
  • /home/ksharan/slib/com_jdojo_jni_HelloJNI.h
  • /home/ksharan/java8/include
  • /home/ksharan/java8/include/linux

以下命令将在/home/ksharan/slib目录中生成beginingjava.so文件。为了清楚起见,命令的每个部分都显示在单独的行中;您将在一行中输入整个命令。

$ g++ -shared

-I/home/ksharan/java8/include -I/home/ksharan/java8/include/linux -I/home/ksharan/slib

-o /home/ksharan/slib/beginningjava.so

/home/ksharan/slib/hellojni.cpp

请更改命令中的路径,以匹配您计算机上这些文件和目录的路径。

Note

在 Linux 上,如果您想将 NetBeans IDE 与 g++编译器一起使用,请参考下面的设置说明链接: https://netbeans.org/community/releases/80/cpp-setup-instructions.html .

运行 Java 程序

在继续运行 Java 类之前,请确保您能够创建共享库(Windows 上的beginningjava.dll文件和类 UNIX 操作系统上的beginningjava.so文件)。如果您不能创建共享库,您可以使用本书源代码提供的共享库。共享库位于一个名为cplusplus的目录中。

现在你已经准备好运行你的HelloJNI Java 类了,如清单 8-1 所示。假设您已经将beginningjava共享库文件放在了C:\myjni\lib目录中。使用以下命令运行HelloJNI类:

C:\> java -Djava.library.path=C:\myjni\lib com.jdojo.jni.HelloJNI

-Djava.library.path=C:\myjni\lib选项指示 JVM 在C:\myjni\lib目录中寻找共享库。如果上述命令成功运行,它将在标准输出中打印一条消息Hello JNI。或者,您也可以将包含共享库的目录添加到 PATH 环境变量中,Java 运行时会找到它。如果共享库在当前目录中,Windows 也将找到共享库,而不设置java.library.path选项。下列命令显示如何为当前会话设置 PATH 环境变量(在 Windows 上)并运行类:

C:\> SET PATH=C:\myjni\lib;%PATH%

C:\> java com.jdojo.jni.HelloJNI

本机函数命名规则

该命令使用基于名称管理的命名规则,在 C/C++头文件中生成本机方法名称。Java 运行时使用相同的规则将 Java 本地方法名解析为共享库中的本地函数名。使用名称管理规则,以便为本机函数生成的名称是没有名称冲突的有效 C/C++名称。您可以将名称篡改视为简单地用组成有效函数名的字符替换无效字符。本机函数名是基于以下部分生成的,这些部分用下划线连接在一起:

  • 方法名以单词Java开头。
  • 包含本机方法声明的 Java 类的包的完全限定名。下划线用作包/子包分隔符。
  • Java 中的本机方法名。
  • 对于重载的本机方法,两个下划线后跟被破坏的方法的签名

Java 运行时对本地函数使用两个名称——一个短名称和一个长名称。短名称不使用两个下划线后跟被破坏的方法的签名。Java 运行时首先在共享库中搜索简称。如果没有找到使用短名称的函数,它将使用长名称进行搜索。被破坏的名称使用了表 8-1 中所示的转换表。

表 8-1。

The Escape Sequence Used in the Name-Mangling Process

| 原始字符 | 替代字符 | | --- | --- | | 任何非 ASCII Unicode 字符 | `_0xxxx`注意 _ `oxxxx`中使用的字母都是小写的,比如`_0abcd` | | `_`(下划线) | `_1` | | `;`(一个分号) | `_2` | | ``(一个开始方括号) | `_3` |

诸如分号和以方括号开头的字符可能作为 Java 内部使用的方法参数签名的一部分出现。表 [8-2 显示了。Java 代码和 Java 内部使用的方法签名。

表 8-2。

Examples of Java Method’s Declaration and Internally Used Method Signatures

| 方法声明 | 内部使用的方法签名 | | --- | --- | | `public static void javaPrintMsg(java.lang.String)` | `(Ljava/lang/String;)V` | | `public void javaCallBack()` | `()V` | | `public static void main(java.lang.String[])` | `([Ljava/lang/String;)V` |

如果您声明了一个类型为java.lang.String的参数,它在内部被用作Ljava/lang/String;。要了解 Java 内部使用的方法的签名,您需要使用带有–s选项的javap命令。下面的命令将打印出com.jdojo.jni.HelloJNI类中所有方法的方法签名。您可以使用–private选项打印所有方法的签名,包括private方法。

javap -s -private com.jdojo.jni.HelloJNI

如果您需要在本地代码的 JNI 函数中使用 Java 方法的方法签名,您应该运行javap命令来获取签名,而不是手动输入。您可以学习用于组成 Java 内部使用的方法签名的规则。然而,使用javap命令可以很容易地获得这些信息。让我们考虑一下清单 8-4 所示的Test类中一些本地方法的声明。

清单 8-4。带有一些本机方法声明的测试类

package com.jdojo.jni;

public class Test {

private native void sayHello();

private native void printMsg(String msg);

private native int[] increment(int[] num, int incrementValue);

private native double myMethod(int i, String s[], String ss);

private native double myMethod(double i, String s[], String ss);

private native double myMethod(short i, String s[], String ss);

}

如果编译Test类并运行命令

javah com.jdojo.jni.Test

您会得到一个com_jdojo_jni_Test.h头文件,其内容如清单 8-5 所示。

清单 8-5。为 com.jdojo.jni.Test 类生成的头文件

/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

/* Header for class com_jdojo_jni_Test */

#ifndef _Included_com_jdojo_jni_Test

#define _Included_com_jdojo_jni_Test

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class:    com_jdojo_jni_Test

* Method:    sayHello

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_com_jdojo_jni_Test_sayHello

(JNIEnv *, jobject);

/*

* Class:    com_jdojo_jni_Test

* Method:    printMsg

* Signature: (Ljava/lang/String;)V

*/

JNIEXPORT void JNICALL Java_com_jdojo_jni_Test_printMsg

(JNIEnv *, jobject, jstring);

/*

* Class:    com_jdojo_jni_Test

* Method:    increment

* Signature: (II)[I

*/

JNIEXPORT jintArray JNICALL Java_com_jdojo_jni_Test_increment

(JNIEnv *, jobject, jintArray, jint);

/*

* Class:    com_jdojo_jni_Test

* Method:    myMethod

* Signature: (I[Ljava/lang/String;Ljava/lang/String;)D

*/

JNIEXPORT jdouble JNICALL Java_com_jdojo_jni_Test_myMethod__I_3Ljava_lang_String_2Ljava_lang_String_2

(JNIEnv *, jobject, jint, jobjectArray, jstring);

/*

* Class:    com_jdojo_jni_Test

* Method:    myMethod

* Signature: (D[Ljava/lang/String;Ljava/lang/String;)D

*/

JNIEXPORT jdouble JNICALL Java_com_jdojo_jni_Test_myMethod__D_3Ljava_lang_String_2Ljava_lang_String_2

(JNIEnv *, jobject, jdouble, jobjectArray, jstring);

/*

* Class:    com_jdojo_jni_Test

* Method:    myMethod

* Signature: (S[Ljava/lang/String;Ljava/lang/String;)D

*/

JNIEXPORT jdouble JNICALL Java_com_jdojo_jni_Test_myMethod__S_3Ljava_lang_String_2Ljava_lang_String_2

(JNIEnv *, jobject, jshort, jobjectArray, jstring);

#ifdef __cplusplus

}

#endif

#endif

您可以查看为不同的native方法声明生成的本地函数名。不要担心用于函数参数的数据类型。我将在下一节介绍 Java 和本地语言之间的数据类型映射。

数据类型映射

JNI 定义了 Java 中使用的数据类型和本地函数之间的映射。表 [8-3 列出了 Java 和本地 C/C++语言之间的原始数据类型映射。请注意,在 Java 中,您只需在原始数据类型的名称前添加一个j,就可以在 C/C++中获得等效的数据类型名称。JNI 还定义了一个名为jsize的数据类型,用来存储长度,比如数组或者字符串的长度。

表 8-3。

The Mapping Between Java Primitive Data Types and JNI Native Data Types

| Java 原始类型 | 本地原始类型 | 描述 | | --- | --- | --- | | `boolean` | `jboolean` | 无符号 8 位 | | `byte` | `jbyte` | 带符号的 8 位 | | `char` | `jchar` | 无符号 16 位 | | `double` | `jdouble` | 64 位 | | `float` | `jfloat` | 32 位 | | `int` | `jint` | 有符号 32 位 | | `long` | `jlong` | 有符号 64 位 | | `short` | `jshort` | 有符号 16 位 | | `void` | `void` | 不适用的 |

JNI 定义了 Java 引用类型的引用类型等价物。不可能在 JNI 中为所有可以在 Java 中创建的引用类型定义单独的类型。所有 Java 引用类型都可以映射到名为jobject的 JNI 引用类型。你有一些专门的 JNI 引用类型,代表 Java 中常用的引用类型,比如 JNI 的jstring代表 Java 中的java.lang.String。表 8-4 列出了 Java 和 JNI 之间的引用类型映射。

表 8-4。

The Reference Type Mapping Between Java and JNI

| Java 引用类型 | JNI 类型 | | --- | --- | | 任何 Java 对象 | `jobject` | | `java.lang.String` | `jstring` | | `java.lang.Class` | `jclass` | | `java.lang.Throwable` | `jthrowable` |

JNI 定义了单独的引用类型来表示 Java 数组。类型jarray是表示任何 Java 数组类型的通用数组类型。在 Java 中,每种类型的数组都有专门的数组类型。在 JNI,数组类型被命名为jxxxArray,其中 x xx可以是objectbooleanbytechardoublefloatintlongshort。比如 C/C++中的jintArray在 Java 中代表一个int数组。注意,Java 中的所有引用类型数组在 C/C++中都用jobjectArray类型表示。

当使用 JNI 处理 C/C++代码时,您会遇到另一种称为jvalue的类型。它是在 C/C++中定义的联合类型,如下所示:

typedef union jvalue {

jboolean z;

jbyte    b;

jchar    c;

jshort  s;

jint    i;

jlong    j;

jfloat  f;

jdouble  d;

jobject  l;

} jvalue

请注意,jvalue联合类型在 Java 中没有对等的类型。通常,jvalue类型被定义为内置函数中的参数类型,这些内置函数是 JNI API 的一部分。

在 C/C++中使用 JNI 函数

JNI 函数允许您访问本机代码中的 JVM 数据结构和对象。有时,它们允许您将数据转换成在 Java 和本地环境之间传递的特定格式。所有本地函数都有自己的第一个参数,这个参数总是指向JNIEnv的指针,而这个指针又指向一个包含所有 JNI 函数指针的表。

总有两个版本的函数可以在类型JNIEnv:上调用,一个用于 C,一个用于 C++。该函数的 C 版本接受一个指向JNIEnv的指针作为第一个参数,而 C++不会有那个第一个参数。相同方法的两个版本,C 和 C++,被不同地调用。下面的代码片段展示了在 C 和 C++中调用 JNI 函数的区别,假设FuncXxx是函数名,env是指向JNIEnv类型的指针:

// C style

(*env)->FuncXxx(env, list-of-arguments...);

// C++ style

env->FuncXxx(list-of-arguments...);

本章使用 C++方式调用 JNI 函数。通过使用上面的代码片段作为参考,您可以轻松地将代码转换为 C 风格。

作为一个具体的例子,下面是GetStringUTFChars() JNI 函数的函数签名,它允许你将 Java 字符串转换成 UTF-8 字符串格式:

// C Version of the GetStringUTFChars() JNI function

const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);

// C++ Version of the GetStringUTFChars() JNI function

const char * GetStringUTFChars(jstring string, jboolean *isCopy);

如果您想在 C 或 C++中调用这个函数,您的代码将如下所示:

// C Code

const char *utfMsg = (*env)->GetStringUTFChars(env, msg, iscopy);

// C++ Code

const char *utfMsg = env->GetStringUTFChars(msg, iscopy);

使用字符串

Java 和 C/C++中字符串的表示方式不同。在 Java 中,字符串被表示为 16 位 Unicode 字符序列,而在 C/C++中,字符串是指向以空字符结尾的字符序列的指针。本机代码中的jstring引用类型表示java.lang.String类的一个实例,它是一个 16 位 Unicode 字符序列。JNI 具有将 Java 字符串转换成本地字符串的功能,反之亦然。一组字符串函数处理 UTF-8 字符串,另一组处理 Unicode 字符串。当 Java 将一个字符串传递给本机代码时,在使用它之前,必须将本机代码中的字符串转换为本机格式(UTF-8 或 Unicode)。同样的逻辑也适用于将字符串从本机代码返回到 Java。您必须将原生字符串转换为jstring的实例,然后才能将其返回到 Java。

让我们从一个例子开始,在这个例子中,你将把一个字符串从 Java 代码传递给 C/C++代码。C/C++代码会将 Java 字符串转换成原生的 UTF 8 格式,并使用printf()函数将其打印在标准输出上。Java 中的本地方法声明如下:

  • public native void printMsg(String msg);
  • public native String getMsg();

printMsg()方法接受一个 Java 字符串,它的本地函数将把它打印在标准输出上。getMsg()方法将一个本地字符串返回给 Java,Java 将在标准输出中打印它。清单 8-6 包含了声明这两个本地方法的 Java 代码。注意,静态 initialize 加载了您在上一节中创建的名为beginningjava的共享库。这一次,您将需要在共享库中包含新的本机方法的 C++代码。

清单 8-6。将字符串从 Java 传递到本机函数,反之亦然

// JNIStringTest.java

package com.jdojo.jni;

public class JNIStringTest {

static {

System.loadLibrary("beginningjava");

}

public native void printMsg(String msg);

public native String getMsg();

public static void main(String[] args) {

JNIStringTest stringTest = new JNIStringTest();

String javaMsg = "Hello from Java to JNI";

stringTest.printMsg(javaMsg);

String nativeMsg = stringTest.getMsg();

System.out.println(nativeMsg);

}

}

以下是 C/C++中printMsg()getMsg()的本机函数声明:

  • JNIEXPORT void JNICALL Java_com_jdojo_jni_JNIStringTest_printMsg(JNIEnv *env, jobject obj, jstring msg);
  • JNIEXPORT jstring JNICALL Java_com_jdojo_jni_JNIStringTest_getMsg(JNIEnv *env, jobject obj);

注意,本地函数中的前两个参数是类型JNIEnvjobjectprintMsg()函数包含类型为jstring的第三个参数,其返回类型为voidgetMsg()函数只包含两个标准参数,它返回一个jstring

要将jstring转换成 UTF-8 原生字符串,需要使用GetStringUTFChars() JNI 函数,可以通过使用JNIEnv引用来访问该函数。GetStringUTFChars() JNI 函数有两个版本:一个用于 C,一个用于 C++。

GetStringUTFChars()函数将 Java 字符串(在 C/C++代码的jstring中)转换成 UTF 8 格式,并返回一个指向转换后的 UTF 8 字符串的指针。如果失败,它返回NULLGetStringUTFChars()函数可能需要在内存中复制原始的 Java 字符串对象,以便将其转换为 UTF-8 格式。函数的isCopy参数是一个指向boolean变量的指针,可以用来检查这个函数是否必须复制原始的 Java 字符串。如果isCopy不是NULL,如果复制了 Java 字符串,则它被设置为JNI_TRUE。否则设置为JNI_FALSE。一旦处理完这个函数的返回值,就必须调用ReleaseStringUTFChars()方法来释放内存。此方法的 C 和 C++样式签名如下:

// C Style

void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);

// C++ Style

void ReleaseStringUTFChars(jstring string, const char *utf);

清单 8-7 包含了 C++中本地方法printMsg()getMsg()的实现。代码在本书源代码的jnistringtest.cpp文件中。getMsg()的代码很简单。它使用NewStringUTF() JNI 函数从本地字符串中获取一个 Java 字符串。

清单 8-7。jnistringtest.cpp 文件的内容

// jnistringtest.cpp

#include <stdio.h>

#include <jni.h>

#include "com_jdojo_jni_JNIStringTest.h"

JNIEXPORT void JNICALL Java_com_jdojo_jni_JNIStringTest_printMsg

(JNIEnv *env, jobject obj, jstring msg) {

const char *utfMsg;

jboolean *iscopy = NULL;

// Get the UTF string

utfMsg = env->GetStringUTFChars(msg, iscopy);

if (utfMsg == NULL) {

printf("Could not convert Java string to UTF-8 string.\n");

return;

}

// Print the message on the standard output

printf("%s\n", utfMsg);

// Release the memory

env->ReleaseStringUTFChars(msg, utfMsg);

}

JNIEXPORT jstring JNICALL Java_com_jdojo_jni_JNIStringTest_getMsg

(JNIEnv *env, jobject obj) {

const char *utfMsg = "Hello from JNI to Java";

jstring javaString = env->NewStringUTF(utfMsg);

return javaString;

}

运行JNIStringTest类的javah命令来创建com_jdojo_jni_JNIStringTest.h C++头文件。

javah com.jdojo.jni.JNIStringTest

要在同一个名为beginningjava的共享库中包含hellojni.cppjnistringtest.cpp文件的 C++内容,需要将这两个文件作为源文件传递给 g+=命令。下面是 Windows 上的命令,假设您已经将两个源文件的头文件放在了C:\dll目录中。

C:\> g++ -Wl,--kill-at -shared

-IC:/java8/include -IC:/java8/include/win32 -IC:/dll

-o C:/dll/beginningjava.dll

C:/dll/hellojni.cpp C:/dll/jnistringtest.cpp

请参考“在 Linux 上创建共享库”一节,在 Linux 上用两个 C++源文件创建共享库。

现在你已经准备好运行清单 8-6 中列出的JNIStringTest类了。它将生成以下输出:

Hello from JNI to Java

Hello from Java to JNI

您可以使用GetStringUTFLength(jstring string) JNI 函数来获得以字节为单位的jstring的长度,以 UTF-8 格式表示它。JNI 也有让您使用 Unicode 原生字符串的功能。Unicode 字符串函数被命名为不带单词“UTF”的 UTF 字符串函数。例如,要用 Unicode 字符获得一个jstring的长度,您需要一个GetStringLength()函数,而不是GetStringUTFLength()函数。为了从 Unicode 字符构造一个新的 Java String (a jstring),我们有一个NewString() JNI 函数,而不是NewStringUTF() JNI 函数,后者从 UTF 8 本地字符串创建一个 Java 字符串。有时您可能需要将jstring中的 Java String转换成本地编码,反之亦然。您可以使用java.lang.String类,它有一组丰富的构造函数和方法,允许您将一种编码的字符串转换为字节数组,反之亦然。我将在后面的章节中介绍如何在本机代码中访问 Java 类。

使用数组

JNI 允许您将一组原语或引用类型从 Java 传递到本机代码,反之亦然。您不能在本机代码中直接访问或使用 Java 数组。您将需要使用 JNI 函数在本机代码中处理 Java 数组。JNI 为原始数组和引用数组提供了一组不同的函数。有些函数是两种类型共有的。本节中使用的所有与数组相关的方法都使用 C++版本。给它们加上JNIEnv *env作为第一个参数,得到对应的 C 版本。

GetArrayLength()方法返回基元或引用类型的数组长度。它的宣言是

jsize GetArrayLength(jarray array)

您可以使用New<Xxx>Array()方法创建一个原始类型的数组,其中<Xxx>BooleanByteDoubleFloatIntLongShort的原始类型之一。您需要将基本类型数组的长度作为参数传递给该方法。如果无法创建数组,它将返回NULL。以下代码片段创建了一个长度均为 10 的int数组和一个double数组:

jintArray iArray = env->NewIntArray(10);

jdoubleArray dArray = env->NewDoubleArray(10);

您可以使用Get<Xxx>ArrayElements()来获取原始数组的内容,其中<Xxx>BooleanByteCharDoubleFloatIntLongShort的原始类型之一。兹声明如下:

<RRR> *Get<Xxx>ArrayElements(<AAA> array, jboolean *isCopy)

Here, <RRR>为 JNI 原生数据类型,如jintjdouble,<AAA>为 JNI 数组类型,如jintArrayjdoubleArray等。isCopy参数指示返回的数组元素是否是原始数组的副本。如果isCopy不是NULL,如果复制了原始数组,则设置为JNI_TRUE。如果没有复制原始数组,则设置为JNI_FALSE。您还可以对本机代码中的数组元素进行更改,这些更改将反映到原始数组中。您需要释放元素,这些元素是在您使用完之后通过这个方法获得的。您需要使用Release<Xxx>ArrayElements()方法来释放数组元素,声明如下:

void Release<Xxx>ArrayElements(<AAA> array, <RRR> *elems, jint mode)

Release<Xxx>ArrayElements()函数中的最后一个参数mode指示如何释放在本机代码中用于数组元素的缓冲区。其值可以是 0、JNI_COMMITJNI_ABORT。0 表示复制回内容并释放elems缓冲区;JNI_COMMIT表示复制回内容,但不释放elems缓冲区;而JNI_ABORT意味着释放缓冲区而不复制回可能的更改。下面的代码片段用本机代码访问一个int Java 数组,并在标准输出中打印它的所有元素值:

jintArray num = get a Java array...;

const jsize count = env->GetArrayLength(num);

jboolean isCopy;

jint *intNum = env->GetIntArrayElements(num, &isCopy);

for (jsize i = 0; i < count; i++) {

printf("%i\n", intNum[i]);

}

// Release the intNum buffer without copying back any changes made to the array elements

env->ReleaseIntArrayElements(num, intNum, JNI_ABORT);

本机代码中的引用类型 Java 数组被区别对待。你可以使用NewObjectArray()函数创建一个新的引用类型数组。该方法声明如下:

jobjectArray NewObjectArray(jsize length, jclass elementClass, jobject initialElement)

请注意,您需要使用数组元素的类类型对象来创建一个引用数组。最后一个参数是初始化数组所有元素的初始元素。

与基元类型数组不同,您不需要获取引用类型数组的数组元素来访问它们。您可以使用GetObjectArrayElement()功能一次访问一个元素。您可以使用SetObjectArrayElement()函数来设置引用类型的数组元素的值。这些方法声明如下:

  • jobject GetObjectArrayElement(jobjectArray array, jsize index)
  • void SetObjectArrayElement(jobjectArray array, jsize index, jobject value)

让我们看看在 JNI 应用中使用数组的例子。清单 8-8 包含使用数组声明三个本地方法的 Java 代码。

清单 8-8。在本机代码中访问和操作数组的示例

// JNIArrayTest.java

package com.jdojo.jni;

import java.util.Arrays;

public class JNIArrayTest {

static {

System.loadLibrary("beginningjava");

}

// Three native method declarations

public native int sum(int[] num);

public native String concat(String[] str);

public native int[] increment(int[] num, int incrementBy);

public static void main(String[] args) {

JNIArrayTest test = new JNIArrayTest();

int[] num = {1, 2, 3, 4, 5};

String[] str = {"One", "Two", "Three", "Four", "Five" } ;

System.out.println("Original Number Array: " + Arrays.toString(num));

System.out.println("Original String Array: " + Arrays.toString(str));

int sum = 0;

sum = test.sum(num);

System.out.println("Sum: " + sum);

String concatenatedStr = test.concat(str);

System.out.println("Concatenated String: " + concatenatedStr);

int increment = 5;

int[] incrementedNum = test.increment(num, increment);

System.out.println("Increment By: " + increment);

System.out.println("Incremented Number Arrays: " +

Arrays.toString(incrementedNum));

}

}

sum()本地方法接受一个int数组,并返回其所有元素的总和作为int。当您调用sum()方法时,注意不要在int数组中传递大的数字。否则,结果可能会溢出。concat()本地方法接受一个String数组。它连接数组中的所有元素并返回一个String对象。increment()本地方法接受一个int数组和一个int数字。它返回一个新的int数组,该数组包含原数组中按指定数字递增的所有元素。main()方法包含测试三个本地方法的代码。

运行JNIArrayTest类的javah命令来创建com_jdojo_jni_JNIArrayTest.h C++头文件。

javah com.jdojo.jni.JNIArrayTest

清单 8-9 包含了jniarraytest.cpp文件中三个本地方法的 C++实现。concat()方法的实现假设String数组中所有元素的长度不会超过 500 字节。请参考上一节关于如何在共享库中包含 C+=源文件的内容。

清单 8-9。jniarraytest.cpp 文件的内容,带有 sum()、concat()和 increment()本机方法的 C++实现

// jniarraytest.cpp

#include <jni.h>

#include <cstring>

#include "com_jdojo_jni_JNIArrayTest.h"

JNIEXPORT jint JNICALL Java_com_jdojo_jni_JNIArrayTest_sum

(JNIEnv *env, jobject obj, jintArray num) {

jint sum = 0;

const jsize count = env->GetArrayLength(num);

jboolean isCopy;

jint *intNum = env->GetIntArrayElements(num, &isCopy);

for (jsize i = 0; i < count; i++) {

sum += intNum[i];

}

// Release the intNum buffer without copying back any changes made to the array elements

env->ReleaseIntArrayElements(num, intNum, JNI_ABORT);

return sum;

}

JNIEXPORT jstring JNICALL Java_com_jdojo_jni_JNIArrayTest_concat

(JNIEnv *env, jobject obj, jobjectArray strArray) {

const int MAX_LENGTH = 500;

char dest[MAX_LENGTH];

for (int i = 0; i < MAX_LENGTH; i++) {

dest[i] = (char)NULL;

}

const jsize count = env->GetArrayLength(strArray);

for (jsize i = 0; i < count; i++) {

// Get the string object from the array

jstring strElement =

(jstring) env->GetObjectArrayElement(strArray, i);

const char *tempStr = env->GetStringUTFChars(strElement, NULL);

if (tempStr == NULL) {

printf("Could not convert Java string to UTF-8 string.\n");

return NULL;

}

// Concatenate tempStr to dest

strcat(dest, tempStr);

// Release the memory used by tempStr

env->ReleaseStringUTFChars(strElement, tempStr);

// Delete the local reference of jstring

env->DeleteLocalRef(strElement);

}

jstring returnStr = env->NewStringUTF(dest);

return returnStr;

}

JNIEXPORT jintArray JNICALL Java_com_jdojo_jni_JNIArrayTest_increment

(JNIEnv *env, jobject obj, jintArray num, jint incrementBy) {

const jsize count = env->GetArrayLength(num);

jboolean isCopy;

jint *intNum = env->GetIntArrayElements(num, &isCopy);

jintArray modifiedNumArray = env->NewIntArray(count);

jboolean isNewArrayCopy;

jint *modifiedNumElements =

env->GetIntArrayElements(modifiedNumArray, &isNewArrayCopy);

for (jint i = 0; i < count; i++) {

modifiedNumElements[i] = intNum[i] + incrementBy;

}

if (isCopy == JNI_TRUE) {

env -> ReleaseIntArrayElements(num, intNum, JNI_COMMIT);

}

if (isNewArrayCopy == JNI_TRUE) {

env -> ReleaseIntArrayElements(modifiedNumArray,

modifiedNumElements,

JNI_COMMIT);

}

return modifiedNumArray;

}

运行清单 8-8 所示的JNIArrayTest类将产生以下输出:

Original Number Array: [1, 2, 3, 4, 5]

Original String Array: [One, Two, Three, Four, Five]

Sum: 15

Concatenated String: OneTwoThreeFourFive

Increment By: 5

Incremented Number Arrays: [6, 7, 8, 9, 10]

在本机代码中访问 Java 对象

您可以以不同的方式在本机代码中使用 Java 对象:您可以

  • 用本机代码创建 Java 对象。
  • 从本机代码访问 JVM 中存在的 Java 对象和类。
  • 访问/修改本机代码中 Java 对象的字段。
  • 从本机代码中调用 Java 实例和 Java 对象的静态方法。

以下部分描述了在本机代码中使用 Java 对象所需的步骤。

获取类引用

jclass类型的实例表示本机代码中的类对象。如果你调用一个本地函数,在 Java 类中声明为staticnative,你的本地函数总是获得类对象的引用作为第二个参数。有时你可能有一个 Java 对象的引用在jobject类型中,你想得到它的类对象引用。您需要使用GetObjectClass() JNI 函数来获取 Java 对象的类对象的引用,如下所示:

jobject obj = get the reference to a Java object;

jclass cls = env->GetObjectClass(obj);

使用FindClass() JNI 函数通过类名获得类对象的引用。您需要在FindClass()方法中使用类的完全限定名,方法是用正斜杠替换包名中的点。如果您试图获取数组的类对象的引用,您需要使用数组类签名。为了获得对java.lang.String类的类对象的引用,您需要使用java/lang/String作为类名。要获得int[]的类对象引用,需要使用[I作为类名。要知道数组类型的类的正确签名,可以在该数组类型的类中声明一个字段,并使用带有–s–private选项的javap命令。以下代码片段演示了如何获取某些 Java 引用类型的类对象的引用:

jclass cls;

// Get the reference of the java.lang.String class object

cls = env->FindClass("java/lang/String");

// Get the reference of the int[] array class object

cls = env->FindClass("[I");

// Get the reference of the int[][] array class object

cls = env->FindClass("[[I");

// Get the reference of the String[] array class object. Note a semi-colon in signature

cls = env->FindClass("[Ljava/lang/String;");

访问 Java 对象/类的字段和方法

在访问本地代码中的 Java 对象/类的字段之前,必须获得字段 ID。您需要使用GetFieldID() JNI 函数获取实例字段的字段 ID,使用GetStaticFieldID() JNI 函数获取静态字段的字段 ID。这两种方法的签名如下:

  • jfieldID GetFieldID(jclass cls, const char *name, const char *sig)
  • jfieldID GetStaticFieldID(jclass cls, const char *name, const char *sig)

cls参数是类对象的引用,定义了实例/静态字段。name参数是字段的名称。sig参数是字段的签名。您需要使用带有–s–private选项的javap命令来获取类中定义的字段的签名。

您需要使用Get<Xxx>Field() JNI 函数来获取实例字段的值,使用GetStatic<Xxx>Field() JNI 函数来获取静态字段的值,其中<Xxx>是字段的类型,其值可以是BooleanByteCharDoubleFloatIntLongShortObjectSet<Xxx>Field()SetStatic<Xxx>Field() JNI 函数允许您分别设置实例和静态字段的值。这些方法的声明如下,其中<RRR>是本地数据类型,例如,如果<Xxx>int,则<RRR>jint:

  • <RRR> Get<Xxx>Field(jobject obj, jfieldID fieldID)
  • <RRR> GetStatic<Xxx>Field(jclass clazz, jfieldID fieldID)
  • void Set<Xxx>Field(jobject obj, jfieldID fieldID, <RRR> value)
  • void SetStatic<Xxx>Field(jclass clazz, jfieldID fieldID, <RRR> value)

假设objjobject的一个实例(即一个 Java 对象引用)cls是它的类引用。在由cls表示的类中有两个类型为int的字段numcountnum字段是实例字段,而count字段是静态字段。以下代码片段显示了如何在本机代码中访问这两个字段并将它们的值递增 1:

// Get the field ID of num and count fields

jfieldID numFieldId = env->GetFieldID(cls, "num", "I");

jfieldID countFieldId = env->GetStaticFieldID(cls, "count", "I");

// Get the field values

jint numValue = env->GetIntField(obj, numFieldId);

jint countValue = env->GetStaticIntField(cls, countFieldId);

// Increment the values by 1 and set them back to the fields

numValue = numValue + 1;

countValue = countValue + 1;

env->SetIntField(obj, numFieldId, numValue);

env->SetStaticIntField(cls, countFieldId, countValue);

在本机代码中使用 Java 对象/类的方法的步骤类似于使用它们的字段。在访问方法之前,您需要获取方法的方法 ID。您可以使用GetMethodID()GetStaticMethodID(JNI 函数来分别获取实例方法和静态方法的方法 ID。他们的声明如下:

  • jmethodID GetMethodID(jclass clazz, const char *name, const char *sig)
  • jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig)

该方法的name是它的简单名称,可以使用带有–s–private选项的javap命令来获得它的签名。下面的代码片段展示了如何从一个 Java 类的几个方法中获取方法 ID,假设cls表示类对象引用:

jmethodID methodID

// Method is "void objectCallBack()"

methodID = env->GetMethodID(cls, "objectCallBack", "()V");

// Method is "static void classCallBack()"

methodID = env->GetStaticMethodID(cls, "classCallBack", "()V");

// Method is "int getLength(String str)"

methodID = env->GetMethodID(cls, "getLength", "(Ljava/lang/String;)I");

// Method is "int[] increment(int[], int)"

methodID = env->GetMethodID(cls, "increment", "([II)[I");

调用实例或static方法很容易。您需要使用对象/类、方法 ID 和方法参数(如果有的话)来调用方法。您可以使用以下任何方法来调用对象的实例方法:

  • <RRR> Call<Xxx>Method(jobject obj, jmethodID methodID, arg1, arg2...)
  • <RRR> Call<Xxx>MethodA(jobject obj, jmethodID methodID, const jvalue *args)
  • <RRR> Call<Xxx>MethodV(jobject obj, jmethodID methodID, va_list args)

这里,方法名中的<Xxx>是该方法的返回类型,可以是BooleanByteCharDoubleFloatIntLongShortObjectVoid<RRR>是方法的返回类型,根据对应的<Xxx>值,可以是jbooleanjbytejcharjdoublejfloatjintjlongjshort, jobjectvoidCall<Xxx>Method()Call<Xxx>MethodA()Call<Xx>MethodV()之间的区别在于您希望如何将参数传递给方法。Call<Xxx>Method()方法允许您将参数作为逗号分隔的列表传递给方法。Call<Xxx>MethodA()方法允许您将参数作为jvalue类型的数组传递给方法。Call<Xxx>MethodV()方法允许您将参数作为va_list传递给一个方法。以下代码片段显示了如何调用实例方法,假设objjobject类型的引用,方法 ID 是methodID:

// Method is "void m1()"

env->CallVoidMethod(obj, methodID);

// Method is "void m2(int a)"

env->CallVoidMethod(obj, methodID, 109);

// Method is "int m2(double a)"

jint value = env->CallIntMethod(obj, methodID, 109.23);

调用静态方法类似于调用实例方法。你需要使用一个类对象引用来调用一个静态方法。您需要使用下列 JNI 函数之一来调用静态方法。注意,用于调用static方法的 JNI 函数名包含单词Static

  • <RRR> CallStatic<Xxx>Method(jclass cls, jmethodID methodID, arg1, arg2...)
  • <RRR> CallStatic<Xxx>MethodA(jclass cls, jmethodID methodID, jvalue *args)
  • <RRR> CallStatic<Xxx>MethodV(jclass cls, jmethodID methodID, va_list args)

JNI 允许您从对象的类层次结构中的任何类调用对象的实例方法。当你使用一个Call<Xxx>Method()函数时,它使用对象的类来调用方法。考虑以下类层次结构:

// A.java

package com.jdojo.jni;

public class A {

public int m1() {

return 1;

}

}

// B.java

package com.jdojo.jni;

public class B extends A {

@Override

public int m1() {

return 3;

}

}

// C.java

package com.jdojo.jni;

public class C extends B {

@Override

public int m1() {

return 3;

}

}

BC覆盖了m1()方法。如果您使用CallIntMethod()调用类C的对象的m1()方法,它将调用类C中的m1()方法并返回 3。JNI 允许您使用类C的对象调用类A或类B中的m1()方法。要从超类中调用对象的方法,需要使用以下 JNI 方法之一:

  • <RRR> CallNonvirtual<Xxx>Method(jobject obj, jclass cls, jmethodID methodID, arg1, arg2...)
  • <RRR> CallNonvirtual<Xxx>MethodA(jobject obj, jclass cls, jmethodID methodID, const jvalue *args)
  • <RRR> CallNonvirtual<Xxx>MethodV(jobject obj, jclass cls, jmethodID methodID, va_list args)

您需要在这些版本的方法中使用对象及其类的引用。必须使用需要调用该方法的类来获取methodID。例如,下面的代码片段在类C的对象上调用类Bm1()方法。代码还创建了一个C类的对象。

// Get the class references for B and C

jclass bCls = env->FindClass("com/jdojo/jni/B");

jclass cCls = env->FindClass("com/jdojo/jni/C");

// Get method ID for the constructor of class C

jmethodID cConstrctorID = env->GetMethodID(cCls, "<init>", "()V");

// Create an object of class C

jobject cObject = env->NewObject(cCls, cConstrctorID);

// Get the method ID for the m1() method in class B

jmethodID bMethodID = env->GetMethodID(bCls, "m1", "()I");

// Call the m1() method in class B using an object of class C

jint h = env->CallNonvirtualIntMethod(cObject, bCls, bMethodID);

// will print 2, which is returned from m1() in class B

printf("%i\n", h);

让我们来看一个在本地代码中访问 Java 对象的字段和方法的完整示例。清单 8-10 包含 Java 代码,其中一个名为JNIJavaObjectAccessTest的类包含两个名为numcount的字段。它还包含两个名为objectCallBack()classCallBack()的方法。您将访问本机代码中的字段和方法。它有一个名为callBack()的本地方法。callBack() native方法将numcount字段增加 1,并调用objectCallBack() and classCallBack()方法。在运行JNIJavaObjectAccessTest类之前,您需要生成com_jdojo_jni_JNIJavaObjectAccessTest.h C++头文件和共享库,包括来自jnijavaobjectaccesstest.cpp文件的内容,如清单 8-11 所示。

清单 8-10。从本机代码访问 Java 对象/类的字段和方法

// JNIJavaObjectAccessTest.java

package com.jdojo.jni;

public class JNIJavaObjectAccessTest {

static {

System.loadLibrary("beginningjava");

}

private int num = 10;

private static int count = 1001;

public void objectCallBack() {

System.out.println("Inside objectCallBack() method.");

}

public static void classCallBack() {

System.out.println("Inside classCallBack() method.");

}

public native void callBack();

public int hashCode() {

return -9999;

}

public static void main(String[] args) {

JNIJavaObjectAccessTest test = new JNIJavaObjectAccessTest();

System.out.println("Before calling native method...");

System.out.println("num = " + test.num);

System.out.println("count = " + count);

// Call native method

test.callBack();

System.out.println("After calling native method...");

System.out.println("num = " + test.num);

System.out.println("count = " + count);

}

}

Before calling native method...

num = 10

count = 1001

Inside objectCallBack() method.

Inside classCallBack() method.

After calling native method...

num = 11

count = 1002

清单 8-11。jnijavaobjectsaccesstest.cpp 文件的内容,该文件包含 JNIJavaObjectAccessTest 类中声明的 callBack()本机方法的 C++实现

// jnijavaobjectaccesstest.cpp

#include <stdio.h>

#include <jni.h>

#include "com_jdojo_jni_JNIJavaObjectAccessTest.h"

JNIEXPORT void JNICALL Java_com_jdojo_jni_JNIJavaObjectAccessTest_callBack

(JNIEnv *env, jobject obj) {

jclass cls;

// Get the class reference for the object

cls = env->GetObjectClass(obj);

if (cls == NULL) {

return;

}

// Access the fields

jfieldID numFieldId = env->GetFieldID(cls, "num", "I");

jfieldID countFieldId = env->GetStaticFieldID(cls, "count", "I");

jint numValue = env->GetIntField(obj, numFieldId);

jint countValue = env->GetStaticIntField(cls, countFieldId);

numValue = numValue + 1;

countValue = countValue + 1;

env->SetIntField(obj, numFieldId, numValue);

env->SetStaticIntField(cls, countFieldId, countValue);

// Call the instance method

jmethodID instanceMethodID = env->GetMethodID(cls,

"objectCallBack",

"()V");

if (instanceMethodID != 0) {

env->CallVoidMethod(obj, instanceMethodID);

}

// Call the static method

jmethodID staticMethodID = env->GetStaticMethodID(cls,

"classCallBack",

"()V");

if (staticMethodID != 0) {

env->CallStaticVoidMethod(cls, staticMethodID);

}

return;

}

创建 Java 对象

JNI 允许您在不调用任何构造函数或调用特定构造函数的情况下,用本机代码创建 Java 对象。您需要使用AllocObject() JNI 函数为一个 Java 对象分配内存,而不需要调用它的任何构造函数。请注意,根据数据类型,所有实例字段都有默认值。使用AllocObject() JNI 函数时,实例字段不会被初始化,也不会调用实例初始化器。下面是用 Java 为一个类的对象分配内存的代码片段:

jclass cls = get the class reference;

jobject obj = env->AllocObject(cls);

if (obj == NULL) {

// The object could not be created. Handle the error condition.

}

通过使用下列 JNI 函数之一调用 Java 类的特定构造函数,可以创建 Java 对象。这些函数的不同之处仅在于如何传递构造函数的参数。

  • jobject NewObject(jclass clazz, jmethodID methodID, arg1, arg2...)
  • jobject NewObjectA(jclass clazz, jmethodID methodID, const jvalue *args)
  • jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args)

methodID参数是您想要调用的构造函数的方法 ID。当您想要获取类的构造函数的方法 ID 时,有一个特殊的字符串用于方法名。你需要使用<init>$init$作为构造函数的方法名。考虑清单 8-12 所示的名为IntWrapper的类的代码。

清单 8-12。一个用本地代码演示 Java 对象创建的示例类

// IntWrapper.java

package com.jdojo.jni;

public class IntWrapper {

private int value = -1;

public IntWrapper() {

}

public IntWrapper(int value) {

this.value = value;

}

public int getValue() {

return value;

}

}

您可以在本机 C++代码中获取对IntWrapper类的引用,如下所示:

jclass wrapperCls = env->FindClass("com/jdojo/jni/IntWrapper");

以下 C++代码在不调用构造函数的情况下为一个IntWrapper对象分配内存:

jobject wrapperObject = env->AllocObject(wrapperCls);

此时,wrapperObject存在于内存中,其实例字段value仍然具有默认值 0。如果您在此时调用wrapperObject上的getValue()方法,它将返回 0 而不是–1,正如您所料。

如果你想通过调用一个构造函数来创建一个 Java 类的对象,你需要使用NewObject() JNI 函数。下面的代码片段通过调用无参数构造函数创建了一个IntWrapper类的对象。构造函数的签名取决于它接受的参数的数量和类型。对于无参数构造函数,签名是()V。如果构造函数接受一个int参数,它的签名将是(I)V。您可以通过使用带有–s选项的javap命令来获得一个类的构造函数的签名。如果您还想包含private成员的签名,请使用带有javap–private选项。

// Get the method ID for the default constructor of class IntWrapper

jmethodID mid = env->GetMethodID(wrapperCls, "<init>", "()V");

// Create an object of class IntWrapper using the default constructor

jobject wrapperObject = env->NewObject(wrapperCls, mid);

此时,如果在wrapperObject上调用getValue()方法,它将返回-1,这是value实例字段的初始值。当调用构造函数时,所有实例字段都被初始化。

下面的代码片段调用了第二个版本的IntWrapper类的构造函数,它接受一个int参数。它将 999 作为构造函数IntWrapper(int value)的参数值进行传递。

// Get the method ID for the constructor for class IntWrapper

jmethodID wrapperConstrctorID = env->GetMethodID(wrapperCls, "<init>","(I)V");

// Create an object of class IntWrapper passing 999 to the constructor

jobject wrapperObject = env->NewObject(wrapperCls, wrapperConstrctorID, 999);

此时,如果你调用abcObject上的getValue()方法,它将返回 999,这个值是在它创建的时候在它的构造函数中设置的。

Tip

AllocObject()NewObject() JNI 函数只能用于创建非数组引用类型的对象。你需要使用NewObjectArray() JNI 函数来创建一个特定类型的数组。

异常处理

JNI 允许您处理本机代码中的异常。本机代码可以检测和处理由于调用 JNI 函数而在 JVM 中引发的异常。本机代码也可以抛出异常,该异常可以传播到 Java 代码。本机代码中的异常处理机制不同于 Java 代码。当 Java 代码中抛出异常时,控制权会立即转移到最近的能够处理异常的catch块。如果在本机代码执行过程中引发异常,本机代码将继续执行,异常将保持挂起状态,直到控制权返回给 Java 代码。一旦异常挂起,除了释放本机资源的函数之外,不应该执行任何其他 JNI 函数。有两种方法可以检测本机代码中的 JNI 函数调用是否导致了异常:

  • 通过检查函数的特殊返回值
  • 通过检查函数返回后是否出现异常

如果出现异常,一些 JNI 函数会返回一个特殊值。举个例子,如果你调用了FindClass() JNI 函数,没有找到类,那么可能会抛出四个异常中的任何一个:ClassFormatErrorClassCircularityErrorNoClassDefFoundError或者OutOfMemoryError。如果抛出了四个异常中的任何一个,FindClass() JNI 函数将返回NULL作为特殊值。您应该在调用FindClass() JNI 函数后立即检查NULL的返回值,并编写代码来处理该异常。通常,您将控件返回给调用方,以便调用方可以处理异常,如下所示:

jclass cls = env->FindClass("abc/xyz/NonExistentClass");

if (cls == NULL) {

/* Here, free up any resources you had held and return. Exception is pending at            this time. It will be thrown when the control returns to the Java code.

*/

return;

}

在某些情况下,不可能从 JNI 函数返回一个特殊值来指示异常已经发生。假设您正在用本机代码访问一个 Java 数组,并且已经超出了该数组的边界。在这种情况下,JVM 抛出一个类型为ArrayIndexOutOfBoundsException的异常。你可以调用一个发生异常的 Java 对象的方法。在这种情况下,你需要在 JNI 函数调用后立即使用ExceptionOccurred()ExceptionCheck() JNI 函数来检查是否有异常发生。这些函数具有以下特征:

  • jthrowable ExceptionOccurred()
  • jboolean ExceptionCheck()

如果在函数调用过程中发生异常,函数将返回该异常对象的引用。否则,它返回NULL。如果在函数调用过程中出现异常,ExceptionCheck()函数返回JNI_TRUE。否则返回JNI_FALSE。下面的代码片段演示了如何使用这些函数。你只需要使用两个功能中的一个,而不是同时使用两个。

// Using method ExceptionOccurred()

// Call a JNI function, which may throw an exception

jthrowable e = env->ExceptionOccurred();

if (e != NULL) {

/* Free up any resources that you had held and return. Exception is pending at this

time. It will be thrown when the control returns to the Java code.

*/

return;

}

// Using method ExceptionCheck()

// Call a JNI function, which may throw an exception

jboolean gotException = env->ExceptionCheck();

if (gotException) {

/* Free up any resources that you had held and return. Exception is pending at

this time. It will be thrown when the control returns to the Java code.

*/

return;

}

一旦检测到本机代码中发生的异常,您有三种选择:

  • 清除异常并在本机代码中处理它。
  • 将控制返回给 Java 代码,让 Java 代码处理异常。
  • 清除异常,在本机代码中处理它,并从本机代码中抛出一个 Java 代码可以处理的新异常。

以下部分解释了处理异常的三种方式。

在本机代码中处理异常

您可以清除异常并在本机代码中处理异常情况。使用ExceptionClear() JNI 函数清除挂起的异常,如图所示:

// Call a JNI function, which may throw an exception

jboolean gotException = env->ExceptionCheck();

if (gotException) {

// Clear the exception

env->ExceptionClear();

// Write some code to take care of the exceptional condition

}

一旦清除了异常,该异常就不再处于挂起状态。

在 Java 代码中处理异常

您可以使用语句将控件返回给调用方,并让调用方处理异常,如下所示:

// Call a JNI function, which may throw an exception

jboolean gotException = env->ExceptionCheck();

if (gotException) {

/* Free up any resources that you had held and return. Exception  is pending at this time. It will be thrown when the control returns to the caller.

*/

return;

}

从本机代码引发新的异常

您可以在本机代码中处理该异常,清除该异常,并引发新的异常。请注意,从本机代码抛出异常并不会将控制权转移回 Java 代码。您必须编写代码,比如一个return语句,将控制权转移回 Java 代码,所以您抛出的异常是用 Java 处理的。您可以使用以下两个 JNI 函数之一在本机代码中引发异常。两个函数成功时返回零,失败时返回负整数。

  • jint Throw(jthrowable obj)
  • jint ThrowNew(jclass clazz, const char *message)

Throw()函数接受一个jthrowable对象。该函数接受异常的类引用和一条消息。下面的代码片段展示了如何使用ThrowNew()函数抛出一个java.lang.Exception:

if (someErrorConditionIsTrue) {

jclass cls = env->FindClass("java/lang/Exception");

// Check for exception here (omitted)

env->ThrowNew(cls, "your error message goes here");

return;

}

Tip

如果你想打印本机代码中异常的栈跟踪,你可以使用ExceptionDescribe() JNI 函数。它在标准错误上打印异常栈跟踪。如果想从本机代码中引发致命错误,可以使用FatalError(const char *msg) JNI 函数。函数不会返回,JVM 也不会从这个错误中恢复。Java 代码中声明的本机方法也可以像 Java 非本机方法一样使用throws子句。以下是 Java 类中有效的本机方法声明:

public native int myMethod() throws Exception;

创建 JVM 的实例

到目前为止,您已经看到了使用本机代码的 Java 应用。现在你可以看到相反的情况了。即使用 Java 代码的本机应用。为什么要使用本地应用中的 Java 代码?出于以下原因,您可能希望使用本机应用中的 Java 代码:

  • 您可能已经有了一个用 Java 编写的应用,并且想要使用现有的代码。
  • Java 提供了一套丰富的类库。您可能希望在本机应用中利用 Java 类库。

JNI API 中允许您用本机代码创建和加载 JVM 的部分称为。JNI 允许您在本地应用中嵌入 JVM。也就是说,您可以从本地应用创建 JVM,并像在 Java 应用中一样使用 Java 类。用本机代码创建一个 JVM 只需要几行代码。您所需要做的就是准备要传递给 JVM 的初始参数,并调用JNI_CreateJavaVM()调用 API 函数来创建 JVM。

传递给 JVM 的初始参数是一个JavaVMInitArgs结构,定义如下:

typedef struct JavaVMInitArgs {

jint version;

jint nOptions;

JavaVMOption *options;

jboolean ignoreUnrecognized;

} JavaVMInitArgs;

version字段表示 JNI 版本,并且必须至少设置为 JNI 版本 1 2。nOptions字段被设置为您想要传递给 JVM 的选项的数量。选项字段是一个JavaVMOption结构的数组,定义如下:

typedef struct JavaVMOption {

char *optionString;

void *extraInfo;

} JavaVMOption;

如果ignoreUnrecognized设置为JNI_TRUE,则JNI_CreateJavaVM()功能将忽略未识别的选项。如果设置为JNI_FALSEJNI_CreateJavaVM()函数一遇到未识别的选项就会返回JNI_ERR

JavaVMOption结构中的optionString字段是一个字符串,它是默认平台编码中 JVM 选项的值。

extraInfo字段用于特殊类型的 JVM 参数。它代表一个用于重定向 JVM 消息的函数挂钩、一个 JVM 退出挂钩或一个 JVM 中止挂钩。extraInfo字段代表的挂钩类型取决于optionString字段的值。如果optionString字段的值为vfprintfexitabort,则extraInfo字段分别表示 JVM 消息重定向挂钩、JVM 退出挂钩或 JVM 中止挂钩。注意vfprintf钩子只将 JVM 消息重定向到钩子。它不会将System.outSystem.err消息重定向到钩子。如果您在本机代码中设置了一个vsprintf钩子,并在 Java 代码中使用了System.out/System.errprint()/println()方法之一,那么这些消息将不会被重定向到您的vfprintf钩子。您需要使用System类的setOut()setErr()方法来重定向System.outSystem.err消息。JVM 的退出钩子在 JVM 正常终止时被调用,比如通过在 Java 代码中调用System.exit(int exitCode)方法。JVM 的中止钩子在 JVM 异常终止时被调用。下面的代码片段展示了如何用不同的 VM 钩子填充extraInfo字段。首先,定义三个函数作为三种类型的钩子。请注意,函数必须具有相同的签名,如以下代码片段所示:

jint JNICALL jvmMsgRedirection_hook(FILE *stream, const char *format, va_list args) {

// You can log the VM message here.

// Let us just print the VM message on the standard output.

return vfprintf(stdout, format, args);

}

void JNICALL jvmExit_hook(jint code) {

// You can do some cleanup work here

printf("VM exited with exit code %i\n", code);

}

void JNICALL jvmAbort_hook() {

printf("VM was aborted\n");

}

JavaVMOption jvmOption[3];

// Add JVM hooks

options[0].optionString = "vfprintf";

options[0].extraInfo = jvmMsgRedirection_hook;

options[1].optionString = "exit";

options[1].extraInfo = jvmExit_hook;

options[2].optionString = "abort";

options[2].extraInfo = jvmAbort_hook;

下面的代码片段展示了如何用 JVM 的初始参数填充一个JavaVMInitArgs结构。它只设置了两个参数,java.class.pathjava.lib.path。如果需要,可以设置更多的 JVM 参数。

// Populate the JVM options in JavaVMOption structure

const jint MAX_OPTIONS = 2; // will pass two arguments to the JVM

JavaVMOption options[MAX_OPTIONS];

// Our first argument is java.class.path (CLASSPATH for JVM)

options[0].optionString = "-Djava.class.path=.;c:\\myjni\\classes";

// Our second argument is java.library.path (PATH to find a shared library)

options[1].optionString = "-Djava.library.path=c:\\myjni\\libs";

// Populate JavaVMInitArgs structure with options details

JavaVMInitArgs vm_args;

vm_args.version  = JNI_VERSION_1_2;

vm_args.nOptions = MAX_OPTIONS;

vm_args.options  = options;

vm_args.ignoreUnrecognized = true;

一旦在一个JavaVMInitArgs结构中准备好了 JVM 参数,就只需要一次 JNI 函数调用,就可以用本机代码创建一个 JVM 了。JNI_CreateJavaVM() JNI 函数接受三个参数。第一个参数是一个指向代表 JVM 的JavaVM结构的指针。第二个参数是一个指向JNIEnv结构的指针,它是 JNI 接口。第三个参数是 JVM 的初始参数。下面的代码片段展示了如何用本机代码创建 JVM。您需要检查JNI_CreateJavaVM()函数可能返回的任何错误。如果不能创建 JVM,它返回JNI_ERR

JNIEnv *env;

JavaVM *jvm;

long status;

status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);

if (status == JNI_ERR) {

printf("Could not create VM. Exiting application...\n");

return 1;

}

一旦获得了JNIEnv结构,就可以用它来查找一个类,创建该类的一个对象,并对该对象执行任何方法。事实上,它允许您使用 JNI 访问整个 JVM。

在使用完 JVM 之后,您需要销毁它。

// Destroy JVM

jvm->DestroyJavaVM();

清单 8-13 包含了一个带有静态方法printMsg()EmbeddedJVMJNI类的代码,用于在标准输出中打印一条消息。稍后,您将使用本机代码创建一个 JVM,并调用printMsg()方法。

清单 8-13。嵌入式 JVMJNI Java 类

// EmbeddedJVMJNI.java

package com.jdojo.jni;

public class EmbeddedJVMJNI {

public static void printMsg(String msg) {

System.out.println(msg);

}

}

清单 8-14 中列出的 C++控制台应用创建一个 JVM 并调用EmbeddedJVMJNI类的printMsg()方法。本书的源代码包含了createjvm.cpp文件中的 C++代码。该程序允许您将类路径指定为命令行参数。如果不指定类路径,它将使用当前目录作为类路径。

清单 8-14。在本地应用中创建 jvm 的 createjvm.cpp 文件的内容

// createjvm.cpp

#include <jni.h>

#include <iostream>

#include <string>

int main(int argc, char **argv) {

std::string classpath("");

if (argc < 2) {

std::cout << "You did not pass the classpath."

<< " Using the current directory as the classpath.\n";

classpath = ".";

}

else {

classpath = argv[1];

}

std::string classpathOption("-Djava.class.path=");

classpathOption = classpathOption + classpath;

// Pass the classpath as an argument to the JVM

const jint MAX_OPTIONS = 1;

JavaVMOption options[MAX_OPTIONS];

options[0].optionString = (char *)(classpathOption.c_str());;

// Prepare the JVM initial arguments

JavaVMInitArgs vm_args;

vm_args.version = JNI_VERSION_1_2;

vm_args.nOptions = MAX_OPTIONS;

vm_args.options = options;

vm_args.ignoreUnrecognized = true;

// Create the JVM

JavaVM *jvm;

JNIEnv *env;

long status = JNI_CreateJavaVM(&jvm, (void**) &env, &vm_args);

if (status == JNI_ERR) {

std::cout << "Could not create VM. Exiting application...\n";

return 1;

}

const char *className = "com/jdojo/jni/EmbeddedJVMJNI";

jclass cls = env->FindClass(className);

if (cls == NULL) {

// Print exception stack trace and destroy the JVM

env->ExceptionDescribe();

jvm->DestroyJavaVM();

return 1;

}

if (cls != NULL) {

jmethodID mid = env->GetStaticMethodID(cls, "printMsg",

"(Ljava/lang/String;)V");

if (mid != NULL) {

jstring m = env->NewStringUTF("Hello from C++...\n");

env->CallStaticVoidMethod(cls, mid, m);

if (env->ExceptionCheck()) {

env->ExceptionDescribe();

env->ExceptionClear();

}

}

}

// Destroy JVM

jvm->DestroyJavaVM();

return 0;

}

您需要将createjvm.cpp文件编译成可执行文件。当你编译这个程序的时候,你需要提供jvm.lib文件的路径,这个文件安装在 Windows 的JAVA_HOME\lib目录下。假设您已经在 Windows 上的C:\java8中安装了 JDK,您可以使用下面的命令在 Windows 上创建createjvm.exe文件:

C:> g++ -IC:/java8/include -IC:/java8/include/win32

-o createjvm

createjvm.cpp

C:/java8/lib/jvm.lib

该命令在一行中输入,但为了便于阅读,它显示在多行中。该命令的前两行与您之前用来创建共享库的行相同。–o选项用于指定可执行输出文件名,在本例中是createjvm。最后一个选项是需要静态链接的名为jvm.lib的库的路径。

下面的命令将在 Linux 上创建一个createjvm可执行文件,假设您已经在/home/ksharan/java8目录中安装了 JDK:

$ g++ -I/home/ksharan/java8/include -I/home/ksharan/java8/include/linux

-o createjvm

createjvm.cpp

/home/ksharan/java8/jre/lib/i386/client/libjvm.so

在 Windows 上,当你运行createjvm.exe应用时,它会寻找jvm.dll共享库,这个库在JRE_HOME\bin\client目录中。您需要在PATH环境变量中包含包含jvm.dll文件的目录。

C:\> SET PATH=C:\java8\bin\client;%PATH%

C:\> createjvm C:\myclasses

Hello from C++...

当您运行createjvm.exe文件时,您可能会得到以下错误:

Exception in thread "main" java.lang.NoClassDefFoundError: com/jdojo/jni/EmbeddedJVMJNI

Caused by: java.lang.ClassNotFoundException: com.jdojo.jni.EmbeddedJVMJNI

...

该错误表明类路径设置不正确,JVM 无法找到EmbeddedJVMJNI类。使用上面的命令,在C:\myclasses目录中搜索该类。要修复这个错误,要么使用正确的类路径参数运行createjvm应用,要么将com\jdojo\jni\EmbeddedJVMJNI.class文件移动到C:\myclasses目录中。

在 Linux 上,您需要设置LD_LIBRARY_PATH,这样当 createjvm 应用运行时,就会加载libjvm.so文件。您可以按如下方式进行设置:

$ export LD_LIBRARY_PATH=/home/ksharan/java8/jre/lib/i386/client

现在,您已经准备好运行 createjvm 应用,如下所示:

$ ./createjvm /home/ksharan/myclasses

Hello from C++...

该命令将在/home/ksharan/myclasses目录中搜索com/jdojo/jni/EmbeddedJVMJNI.class

本机代码中的同步

JNI 提供了两个名为MonitorEnter()MonitorExit()的函数,用于在多线程环境中同步对本地代码的访问。这些函数被串联使用,它们的使用等同于在 Java 代码中使用synchronized关键字。这些函数声明如下:

  • jint MonitorEnter(jobject obj)
  • jint MonitorExit(jobject obj)

如果成功,两个函数都返回 0 ( JNI_OKjni.h头文件中被定义为 0),如果失败,则返回负数。您必须检查它们的返回值,以正确处理代码同步。下面是使用同步的 Java 代码示例:

Object someObject = get the reference of a java object;

// Other logic goes here

synchronized(someObject) {

// Synchronized code goes here

}

等效的本机代码如下:

jobject someObject = get the reference of a java object;

// Other logic goes here

jint enterStatus = env->MonitorEnter(someObject);

if (enterStatus != JNI_OK) {

// Handle the error condition here

}

// Synchronized code goes here

jint exitStatus = env->MonitorExit(someObject);

if (exitStatus != JNI_OK ) {

// Handle the error condition here

}

Java wait()notify()没有对等的 JNI 函数来帮助线程同步。然而,您总是可以从本机代码中调用这两个 Java 方法。

摘要

Java 本地接口(Java Native Interface,JNI)是一种编程接口,它有助于 Java 程序与用 C、C++、FORTRAN 等本地语言编写的程序之间的交互。JNI 使得在 Java 代码中使用一个方法并在本地语言(如 C 或 C++)中实现该方法成为可能。JNI 还使得将 JVM 嵌入到可以访问 Java 类库的本地应用中成为可能。

在 Java 中使用但在本地语言中实现的方法称为本地方法,使用关键字native声明。Java 中的native方法没有主体。它的主体用分号表示。native方法的实现是用本地语言编写的,并被编译成一个共享库。使用java.library.path JVM 选项使共享库对 Java 运行时可用,或者它们位于 PATH 环境变量中。

javah命令用于生成本地语言所需的头文件。它将包含本机方法的类的完全限定类名作为参数。

JNI 定义了 Java 和本机代码中使用的数据类型之间的映射。比如jbooleanjcharjint等。相当于本地的booleancharint等。Java 中的原始数据类型。本机代码中的jclassjobjectjstring类型映射到 Java 中的ClassObjectString类。

JNI 提供了一些函数来促进字符串的 Java 表示和本机表示之间的转换。它还提供了特殊的函数来访问 Java 数组和数组元素的长度。

JNI 还允许您在本机代码中创建 Java 对象。您还可以访问本机代码中 Java 对象的字段和方法。

Java 中的Throwable类型被映射到本机代码中的类型jthrowable。JNI 允许您处理本机代码中的异常。本机代码可以检测和处理由于调用 JNI 函数而在 JVM 中引发的异常。本机代码也可以抛出异常,该异常可以传播到 Java 代码。如果在本机代码执行过程中引发异常,本机代码将继续执行,异常将保持挂起状态,直到控制权返回给 Java 代码。

JNI 允许您将 JVM 嵌入到本地应用中,从而为它们提供对丰富的 Java 类库的完全访问权。JNI API 中允许您用本机代码创建和加载 JVM 的部分称为调用 API。JVM 是使用调用 API 提供的JNI_CreateJavaVM()方法在本机代码中创建的。

在多线程环境中,可以通过使用两个名为MonitorEnter()MonitorExit()的 JNI 函数来同步对本机代码中关键部分的访问。这些函数被串联使用,它们的使用等同于在 Java 代码中使用synchronized关键字。