内容


走出 JNDI 迷宫

学习从容地寻找 bean 的Home接口

Comments

如果您在让客户机应用程序看到 EJB 组件这一方面从来都没有任何问题,并且认为在 Java 平台的不同安装上、或者在完全不同的计算机上运行您的客户机和 bean 一点也不复杂,那么本文可能不会吸引您。不过,如果您才刚刚开始,并且在试图用真实的配置做任何事情时看见弹出了许多奇怪和意义不明的错误消息,那么就请读下去。

EJB 错误?不要慌!

您已经在自己所钟爱的 Java 书籍中读过了关于企业 Javabean 技术的那一章,也已经练习过了简单的 HelloWorld bean,并遵循所建议的部署过程发布了它。现在您得编写一个客户机,以便通过这个客户机来调用这个杰作。因此您写出了类似清单 1 中的代码:

清单 1. 一个调用 bean 的非常简单的客户机
InitialContext ic = new InitialContext();
Object or = ic.lookup("ejb/HelloWorldHome");
if (or != null) {
  // Narrow the return object to the Home class type
    HelloWorldHome home = 
      (HelloWorldHome)PortableRemoteObject.narrow(or, 
        HelloWorldHome.class);
  // Create an EJB object instance using the home interface.
    HelloWorld hw = home.create();
  // Invoke the method
    System.out.Println(hw.hello());
}

在命令行中运行这个客户机,使用手头最方便的一个 Java 安装 ―― 即应用服务器使用的那一个。所有事情都很完美!带着成功的喜悦,您转移到第二台计算机上运行您的客户机。这回,您得到了一个可怕的错误消息。首先,您可能得到 java.lang.NoClassDefFoundError: javax/ejb/EJBObject ,然后是一大堆其他的 NoClassDefFoundError s,因为您忘记提交一个带有必需的 stub 和 tie 的 JAR 文件,并且没有提供或者考虑到其他各种 EJB 相关的内容。不过最终,您的客户机运行到了第一行有意思的代码( InitialContext ic = new InitialContext(); )。在到达这一行时得到的异常 ―― 您几乎肯定会得到一个异常 ―― 将会根据您所选择的特定 上下文 provider而有所不同。

解释这些术语

在我们继续往下之前,定义几个术语会很有帮助。计算世界使用的都是一些奇怪的术语、时髦的语汇和首字母缩写词,Java 技术也不例外(也许这应该是 JavaIsNoException ?)。如果您遇到了上面所说的问题,那么这里面的术语可能会让您感到有些无所适从。所以让我们讨论在本文中将会遇到的术语,搞明白它们的意思是一个好主意。

名称空间、上下文、初始上下文和子上下文 这些术语都是有关位置的 ―― 是从客户机的角度看时 EJB 组件所在的概念性的位置。将一个 名称空间 想像为一个城镇,城镇中的商店由 EJB home接口(我们将在稍后讨论它)表示。 上下文是城镇中的一个位置。 初始上下文 是您开始时所在的位置 ―― 就像它是到城镇的道路。而 子上下文是街道名。

home接口(home interface)和远程接口(remote interface) 企业 JavaBean 组件有三个部分。首先是 bean 代码本身。然后是 home接口,它定义了创建您自己的 EJB bean 的方法。home接口是在名称空间中发布的。当您有了home接口后,就可以调用 Create() 以从应用服务器获得远程接口。获得了远程接口后,就可以调用构成实际的 EJB 代码的方法了。

如何将这些术语应用到您的城镇模拟中去呢?到达正确的城镇并找到正确的地址后,您需要走进商店或者按铃(调用 Create() )。这个过程对于您要去的所有商店都是一样的,不过,您所收到的响应取决于是由谁来提供服务 ―― 比如是一位屠夫、一位面包师还是一位烛台制作者。这个响应代表了 远程接口。每个人都是不同的并且可以要求他提供不同的东西。您必须知道与您交谈的人(即 bean)的职业才能提出正确的问题(即调用正确的方法) ―― 向一位屠夫要一条面包可不妥当。

CosNaming、LDAP 和 JNDI Java 命名和目录接口(Java Naming and Directory Interface JNDI)提供了一个标准接口,它指明您需要如何与名称空间交互。我们所提到的 LDAPCosNaming 就是 JDNI 名称空间类型。现在扩展我们的比喻:JNDI 是城镇的模板,而 CosNaming 和 LDAP 是特定的城镇。它们以相似的方式操作,但是有不同的布局。

属性提供了一个映射

让我们看一看如何使用所有这些元素以成功地从远程计算机上调用我们的 EJB 组件上的方法。为了让客户程序连接到您精心打造的 EJB 组件,需要几样东西。首先,它需要客户代码的所有 JAR 文件、一般性的 EJB 相关 JAR 文件如 J2EE.jar 以及在部署 bean 时生成的 stub 和 tie。这些文件让您的客户机可以一直到达初始上下文。

接下来您的客户机需要的信息是一些属性的值。首先,您将需要几个 java.naming.factory.initial 的值。该属性指向一个提供初始上下文工厂的类。该属性的一个典型值是 com.sun.jndi.cosnaming.CNCtxFactory ,这也是我们在这里的几个例子中所使用的值。这个类存在于 rt.jar 中,因而它是基本 JVM 的一部分。工厂是由 CosNaming 命名服务器所使用的,但是 JVM 还包括一个 LDAP 工厂。我们在后面将会看到,不同的应用服务器提供它们自己的初始上下文工厂。

这个类连同命名服务器 URL 和端口号的详细信息,用于生成与名称空间交互的 InitialContext 类。不过,如果没有 provider URL,那么它将连接到 localhost 的 900 端口(或者您的上下文工厂的其他默认端口)。要连接到远程服务器,您需要有属性 java.naming.provider.url 的一个值。

新程序员对于所有这些觉得很难理解的原因是:不管您在应用服务器本地运行任何东西,这东西通常都会听话地工作。这是由于环境照管了一切,当您要求一个 InitialContext 时,环境就会给您提供您想要的那个。但是当您将客户即转移到不同的计算机上时,就得靠自己了。您需要知道拷贝哪一个 JAR 文件,以及要做哪些设置。我知道有些人为使他们的客户机正确工作,将应用服务器上的所有 JAR 文件都拷贝到第二台计算机上!

在默认情况下, InitialContext 工厂是在 jndi.properties 中定义的,这个工厂类有默认的服务器 URL 和端口号默认值。这个文件在类路径中(这一般意味着在本地目录)或者在您的类路径中的任何 JAR 中。不同的应用服务器可能在不同的 JAR 文件中提供它们的默认值,WebSphere Application Server 在 namingclient.jar 中储存一个默认副本。要指定您自己的默认值,只需要编辑在类路径中的第一个副本。这是配置属性的一种方法,如果缺少命令行或者代码驱动的设置,那么客户机将使用 jndi.properties 中的值。不过,虽然这可能适合于简单的设置,但是如果处理多个服务器和名称空间,那么您可能希望一个客户一个客户地进行配置。

这些属性是如何根据我们要使用的名称空间而使用不同的值的呢?正如前面提到的,有两种形式的 JNDI 名称空间:CosNaming 和 LDAP。其中每一个都有与之相关联的传输:分别是 IIOP 和 LDAP。一个 LDAP 名称空间使用 LDAP 传输(您将用一个像 ldap://myldapnameserver 这样的 URL 连接到它),而 CosNaming 使用一个 IIOP 传输(您将用一个像 iiop://mycosnamingserver 这样的 URL 连接到它)。CosNaming 的默认端口号是 900,而 LDAP 的默认端口号是 389。不过,任何给定的名称空间服务器实现使用的默认值可能是不同的。

用命令行配置属性

让我们看一下如何用命令行配置属性。如果您要在家里自己练习,进入 JDK 安装中的 bin 目录。在这个文件夹中,可以找到一个名为 tnameserv.exe 的程序(对于 Windows)或者只是 tnameserv (对于基于 UNIX 的系统)。通过执行这个程序将会在端口 900 启动一个示例 CosNmaing 命名服务器。

现在正好可以用一个可以查看 CosNaming 名称空间的实用工具来装备您自己。我本人使用 Eclipse 作为开发环境,我在下面的 参考资料 部分中提供了到 JNDI 浏览器插件的链接。理论上,您应该可以将一个名称空间浏览器指向自己计算机的端口 900,并看到一个非常无聊的空名称空间(尽管一些应用服务器在默认情况下会用很多不同的内容填充名称空间)。为了丰富我们的名称空间,我们现在将编写一个简单的程序以在它里面放一些内容,如清单 2 所示:

清单 2. 一个简单的 cosNaming 名称空间交互
package example.publisher;

import javax.naming.InitialContext;

public class Publish {

    public static void main(String[] args) {
        //
        //This example creates a subcontext in a namespace
        //
        try{
            InitialContext ic = new InitialContext();
            ic.createSubcontext("Test");
        }catch(Exception e){
            System.out.println(e);
            e.printStackTrace();
            
        }
    }
}

这个应用程序将假定为得到正确的初始上下文件所需的所有属性都是可用的。所以现在可以从命令行运行它并在运行时提供这些属性(其中 URL 要根据您的环境作调整):

java -Djava.naming.factory.initial=com.sun.jndi.cosnaming.CNCtxFactory 
     -Djava.naming.provider.url=iiop://mymachine:900 
       example.publisher.Publish

一切正常,我们的客户会找到示例名称空间的上下文并创建名为 Test 的子上下文。您可以用名称空间浏览器确认这一点。

现在试着在一台计算机上运行命名服务器,用同一个命令行(当然,对 URL 再次做了调整)在另一台计算机上运行清单 2 中的应用程序。它运行起来应该没有问题(您可能需要修改这个例子以改变所限定的内容,甚至删除子上下文而不是创建它,这样在第二次运行时您就可以确信它已经起过作用了)。

在应用程序中配置属性

那么,如果不希望在命令行中设置这些属性怎么办?还有另外一个方法。可以在程序中显式地声明这些属性。这意味着您不需要为 java 命令提供特殊的选项。改变清单 2 中的代码以显式地设置所需要的属性后,它看起来与清单 3 中的代码一样:

清单 3. 简单的 cosNaming 名称空间交互,在应用程序代码中设置属性
package example.publisher;

import javax.naming.InitialContext;

public class Publish {

    public static void main(String[] args) {
        //
        //This example creates a subcontext in a namespace
        //
        try{
            Properties prop = new Properties();
            prop.setProperty("java.naming.factory.initial",
              "com.sun.jndi.cosnaming.CNCtxFactory");
            prop.setProperty("java.naming.provider.url",
              "iiop://mymachine:900");
            InitialContext ic = new InitialContext(prop);
            ic.createSubcontext("Test");
        }catch(Exception e){
            System.out.println(e);
            e.printStackTrace();
            
        }
    }
}

现在这个程序不再需要长长的命令行配置,不过要记住,以这种方式编写的应用程序硬编码了这些设置。

寻找通往 bean 的道路

到目前为止,我们已经看到了几个可以证明我们已连接到远程名称空间并完成一些任务的例子,尽管这些任务是相当无聊的 ―― 创建一个子上下文。在实际中,一般是由工具来为您完成所有的创建和发布工作,您 真正 需要的做是查找一个对象。在这一节,我们将在 CosNaming 名称空间中获得已发布的 HelloWorld bean 的 Home 接口。然后我们再看一下如何在 LDAP 名称空间中找到它的 Home 接口。

为了说明问题,我们假设您已经部署了 HelloWorld bean,它的home接口 HelloWorldHome 发布在 example/HelloWorldHome (如果您只想试一试,但是又不想自己创建一个 HellowWorld bean,那么在 参考资料中有一个下载预打包的 bean JAR 文件的链接以及一个使用它的客户机的文件)。

在上一节,我们进行了连接到命名服务器的艰苦工作,现在我们所需要的就只是查询 EJB 组件了。这需要我们向查询方法传递一个字符串,它表示从 InitialContext (您在城镇中的出发点)到想要去的 HomeInterface (房屋或者商店)的方向。听起来简单 ―― 但是这里您所选择的特定上下文工厂就要产生影响了。像 WebSphere 这样的应用服务器所带的工厂类并不总是把您放到名称空间的根上。所以我们为了查询 HomeInterface 而需要的字符串会根据 InitialContext 将您所放到城镇中的位置而变化。并且,在本地服务器上,上下文工厂可能将您放到与在远程服务器上不同的起始位置。

因为这个原因,我建议您不要像在清单 3 中那样硬编码所使用的查询字符串,而是用命令行或者属性文件传递。特别是对于具有多步的体系结构更应如此。例如,您可能有一个调用一个 EJB 组件的客户机,这个 bean 可能又需要调用也许是在不同的服务器上的第二个 EJB 组件!在这种情况下,属性应该在每一步中传递。这为反复实验(trial-and-error)查询提供了一种简单的机制,并且只需要相对较少的改变就可以得到最终应用程序的灵活性。因此让我们看一个示例查询应用程序。在清单 4 中,属性是在程序中设置的,但是它又以命令行值为依据。这样命令行与在我们前一个例子中使用的稍有不同,如我们在下面所看到的。

清单 4. 查询一个home接口
package example.lookup;
import java.util.Properties;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;

import example.HelloWorld;
import example.HelloWorldBean;
import example.HelloWorldHome;
import javax.naming.InitialContext;

public class Lookup {

    public static void main(String[] args) {
        //
        //This example looks up the home interface of an EJB to a namespace
        //
        try{
            Properties prop = new Properties();
            prop.setProperty("java.naming.factory.initial",args[0]);
            prop.setProperty("java.naming.provider.url",args[1]);
            InitialContext ic = new InitialContext(prop);
            Object or = ic.lookup(args[2]);
            if (or != null) {
                // Narrow the return object to the Home class type
                HelloWorldHome home = 
                  (HelloWorldHome)PortableRemoteObject.narrow(or, 
                      HelloWorldHome.class);
                // Create an EJB object instance using the home interface.
                HelloWorld hw = home.create();
                // Invoke the method
                System.out.Println(hw.hello());
            }
        }catch(Exception e){
            System.out.println(e);
            e.printStackTrace();
            
        }
    }
}

这个程序是用三个参数调用的:要使用的上下文工厂、provider URL 和包含要查询的名字的字符串。我们已经知道前两个是什么,那么第三个呢?

如果您仍然使用 tnameserv 作为命名服务器,那么您很可能将 bean 直接发布到 /example/HelloWorldHome 。在这种情况下,只要将 /example/HelloWorldHome 作为第三个参数传递就可以进行成功的查询。不过,如果您使用的命名服务器有一个更复杂的命名空间,那么可能会存在由所使用的部署工具增加的额外的层。例如,WebSphere 在默认情况下将 JavaBean 部署到 ejb/ ,但是这不是名称空间的根,并且只有当使用 WebSphere 的上下文工厂时,通过传入字符串 /ejb/example/HelloWorldHome 才会使您处于名称空间中的正确位置。 如果您使用一个与应用服务器提供的不同的上下文工厂(例如在一台只有标准 Java 安装的计算机上运行客户机时就需要这样做)时,这个问题会更加恶化。不过,应用服务器的命名服务器文档应当说明在查询 EJB 组件时将会从名称空间的什么地方开始。看一下文档中的例子,再用浏览器查看名称空间以确定其客户机的 InitialContext 会将它们放到什么地方。名称空间往往会循环,这样您就可以试着沿着一个分枝到无限。这意味着从最开始的上下文可以找到一条回家的道路。

总之,下面是传递相应参数给清单 4 中的应用程序的命令行:

java Lookup com.sun.jndi.cosnaming.CNCtxFactory 
  iiop://mymachine:900 example/HelloWorldHome

在 CosNaming 中,名称空间子上下文由斜线(/)字符分隔,这与标准 URL 一样。LDAP 的语法则不同,我们在下面将会看到。

介绍 LDAP

现在让我们再加上 LDAP。就本文的内容来讲,LDAP 是另一个 JNDI 名称空间,但是它的结构的表示方法与 CosNaming 名称空间的表示方法截然不同。它还需要一个不同的上下文工厂 ―― 但是这不成问题,因为我们总会在命令行指定正确的工厂(在我们的例子中,我们将使用属于基本 JVM 一部分的工厂,但是要记住不同的应用服务器可能有自己的工厂)。并且它需要一个指向不同命名服务器的指针 ―― 并且,幸运的是,我们也是在命令行中指定它的。当然,表示home接口位置的字符串是不同的,但是您猜如何?是的,我们还是在命令行中指定它。您可以看到使用这些命令行调用的好处:所有要做的只是改变调用我们的测试程序的方式,理论上我们可以到达任何 JDNI 命名服务器,甚至可以顺利地从 CosNaming 转移到 LDAP 而不用改变任何代码。是的,这是只是理论,当然无论如何,关键是要有正确的参数。

一些命名服务器会保护部分命名空间,这意味着只能发布到允许的区域。假设您有一个运行的 LDAP 服务器,它的细节如下:

  • URL: ldap://mymachine:1389
  • BaseDN: c=myldap

LDAP 名称空间中的树结构一般来说像下面这样:

ibm-wsnName=MyServer,ibm-wsnName=HelloWorldHome

不过,当我们将这个字符串传递给程序时,我们需要反转它(不要问我为什么)。所以我们使用的字符串看起来是这样的:

ibm-wsnName=HelloWorldHome,ibm-wsnName=MyServer,

BaseDN 表示在名称空间中您希望开始的位置。对于给定的 LDAP 命名服务器来说这可以是很多位置,这取决它是如何构造的。在这个例子中,我们直接到 c=myldap 的根。但是如果我们希望跳到名称空间中的一个树,那么可以指定 ibm-wsnTree=myTree,c=myldap 作为 BaseDN 而不是跳到那一点。

这样,我们将传递给程序的命令行参数就像下面这样:

java Lookup com.sun.jndi.ldap.LdapCtxFactory ldap://mymachine:1389/c=myldap/ 
     ibm-wsnName=HelloWorldHome,ibm-wsnName=MyServer

这里我们指定一个 LDAP 上下文工厂 ,然后传递 LDAP 服务器的名字以及我们想要开始的位置。然后是到要查询的 EJB 组件的反转的路径。我们可以用这个命令行调用在 CosNaming 例子中使用的同一段代码( 清单 4)。

当然,本文中使用的代码没有理由不能构成一个助手类的一个方法 ―― 它带三个参数,并且返回 Object or ,这个对象是在试图做任何事情之前调用 ic.lookup(args[2]) 时返回的。然后,当您需要进行一次查询时,只需使用这个助手类,向它传递适合于当前情况的适当参数,取回您所需要的对象引用,并准备将它窄化到实际的类。( 注意:我不保证这种类的性能,而只是提供这段原本就是如此的代码,我或者 IBM 对此不作任何保证。)当然,可以通过反射实现一种完全通用的方式,但这会使事情复杂得多,也超出了本文的范围。

在我们结束之前还有最后一件事要考虑。您可以编写一个结合了我们在清单 3 和 4 中使用的技术的客户机。它会检查命令行中是否给出了一个值;如果有,它就设置这些值,如果没有,它就使用硬编码的值。这样,在程序中可以有有意义的默认值,但是如果需要,也可以用命令行选项覆盖它们。只需要对代码进行微不足道的更改。

安全到家

作为回顾:下面是在有多个 EJB 查询和多个应用服务器的情况下,要使任何系统运行而应该有或者应该知道的最重要的四件事情:

  • 在任何给定阶段,下一阶段的所有 stub 和 tie 都必须在类路径上。除非环境知道这个类是什么样的,否则您不能窄化一个对象以使用它。
  • 每一阶段都需要与 EJB 相关的一般性 JAR 文件,如 J2EE.jar
  • 以参数的形式传递上下文工厂类型、命名服务器名和 JDNI 查询字符串。以便能够轻松顺应变化。
  • 知道您的名称空间。记住您的 JDNI 查询字符串需要将您从在名称空间中开始的位置移动到您的对象所储存的位置。但是您并不总是在同一位置开始!用一个工具浏览名称空间,并了解在本地和远程查询中是从什么位置开始的。

对于习惯于编写全部在同一台计算机上执行的代码的开发人员来说,浏览远程名称空间可能是一个困难的过程。希望本文的提示和代码可以帮助您设置并运行您的分布式 EJB 应用程序。当您掌握了 JNDI 名称空间后,再去看一下 developerWorks 上由 Brett McLaughlin 所写的 EJB 最佳实践系列(请参阅 参考资料),以获得用于优化代码的一些很棒的技巧。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=145401
ArticleTitle=走出 JNDI 迷宫
publish-date=12012003