JNDI在JavaEE中的角色_动力节点Java学院整理

时间:2022-09-01 14:31:07

虽然 J2EE 平台提高了普通企业开发人员的生活水平,但是这种提高是以不得不学习许多规范和技术为代价的,这些规范和技术则是 J2EE 为了成为无所不包的分布式计算平台而整合进来的。Dolly Developer 是众多开发人员中的一员,她已经发现了一个特性,该特性有助于缓解随企业级应用程序部署而带来的负担,这个特性就是 JNDI,即 Java 命名与目录接口(Java Naming and Directory Interface)。让我们来看看 Dolly 在没有 JNDI 的时候是怎么做的,以及她是如何正确地应用 JNDI 来改善其状况的。

所有人都非常熟悉的旅程

Dolly Developer 正在编写使用 JDBC 数据源的 Web 应用程序。她知道自己正在使用 MySQL,所以她将一个对 MySQL JDBC 驱动程序类的引用进行了编码,并通过使用适当的 JDBC URL 连接到其 Web 应用程序中的数据库。她认识到数据库连接池的重要性,所以她包含了一个连接池包,并把它配置成最多使用 64 个连接;她知道数据库服务器已经被设置成最多允许 128 台客户机进行连接。

Dolly 在走向灾难

在开发阶段,每件事都进行得很顺利。但是,在部署的时候,开始失控。Dolly 的网络管理员告诉她,她不能从她的桌面机访问生产服务器或登台服务器(staging server),所以她不得不为每个部署阶段开发不同的代码版本。因为这种情况,她需要一个新的 JDBC URL,所以还要为测试、阶段和生产进行独立的部署。(一听到要在每个环境中建立单独部署,熟悉配置管理的人会战战兢兢的,但是既然这是种非常普遍的情况,所以他们也只好硬着头皮上了。)

就在 Dolly 认为通过不同的 URL 建立彼此独立的部署已经解决了自己的配置问题时,她发现她的数据库管理员不想在生产环境中运行 MySQL 实例。他说,MySQL 用作开发还可以,但是对于任务关键型数据而言,业务标准是 DB2®。现在她的构建不仅在数据库 URL 方面有所不同,而且还需要不同的驱动程序。

事情越变越糟。她的应用程序非常有用,并且变得非常关键,以致于它从应用服务器那里得到了故障恢复的能力,并被复制到 4 个服务器集群。但是数据库管理员提出了*,因为她的应用程序的每个实例都要使用 64 个连接,而数据库服务器总共只有 200 个可用连接 —— 全部都被 Dolly 的应用程序占用了。更麻烦的是,DBA 已经确定 Dolly 的应用程序只需要 32 个连接,而且每天只有一个小时在使用。随着她的应用程序规模扩大,应用程序遇到了数据库级的争用问题,而她的惟一选择就是改变集群的连接数量,而且还要做好准备,在集群数量增长或者应用程序复制到另一个集群时再重复一次这样的操作。看来她已经决定了如何配置应用程序,应用程序的配置最好是留给系统管理员和数据库管理员来做。

J2EE 的角色

如果 Dolly 在开发应用程序时了解 J2EE 所扮演的角色,那么她就可能避免遭遇这种困境。J2EE 规范把职责委托给多个开发角色:组件提供者(Component Provider)、应用程序组装者(Application Assembler)、部署人员(Deployer)和系统管理员(System Administrator)。(在许多公司中,组件提供者和组件组装者的角色是融合在一起的,部署人员和系统管理员的角色是融合在一起的。)在真正了解 J2EE 中的 JNDI 角色之前,掌握 J2EE 角色的作用非常重要。

组件提供者

这个角色负责创建 J2EE 组件,J2EE 组件可以是 Web 应用程序、企业级 JavaBean(EJB)组件,或者是应用程序客户机(例如基于 Swing 的 GUI 客户机应用程序)。组件提供者包括:HTML 设计师、文档编程人员以及其他开发人员角色。大多数 J2EE 开发人员在组件提供者这一角色上耗费了相当多的时间。

应用程序组装者

这个角色将多个 J2EE 模块捆绑成一个彼此结合的、可以部署的整体:企业归档(EAR)文件。应用程序组装者要选择组件,分清它们之间的交互方式,配置它们的安全性和事务属性,并把应用程序打包到 EAR 文件中。许多 IDE,例如 WebSphere® Studio、IDEA、JBuilder、WebLogic Workshop 和其他 IDE,都可以帮助应用程序组装者以交互方式配置 EAR 文件。

部署人员(Deployer)

这个角色负责部署,这意味着将 EAR 安装到 J2EE 容器(应用服务器)中,然后配置资源(例如数据库连接池),把应用程序需要的资源绑定到应用服务器中的特定资源上,并启动应用程序。

系统管理员(System Administrator)

这个角色负责保证容器需要的资源可用于容器。

角色实战

假设有一个企业应用程序,该应用程序包含一个 Web 应用程序,还有一个负责业务逻辑和持久性的 EJB 组件。开发这个应用程序的组件供应商可能有许多,但是在许多情况下,可以由一个人来承担全部职责。组件可以包含数据传输对象(一个 JAR 文件)、EJB 接口(另一个 JAR 文件)、EJB 实现本身(另一个 JAR 文件),以及用户界面组件 —— servlet、JSP、HTML 页面和其他静态 Web 内容。用户界面组件被进一步打包成 Web 应用程序,其中包含 servlet 类、JSP 文件、静态内容,以及其他必需组件的 JAR(包括 EJB 接口)。

这听起来好像用到的组件太多了,几乎超出了人的想像范围,尤其是在考虑构建一个典型的 Web 应用程序需要使用多少个 JAR 文件的时候。但是,重要的是认识到在这里必须小心地管理依赖性。接口和传输对象是 Web 应用程序和 EJB 实现可以依赖的对象,但是依赖性的方向应该是相同的;还要避免产生循环依赖。J2EE 组件(例如 WAR 文件和 EJB JAR 文件)必须在它们的部署单元之外声明它们在资源上的依赖性。

应用程序组装者负责把 Web 应用程序中的依赖内容包含进来,并把它们整体打包成单个企业应用程序。工具在这里帮助很大。IDE 可以协助创建反映模块和 JAR 依赖性的项目结构,还允许您随意指定包含或排除的模块。

部署人员负责确保部署环境中存在组件所需的资源,并将组件绑定到平台的可用资源上。例如,Web 应用程序中的外部 EJB 引用(部署描述符中的 ejb-ref)就是在此时被绑定到实际部署的 EJB 组件 —— 而且是立即绑定。

外部资源的后绑定

任何不平凡(nontrivial)的 J2EE 应用程序都需要访问描述它期望使用环境的信息。这意味着开发和测试组件时,为了临时测试代码,开发人员要承担一些部署方面的职责。重要的是要理解:这么做的时候,您就走出了开发人员的领域。否则,可以试着依靠 JDBC 驱动程序,或 URL、JMS 队列名称,或者其他具有无意识的、偶尔可能是灾难性暗示的机器资源。

JNDI 前来援助

Dolly 的问题的解决方案是从她的应用程序中清除所有对数据存储的直接引用。没有对 JDBC 驱动程序的引用,没有服务器名称,没有用户名称或口令 —— 甚至没有数据库池或连接管理。Dolly 需要编写代码来忽略将要访问的特定外部资源,只需要知道其他人会提供使用这些外部资源所需的链接即可。这允许部署人员(任何处在这个角色的人)把数据库连接分配给 Dolly 的应用程序。Dolly 没有必要参与其中。(从数据库安全性到遵守 Sarbanes-Oxley 法案,她都没有参与进来,她这样做也有充足的业务理由。)

许多开发人员知道:代码和外部资源之间的紧密耦合是潜在的问题,但是在实践中却经常忘记角色的划分。在小型开发工作中(指的是团队规模或部署规模),即使忽视角色划分也能获得成功。(毕竟,如果应用程序只是个人的应用程序,而且您不准备依靠它,那么把应用程序锁定在特定的 PostgreSQL 实例上也挺好的。)

J2EE 规范要求所有 J2EE 容器都要提供 JNDI 规范的实现。JNDI 在 J2EE 中的角色就是“交换机” —— J2EE 组件在运行时间接地查找其他组件、资源或服务的通用机制。在多数情况下,提供 JNDI 供应者的容器可以充当有限的数据存储,这样管理员就可以设置应用程序的执行属性,并让其他应用程序引用这些属性(Java 管理扩展(Java Management Extensions,JMX)也可以用作这个目的)。JNDI 在 J2EE 应用程序中的主要角色就是提供间接层,这样组件就可以发现所需要的资源,而不用了解这些间接性。

Dolly 的情况更糟了

现在我们重新来看一下 Dolly 的情况。在其简单的 Web 应用程序中,她直接从应用程序代码中使用了一个 JDBC 连接。参见清单 1,我们可以看出,Dolly 显式地把 JDBC 驱动程序、数据库 URL 以及她的用户名和口令编码到了 servlet 中:

清单 1. 典型(但是不好)的 JDBC 用法

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Connection conn=null;
try {
 Class.forName("com.mysql.jdbc.Driver",
        true, Thread.currentThread().getContextClassLoader());
 conn=DriverManager.getConnection("jdbc:mysql://dbserver?user=dolly&password=dagger");
 /* use the connection here */
 c.close();
}
catch(Exception e) {
 e.printStackTrace();
}
finally {
 if(conn!=null) {
  try {
   conn.close();
  } catch(SQLException e) {}
 }
}

如果不用这种方式指定配置信息,Dolly(以及她的同伴们)使用 JNDI 来查找 JDBC DataSource 会更好一些,如清单 2 所示:

清单 2. 使用 JNDI 得到数据源

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Connection conn=null;
try {
 Context ctx=new InitialContext();
 Object datasourceRef=ctx.lookup("java:comp/env/jdbc/mydatasource");
 DataSource ds=(Datasource)datasourceRef;
 Connection c=ds.getConnection();
 /* use the connection */
 c.close();
}
catch(Exception e) {
 e.printStackTrace();
}
finally {
 if(conn!=null) {
  try {
   conn.close();
  } catch(SQLException e) { }
 }
}

为了能够得到 JDBC 连接,首先要执行一些小的部署配置,这样我们才可以在本地组件的 JNDI 下文中查找 DataSource。这可能有点烦琐,但是很容易学。不幸的是,这意味着即使是为了测试组件,开发人员也必须涉足部署人员的领地,并且还要准备配置应用服务器。

配置 JNDI 引用

为了让 JNDI 解析 java:comp/env/jdbc/mydatasource 引用,部署人员必须把 <resource-ref> 标签插入 web.xml 文件(Web 应用程序的部署描述符)。 <resource-ref> 标签的意思就是“这个组件依赖于外部资源”。清单 3 显示了一个示例:

清单 3. resource-ref 入口

?
1
2
3
4
5
6
<resource-ref>
 <description>Dollys DataSource</description>
 <res-ref-name>jdbc/mydatasource</res-ref-name>
 <res-ref-type>javax.sql.DataSource</res-ref-type>
 <res-auth>Container</res-auth>
</resource-ref>

<resource-ref> 入口告诉 servlet 容器,部署人员要在 组件命名上下文(component naming context) 中设置一个叫做jdbc/mydatasource 的资源。组件命名上下文由前缀 java:comp/env/ 表示,所以完全限定的本地资源名称是:java:comp/env/jdbc/mydatasource.

这只定义了到外部资源的本地引用,还没有创建引用指向的实际资源。(在 Java 语言中,类似的情况可能是: <resource-ref> 声明了一个引用,比如 Object foo,但是没有把 foo 设置成实际引用任何 Object。)

部署人员的工作就是创建 DataSource(或者是创建一个 Object 对象,让 foo 指向它,在我们的 Java 语言示例中就是这样)。每个容器都有自己设置数据源的机制。例如,在 JBoss 中,是利用服务来定义数据源(请参阅 $JBOSS/server/default/deploy/hsqldb-ds.xml,把它作为示例),它指定自己是 DataSource 的全局 JNDI 名称(默认情况下是 DefaultDS)。在创建资源之后,第三步仍然很关键:把资源连接或者绑定到应用程序组件使用的本地名称。在使用 Web 应用程序的情况下,是使用特定于供应商的部署描述符扩展来指定这个绑定,清单 4 中显示了一个这样的例子。(JBoss 用称为 jboss-Web.xml 的文件作为特定于供应商的 Web 应用程序部署描述符。)

清单 4. 用特定于供应商的部署描述符将资源绑定到 JDI 名称

?
1
2
3
4
<resource-ref>
  <res-ref-name>jdbc/mydatasource</res-ref-name>
  <jndi-name>java:DefaultDS</jndi-name>
</resource-ref>

这表明应该将本地资源引用名称( jdbc/mydatasource)映射到名为 java:DefaultDS 的全局资源。如果全局资源名称出于某种原因发生了变化,而应用程序的代码无需变化,那么只需修改这个映射即可。在这里,有两个级别的间接寻址:一个定义并命名资源(java:DefaultDS),另一个把特定于本地组件的名称( jdbc/mydatasource)绑定到命名的资源。(实际上,当您在 EAR 级别上映射资源时,可能还存在第三级别的间接寻址。)

超越数据源

当然,J2EE 中的资源并不局限于 JDBC 数据源。引用的类型有很多,其中包括资源引用(已经讨论过)、环境实体和 EJB 引用。特别是 EJB 引用,它暴露了 JNDI 在 J2EE 中的另外一项关键角色:查找其他应用程序组件。

试想一下这种情况:当一家公司从 Order Ontology Processing Services(OOPS)购买了一个可部署的 EJB 组件来处理客户订单时,会发生什么。为了便于举例说明,我们把它叫做 ProcessOrders V1.0。ProcessOrders 1.0 有两部分:一组接口和支持类(home 和 remote 接口,以及支持的传输类);实际 EJB 组件自身。选择 OOPS 是因为它在这个领域的专业性。

该公司遵照 J2EE 规范,编写使用 EJB 引用的 Web 应用程序。公司的部署人员把 ProcessOrders 1.0 绑定到 JNDI 树中,将它用作ejb/ProcessOrders/1.0,并解析 Web 应用程序的资源名称,以指向这个全局 JNDI 名称。目前为止,这些都是 EJB 组件非常普通的用法。但是,当我们考虑公司的开发周期与公司供应商之间的交互时,事情就变得复杂起来。在这里,JNDI 也能帮助我们。

我们假设 OOPS 发布了一个新版本,即 ProcessOrders V1.1。这个新版本有一些新功能,公司内部的一个新应用程序需要这些新功能,而且很自然地扩展了 EJB 组件的业务接口。

在这里,公司有以下几个选择:可以更新所有应用程序来使用新版本,也可以编写自己的版本,或者使用 JNDI 的引用解析,允许每个应用程序在不影响其他应用程序的情况下使用自己的 EJB 组件版本。立刻更新所有应用程序对维护来说是一场噩梦,它要求对所有组件都进行完整的回归测试,这通常是一项艰巨的任务,而且如果发生任何功能测试失败的话,那么还要进行另一轮调试。

编写内部(in-house)组件常常是没有必要的重复工作。如果组件是由在这个业务领域内具有专业知识的公司编写的,那么给定的 IT 商店不可能像专业的组件供应商那样精通业务功能。

正如您可能已经猜到的那样,最好的解决方案是用 JNDI 解析。EJB 的 JNDI 引用非常类似于 JDBC 资源的引用。对于每个引用,部署人员都需要把新组件按特定的名称(比如说 ejb/ProcessOrders/1.1)绑定到全局树中,对于需要 EJB 组件的其他每个组件,还要为组件在部署描述符中解析 EJB 引用。依赖于 V1.0 以前的应用程序不需要进行任何修改,也不需要重新测试,这缩短了实现的时间、降低了成本并减少了复杂性。在服务趋于转换的环境中,这是一种很有效的方法。可以对应用程序架构中所得到的所有组件进行这类配置管理,从 EJB 组件到 JMS 队列和主题,再到简单配置字符串或其他对象,这可以降低随时间的推移服务变更所产生的维护成本,同时还可以简化部署,减少集成工作。