Spring LDAP 实践教程(一)
零、简介
实用的 Spring LDAP 提供了 Spring LDAP 的完整覆盖,这是一个旨在减轻 LDAP 编程之苦的框架。这本书首先解释 LDAP 的基本概念,并向读者展示如何设置开发环境。然后深入到 Spring LDAP,分析它要解决的问题。在那之后,这本书将重点放在单元测试和集成测试 LDAP 代码的实践方面。接下来是对 LDAP 控件和新的 Spring LDAP 1.3.1 特性的深入研究,比如对象目录映射和 LDIF 解析。最后,本文最后讨论了 LDAP 身份验证和连接池。
这本书涵盖了什么
第一章从目录服务器的概述开始。然后讨论 LDAP 的基础知识,并介绍四种 LDAP 信息模型。最后介绍了用于表示 LDAP 数据的 LDIF 格式。
第二章重点介绍 Java 命名和目录接口(JNDI)。在本章中,您将看到如何使用普通 JNDI 创建与 LDAP 交互的应用。
第三章解释了什么是 Spring LDAP,以及为什么它是企业开发人员的重要选择。在本章中,您将设置创建 Spring LDAP 应用所需的开发环境,以及其他重要工具,如 Maven 和一个测试 LDAP 服务器。最后,您使用注释实现了一个基本但完整的 Spring LDAP 应用。
第四章涵盖了单元/模拟/集成测试的基础。然后,您将看到如何设置一个嵌入式 LDAP 服务器来对您的应用代码进行单元测试。您还将回顾生成测试数据的可用工具。最后,您使用 EasyMock 框架来模拟测试 LDAP 代码。
第五章介绍了 JNDI 对象工厂的基础知识,以及如何使用这些工厂来创建对应用更有意义的对象。然后,使用 Spring LDAP 和对象工厂检查完整的数据访问对象(DAO)层实现。
第六章介绍了 LDAP 搜索。本章从 LDAP 搜索的基本思想开始。然后,我介绍了各种 Spring LDAP 过滤器,它们使 LDAP 搜索变得更加容易。最后,您将看到如何创建一个定制的搜索过滤器来解决当前集合不足的情况。
第七章深入概述了可用于扩展 LDAP 服务器功能的 LDAP 控件。然后,使用排序和页面控件对 LDAP 结果进行排序和分页。
第八章处理对象-目录映射,这是 Spring LDAP 1.3.1 中引入的新特性。在这一章中,您将看到如何弥合域模型和目录服务器之间的差距。然后,使用 ODM 概念重新实现 DAO。
第九章在分析 Spring Framework 提供的事务抽象之前,介绍了事务和事务完整性的重要概念。最后,看一下 Spring LDAP 的补偿事务支持。
第十章从实现认证开始,这是针对 LDAP 执行的最常见的操作。然后,它使用 Spring 1.3.1 中引入的另一个特性来处理解析 LDIF 文件。在本章的最后,我将介绍 Spring LDAP 提供的连接池支持。
目标受众
LDAP 是为那些对使用 LDAP 构建 Java/JEE 应用感兴趣的开发人员设计的。它还教授了为 LDAP 应用创建单元/集成测试的技术。本书假设读者对 Spring 框架有基本的了解;预先接触 LDAP 是有帮助的,但不是必需的。已经熟悉 Spring LDAP 的开发人员将会找到可以帮助他们从框架中获得最大收益的最佳实践和示例。
下载源代码
本书中示例的源代码可以从www.apress.com下载。有关如何找到本书源代码的详细信息,请访问www.apress.com/source-code/。代码按章节组织,可以使用 Maven 构建。
代码使用 Spring LDAP 1.3.2 和 Spring Framework 3.2.4。它在 OpenDJ 和 ApacheDS LDAP 服务器上进行了测试。有关入门的更多信息,请参见第三章。
有问题吗?
如有疑问或建议,可联系作者balaji@inflinx.com。
一、LDAP 简介
在本章中,我们将讨论:
- 目录基础
- LDAP 信息模型
- 用于表示 LDAP 数据的 LDIF 格式
- 示例应用
我们每天都与目录打交道。我们使用电话簿来查找电话号码。当参观图书馆时,我们使用图书馆目录查找我们想读的书。对于计算机,我们使用文件系统目录来存储文件和文档。简单地说,目录是一个信息库。这些信息通常以易于检索的方式组织起来。
通常使用客户机/服务器通信模型来访问网络上的目录。希望在目录中读写数据的应用与专门的目录服务器进行通信。目录服务器对实际目录执行读或写操作。图 1-1 显示了这种客户端/服务器交互。
图 1-1 。目录服务器和客户机交互
目录服务器和客户端应用之间的通信通常使用标准化协议来完成。轻量级目录访问协议(LDAP)提供了与目录通信的标准协议模型。实现 LDAP 协议的目录服务器通常被称为 LDAP 服务器。LDAP 协议是基于早期的 X.500 标准,但是要简单得多(因此也是轻量级的),并且易于扩展。多年来,LDAP 协议经历了多次迭代,目前是 3.0 版本。
LDAP 概述
LDAP 定义了目录客户端和目录服务器使用的消息协议。通过考虑 LDAP 所基于的以下四个模型,可以更好地理解 LDAP:
- 信息模型决定了存储在目录中的信息的结构。
- 命名模型定义了如何在目录中组织和识别信息。
- 功能模型定义了可以在目录上执行的操作。
- 安全模型定义了如何保护信息免受未经授权的访问。
在接下来的章节中,我们将会看到每一个模型。
目录与数据库
初学者经常会感到困惑,把 LDAP 目录想象成一个关系数据库。像数据库一样,LDAP 目录存储信息。然而,有几个关键特征使目录有别于关系数据库。
LDAP 目录通常存储本质上相对静态的数据。例如,存储在 LDAP 中的员工信息(如电话号码或姓名)不会每天都发生变化。然而,用户和应用非常频繁地查找这些信息。由于目录中的数据被访问的频率高于更新的频率,LDAP 目录遵循 WORM 原则(en.wikipedia.org/wiki/Write_Once_Read_Many),并针对读取性能进行了大量优化。将经常变化的数据放在 LDAP 中没有意义。
关系数据库使用引用完整性和锁定等技术来确保数据的一致性。存储在 LDAP 中的数据类型通常没有这样严格的一致性要求。因此,这些特性中的大部分在 LDAP 服务器上是不存在的。此外,在 LDAP 规范中没有定义回滚事务的事务语义。
关系数据库的设计遵循规范化原则,以避免数据重复和数据冗余。另一方面,LDAP 目录是以分层的、面向对象的方式组织的。该组织违反了一些规范化原则。此外,LDAP 中没有表连接的概念。
尽管目录缺少上面提到的 RDBMS 的一些特性,但是许多现代 LDAP 目录都是建立在关系数据库(如 DB2)之上的。
信息模型
存储在 LDAP 中的基本信息单元称为条目。条目包含真实世界对象的信息,如雇员、服务器、打印机和组织。LDAP 目录中的每个条目都由零个或多个属性组成。属性是简单的键值对,保存着条目所代表的对象的信息。属性的关键部分也称为属性类型,它描述了可以存储在属性中的信息种类。属性的值部分包含实际信息。表 1-1 显示了一个代表雇员的条目的一部分。条目中的左列包含属性类型,右列保存属性值。
表 1-1 。员工 LDAP 条目
| 员工条目 | |
|---|---|
| 对象类 | inetOrgPerson |
| 给定名称 | 约翰 |
| 姓 | 锻工 |
| 邮件 | john@inflix.com |
| jsmith@inflix.com | |
| 可动的 | +1 801 100 1000 |
注意属性名默认不区分大小写。但是,建议在 LDAP 操作中使用 camel case 格式。
您会注意到 mail 属性有两个值。允许保存多个值的属性称为多值属性。另一方面,单值属性只能保存一个值。LDAP 规范不保证多值属性中值的顺序。
每种属性类型都与一种语法相关联,该语法规定了作为属性值存储的数据的格式。例如,移动属性类型有一个与之关联的电话号码语法。这将强制属性保存一个长度在 1 到 32 之间的字符串值。此外,该语法还定义了搜索操作期间属性值的行为。例如,givenName 属性的语法是 DirectoryString。此语法强制要求仅允许字母数字字符作为值。表 1-2 列出了一些常见的属性及其相关的语法描述。
表 1-2 。常见条目属性
| 属性类型 | 句法 | 描述 |
|---|---|---|
| 通用名称 | 目录字符串 | 存储一个人的常用名。 |
| 电话号码 | 电话号码 | 存储此人的主要电话号码。 |
| JPEG 图片 | 二进制的 | 存储人的一个或多个图像。 |
| 姓 | 目录字符串 | 存储人员的姓氏。 |
| 员工编号 | 目录字符串 | 在组织中存储员工的标识号。 |
| 给定名称 | 目录字符串 | 存储用户的名字。 |
| 邮件 | IA5 字符串 | 存储个人的 SMTP 邮件地址。 |
| 可动的 | 电话号码 | 存储个人的手机号码。 |
| 通讯地址 | 通讯地址 | 存储用户的位置。 |
| 邮政编码 | 目录字符串 | 存储用户的邮政编码。 |
| 标准时间 | 目录字符串 | 存储州或省的名称。 |
| 用户界面设计(User Interface Design 的缩写) | 目录字符串 | 存储用户 id。 |
| 街道 | 目录字符串 | 存储街道地址。 |
对象类
在 Java 等面向对象的语言中,我们创建一个类,并用它作为创建对象的蓝图。该类定义了这些实例可以拥有的属性/数据(以及行为/方法)。以类似的方式,LDAP 中的对象类决定了 LDAP 条目可以具有的属性。这些对象类还定义了这些属性中哪些是强制的,哪些是可选的。每个 LDAP 条目都有一个名为 objectClass 的特殊属性,用于保存它所属的对象类。查看表 1-1 中的雇员条目中的 objectClass 值,我们可以得出结论,该条目属于 inetOrgPerson 类。表 1-3 显示了标准 LDAP person 对象类中的必需和可选属性。cn 属性保存人的常用名,而 sn 属性保存人的姓。
表 1-3 。人对象类
| 必需的属性 | 可选属性 |
|---|---|
| 锡 | 描述 |
| 电话号码 | |
| 通信网络(Communicating Net 的缩写) | 用户口令 |
| 对象类 | 那就去吧 |
和 Java 一样,一个对象类可以扩展其他对象类。这种继承将允许子对象类继承父类属性。例如,person 对象类定义了常用名和姓氏等属性。对象类 inetOrgPerson 扩展了 Person 类,因此继承了 person 的所有属性。此外,inetOrgPerson 定义了在组织中工作的人员所需的属性,例如 departmentNumber 和 employeeNumber。一个特殊的对象类即 top 没有任何父类。所有其他对象类都是 top 的后代,并继承它所声明的所有属性。顶级对象类包括强制的 object class 属性。图 1-2 显示了对象继承。
图 1-2 。LDAP 对象继承
大多数 LDAP 实现都带有一组标准的对象类,可以开箱即用。表 1-4 列出了一些 LDAP 对象类及其常用属性。
表 1-4 。常见 LDAP 对象类
| 对象类别 | 属性 | 描述 |
|---|---|---|
| 顶端 | 对象类 | 定义根对象类。所有其他对象类都必须扩展这个类。 |
| 组织 | o | 代表公司或组织。o 属性通常保存组织的名称。 |
| 对象类型 | 或者说 | 代表组织内部的部门或类似实体。 |
| 人 | 序列号 | |
| cn | ||
| 电话号码 | ||
| 用户密码 | 表示目录中的一个人,需要 sn(姓氏)和 cn(常用名)属性。 | |
| 组织人员 | 寄存器地址邮政地址邮政编码 | 子类 person,代表组织中的一个人。 |
| inetOrgPerson | uid 部门编号员工编号给定名称管理器 | 提供附加属性,可用于表示在当今基于 Internet 和 intranet 的组织中工作的人。uid 属性保存用户的用户名或用户 id。 |
目录模式
LDAP 目录模式是一组确定存储在目录中的信息类型的规则。模式可以被视为打包单元,包含属性类型定义和对象类定义。在将条目存储在 LDAP 中之前,会验证模式规则。这种模式检查确保条目具有所有必需的属性,并且不包含任何不属于模式的属性。图 1-3 表示一个通用的 LDAP 模式。
图 1-3 。LDAP 通用模式
像数据库一样,目录模式需要很好地设计,以解决数据冗余等问题。在开始实现您自己的模式之前,有必要看一下几个公开可用的标准模式。这些标准模式通常包含所有的定义来存储所需的数据,更重要的是,确保跨其他目录的互操作性。
命名模型
LDAP 命名模型定义了目录中条目的组织方式。它还决定了如何唯一地标识特定条目。命名模型建议条目以分层的方式进行逻辑存储。这种条目树通常被称为目录信息树(DIT)。图 1-4 提供了一个 通用目录树的例子。
图 1-4 。一般曰
树的根通常被称为目录的基或后缀。此条目表示拥有该目录的组织。后缀的格式可以因实施而异,但一般来说,有三种推荐的方法,如图 1-5 中所列。
图 1-5 。目录后缀 命名约定
注 DC 代表域组件。
第一个推荐的技术是使用组织的 do- main 名称作为后缀。例如,如果组织的域名是example.com,目录的后缀将是 o=example。com。第二种技术也使用域名,但是名称的每个组成部分都加上“dc=”前缀,并用逗号连接。因此,域名example.com会产生一个后缀 dc=example,dc=com。这项技术是在 RFC 2247 中提出的,在 Microsoft Active Directory 中很流行。第三种技术使用 X.500 模型,并以 o =组织名称,c =国家代码的格式创建后缀。在美国,组织示例的后缀是 o=example,c=us。
命名模型还定义了如何唯一地命名和标识目录中的条目。共享一个共同直接父项的条目通过其相对可分辨名称(RDN) 进行唯一标识。使用条目的一个或多个属性/值对来计算 RDN。在最简单的情况下,RDN 通常是属性名=属性值的形式。图 1-6 提供了一个组织目录的简化表示。ou=employees 下的每个人员条目都有一个唯一的 uid。因此,第一个 person 条目的 RDN 应该是 uid=emp1,其中 emp1 是雇员的用户 id。
图 1-6 。组织目录的例子
注意识别名不是条目中的实际属性。它只是一个与条目相关联的逻辑名称。
重要的是要记住,RDN 不能用来唯一地标识整个树中的条目。然而,这可以通过组合从树的顶部到条目的路径中所有条目的 rdn 来容易地完成。这种组合的结果称为可分辨名称(DN)。在图 1-6 中,人员 1 的 DN 应该是 uid=emp1,ou=employees,dc=example,dc=com。因为 DN 是由 RDN 组合而成的,所以如果一个条目的 RDN 发生变化,该条目及其所有子条目的 DN 也会发生变化。
可能会出现一组条目没有单一唯一属性的情况。在这些场景中,一种选择是组合多个属性来创建唯一性。例如,在前面的目录中,我们可以使用消费者的常用名和电子邮件地址作为 RDN。多值 rdn 通过用+分隔每个属性对来表示,如下所示:
cn = Balaji Varanasi + mail=balaji@inflinx.com
注意通常不鼓励多值 rdn。在这些情况下,建议创建唯一的序列属性以确保唯一性。
功能模型
LDAP 功能模型描述了可以使用 LDAP 协议在目录上执行的访问和修改操作。这些操作分为三类:查询、更新和验证。
查询操作用于从目录中搜索和检索信息。因此,每次需要读取一些信息时,都需要针对 LDAP 构建和执行一个搜索查询。搜索操作以 DIT 中的一个起点、搜索的深度以及条目必须具有的匹配属性为起点。在第六章中,你将深入搜索并查看所有可用选项。
更新操作添加、修改、删除和重命名目录条目。顾名思义,add 操作向目录中添加一个新条目。该操作需要创建条目的 DN 和一组构成条目的属性。删除操作获取条目的全限定 DN,并将其从目录中删除。LDAP 协议只允许删除叶条目。修改操作更新现有条目。该操作接受条目的 DN 和一组修改,例如添加新属性、更新新属性或删除现有属性。重命名操作可用于重命名或移动目录中的条目。
身份验证操作用于连接和结束客户端和 LDAP 服务器之间的会话。绑定操作启动客户端和 LDAP 服务器之间的 LDAP 会话。通常,这将导致匿名会话。客户端可以提供一个 DN 和一组凭证来对自身进行身份验证,并创建一个经过身份验证的会话。另一方面,解除绑定操作可用于终止现有会话并断开与服务器的连接。
LDAP V3 引入了一个框架,用于扩展现有操作和添加新操作,而无需更改协议本身。你会在第七章中看到这些操作。
安全模式
LDAP 安全模型侧重于保护 LDAP 目录信息免受未经授权的访问。该模型指定了哪些客户端可以访问目录的哪些部分,以及允许哪些类型的操作(搜索还是更新)。
LDAP 安全模型基于客户端向服务器验证自身。如上所述的这个认证过程或绑定操作涉及客户端提供标识其自身的 DN 和密码。如果客户端不提供 DN 和密码,则会建立一个匿名会话。RFC 2829(www.ietf.org/rfc/rfc2829…)定义了 LDAP V3 服务器必须支持的一组认证方法。身份验证成功后,访问控制模型将被查询,以确定客户端是否有足够的权限执行所请求的操作。不幸的是,在访问控制模型方面不存在标准,每个供应商都提供自己的实现。
LDAP 供应商
LDAP 获得了各种供应商的广泛支持。还有一个强大的开源运动来生产 LDAP 服务器。表 1-5 列出了一些流行的目录服务器。
表 1-5 。LDAP 供应商
ApacheDS 和 OpenDJ 是 LDAP 目录的纯 Java 实现。在本书中,您将使用这两台服务器对代码进行单元和集成测试。
LDIF 格式
LDAP 数据交换格式(LDIF) 是一种基于文本的标准格式,用于表示目录内容和更新请求。RFC 2849(www.ietf.org/rfc/rfc2849…)中定义了 LDIF 格式。LDIF 文件通常用于从一个目录服务器导出数据,并将其导入另一个目录服务器。它也常用于归档目录数据和对目录进行批量更新。您将使用 LDIF 文件来存储您的测试数据,并在单元测试之间刷新目录服务器。
用 LDIF 表示的条目的基本格式如下:
#comment
dn: <distinguished name>
objectClass: <object class>
objectClass: <object class>
...
...
<attribute type>: <attribute value>
<attribute type>: <attribute value>
...
LDIF 文件中以#字符开头的行被视为注释。条目的 dn 和至少一个对象类定义被认为是必需的。属性表示为用冒号分隔的名称/值对。在单独的行中指定了多个属性值,这些属性值将具有相同的属性类型。由于 LDIF 文件完全基于文本,二进制数据在存储为 LDIF 文件的一部分之前需要进行 Base64 编码。
同一 LDIF 文件中的多个条目由空行分隔。清单 1-1 显示了一个有三个雇员条目的 LDIF 文件。请注意,cn 属性是一个多值属性,并且为每个雇员表示两次。
清单 1-1 。有三个员工条目的 LDIF 文件
# Barbara’s Entry
dn: cn=Barbara J Jensen, dc=example, dc=com
# multi valued attribute
cn: Barbara J Jensen
cn: Babs Jensen
objectClass: person sn: Jensen
# Bjorn’s Entry
dn: cn=Bjorn J Jensen, dc=example, dc=com
cn: Bjorn J Jensen
cn: Bjorn Jensen
objectClass: person
sn: Jensen
# Base64 encoded JPEG photo
jpegPhoto:: /9j/4AAQSkZJRgABAAAAAQABAAD/2wBDABALD A4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQ ERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVG
# Jennifer’s Entry
dn: cn=Jennifer J Jensen, dc=example, dc=com
cn: Jennifer J Jensen
cn: Jennifer Jensen
objectClass: person
sn: Jensen
样本应用
在本书中,你将使用一个假想图书馆的目录。我选择了图书馆,因为这个概念是通用的,容易掌握。图书馆通常储存书籍和其他多媒体资料,供顾客借阅。图书馆还雇用人员负责图书馆的日常运作。为了便于管理,这个目录不会存储关于书籍的信息。关系数据库可能适合记录书籍信息。图 1-7 显示了我们的库应用的 LDAP 目录树。
图 1-7 。库曰
在这个目录树中,我使用了 RFC 2247(www.ietf.org/rfc/rfc2247…)约定来命名基本条目。基本条目有两个保存雇员和顾客信息的组织单位条目。树的 ou=employees 部分将保存所有的库员工条目。树的 ou =顾客部分将保存图书馆顾客条目。图书馆雇员和顾客条目都属于 inetOrgPerson 对象类类型。员工和顾客都使用他们唯一的登录 id 访问图书馆应用。因此 uid 属性将被用作条目的 RDN。
摘要
LDAP 和与 LDAP 交互的应用已经成为当今每个企业的重要组成部分。本章讲述了 LDAP 目录的基础知识。您了解了 LDAP 将信息存储为条目。每个条目都由简单的键值对属性组成。这些条目可以通过它们的识别名来访问。您还看到了 LDAP 目录具有决定可以存储的信息类型的模式。
在下一章中,您将看到使用 JNDI 与 LDAP 目录通信。在第二章之后的章节中,您将重点关注使用 Spring LDAP 开发 LDAP 应用。
二、Java 对 LDAP 的支持
在本章中,我们将讨论:
- JNDI 基础知识
- 使用 JNDI 的 LDAP 启用应用
- JNDI 的缺点
Java 命名和目录接口(JNDI) 顾名思义,提供了访问命名和目录服务的标准化编程接口。它是一个通用 API,可用于访问各种系统,包括文件系统、EJB、CORBA 和目录服务,如网络信息服务和 LDAP。JNDI 对目录服务的抽象可以被视为类似于 JDBC 对关系数据库的抽象。
JNDI 架构由应用编程接口(API)和服务提供商接口(SPI)组成。开发人员使用 JNDI API 对他们的 Java 应用进行编程,以访问目录/命名服务。供应商实现 SPI 的细节是处理与他们特定服务/产品的实际通信。这种实现被称为服务提供商。图 2-1 显示了 JNDI 架构以及一些命名和目录服务提供商。这种可插拔架构提供了一致的编程模型,避免了为每个产品学习单独的 API 的需要。
图 2-1 。JNDI 建筑
自 Java 版本 1.3 以来,JNDI 一直是标准 JDK 发行版的一部分。API 本身分布在以下四个包中:
- javax.naming 包包含用于在命名服务中查找和访问对象的类和接口。
- javax.naming.directory 包包含扩展核心 javax.naming 包的类和接口。这些类可用于访问目录服务和执行高级操作,如过滤搜索。
- 在访问命名和目录服务时,javax.naming.event 包具有事件通知功能。
- javax.naming.ldap 包包含支持 ldap 版本 3 控件和操作的类和接口。我们将在后面的章节中探讨控制和操作。
javax.naming.spi 包包含 spi 接口和类。就像我上面提到的,服务提供商实现 SPI,我们不会在本书中讨论这些类。
使用 JNDI 的 LDAP
虽然 JNDI 允许访问目录服务,但重要的是要记住,JNDI 本身不是一个目录或命名服务。因此,为了使用 JNDI 访问 LDAP,我们需要一个正在运行的 LDAP 目录服务器。如果您没有可用的测试 LDAP 服务器,请参考第三章中的步骤安装本地 LDAP 服务器。
使用 JNDI 访问 LDAP 通常包括以下三个步骤:
- 连接到 LDAP
- 执行 LDAP 操作
- 关闭资源
连接到 LDAP
使用 JNDI 的所有命名和目录操作都是相对于上下文执行的。因此,使用 JNDI 的第一步是创建一个上下文,作为 LDAP 服务器的起点。这样的上下文被称为初始上下文。一旦建立了初始上下文,就可以使用它来查找其他上下文或添加新对象。
javax . naming 包中的 Context 接口和 InitialContext 类可用于创建初始命名上下文。由于我们在这里处理的是一个目录,我们将使用一个更具体的 DirContext 接口及其实现 InitialDirContext。DirContext 和 InitialDirContext 都可以在 javax.naming.directory 包中找到。目录上下文实例可以用一组提供 LDAP 服务器信息的属性进行配置。清单 2-1 中的代码为运行在本地端口 11389 上的 LDAP 服务器创建了一个上下文。
清单 2-1。
Properties environment = new Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:11389");
DirContext context = new InitialDirContext(environment);
在上面的代码中,我们使用了 INITIAL_CONTEXT_FACTORY 常量来指定需要使用的服务提供者类。这里我们使用的是 sun 提供程序 com . sun . JNDI . LDAP . ldapctxfactory,它是标准 JDK 发行版的一部分。PROVIDER_URL 用于指定 LDAP 服务器的全限定 URL。URL 包括协议(非安全的 ldap 或安全连接的 ldaps)、LDAP 服务器主机名和端口。
一旦建立了与 LDAP 服务器的连接,应用就可以通过提供身份验证信息来识别自己。类似于在清单 2-1 中创建的上下文,其中没有提供认证信息,被称为匿名上下文。LDAP 服务器通常有 ACL(访问列表控制),将操作和信息限制在某些帐户。因此,在企业应用中创建和使用经过身份验证的上下文是非常常见的。清单 2-2 提供了一个创建认证上下文的例子。请注意,我们使用了三个附加属性来提供绑定凭证。SECURITY_AUTHENTICATION 属性设置为 simple,表示我们将使用纯文本用户名和密码进行身份验证。
清单 2-2 。
Properties environment = new Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:11389");
environment.setProperty(DirContext.SECURITY_AUTHENTICATION, "simple");
environment.setProperty(DirContext.SECURITY_PRINCIPAL, "uid=admin,ou=system");
environment.setProperty(DirContext.SECURITY_CREDENTIALS, "secret");
DirContext context = new InitialDirContext(environment);
在创建上下文期间可能发生的任何问题都将被报告为 javax.naming.NamingException 的实例。NamingException 是 JNDI API 抛出的所有异常的超类。这是一个已检查的异常,必须正确处理才能编译代码。表 2-1 列出了我们在 JNDI 开发过程中可能遇到的常见异常情况。
表 2-1 。常见的 LDAP 例外
| 例外 | 描述 |
|---|---|
| AttributeInUseException | 当操作试图添加现有属性时引发。 |
| 属性修改异常 | 当操作试图添加/移除/更新属性并违反属性的架构或状态时引发。例如,向单值属性添加两个值会导致此异常。 |
| 沟通例外 | 当应用无法与 LDAP 服务器通信(例如网络问题)时抛出。 |
| InvalidAttributesException | 当操作试图添加或修改指定不完整或不正确的属性集时抛出。例如,试图在没有指定所有必需属性的情况下添加新条目会导致此异常。 |
| limitexceedededexception | 当搜索操作因达到用户或系统指定的结果限制而突然终止时引发。 |
| InvalidSearchFilterException | 当搜索操作被赋予格式错误的搜索筛选器时引发。 |
| NameAlreadyBoundException | 抛出以指示不能添加条目,因为关联的名称已经绑定到不同的对象。 |
| PartialResultException | 抛出表示只返回了预期结果的一部分,操作无法完成。 |
LDAP 操作
一旦我们获得了初始上下文,我们就可以使用该上下文在 LDAP 上执行各种操作。这些操作可能涉及查找另一个上下文、创建新上下文以及更新或删除现有上下文。下面是一个使用 DN uid=emp1,ou=employees,dc=inflinx,d c=com 查找另一个上下文的示例。
DirContext anotherContext = context.lookup("uid=emp1,ou=employees,
dc=inflinx,dc=com");
在下一节中,我们将仔细研究这些操作。
关闭资源
在所有期望的 LDAP 操作完成之后,正确关闭上下文和任何其他相关资源是很重要的。关闭 JNDI 资源只需要调用它的 close 方法。清单 2-3 显示了与关闭 DirContext 相关的代码。从代码中可以看出,close 方法也抛出了一个需要正确处理的 NamingException。
清单 2-3。
try {
context.close();
}
catch (NamingException e) {
e.printstacktrace();
}
创建新条目
考虑这样一种情况,一个新员工从我们假设的库开始,我们被要求将他的信息添加到 LDAP 中。正如我们前面看到的,在将条目添加到 LDAP 之前,有必要获取 InitialDirContext。清单 2-4 为此定义了一个可重用的方法。
清单 2-4。
private DirContext getContext() throws NamingException{
Properties environment = new Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.
LdapCtxFactory");
environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:10389");
environment.setProperty(DirContext.SECURITY_PRINCIPAL, "uid=admin,ou=system");
environment.setProperty(DirContext.SECURITY_CREDENTIALS, "secret");
DirContext context = new InitialDirContext(environment);
return context;
}
一旦我们有了初始的上下文,添加新的雇员信息就是一个简单的操作,如清单 2-5 所示。
清单 2-5。
public void addEmploye(Employee employee) {
DirContext context = null;
try {
context = getContext();
// Populate the attributes
Attributes attributes = new BasicAttributes();
attributes.put(new BasicAttribute("objectClass", "inetOrgPerson"));
attributes.put(new BasicAttribute("uid", employee.getUid()));
attributes.put(new BasicAttribute("givenName", employee.getFirstName()));
attributes.put(new BasicAttribute("surname", employee.getLastName()));
attributes.put(new BasicAttribute("commonName", employee.getCommonName()));
attributes.put(new BasicAttribute("departmentNumber",
employee.getDepartmentNumber()));
attributes.put(new BasicAttribute("mail", employee.getEmail()));
attributes.put(new BasicAttribute("employeeNumber",
employee.getEmployeeNumber()));
Attribute phoneAttribute = new BasicAttribute("telephoneNumber");
for(String phone : employee.getPhone()) {
phoneAttribute.add(phone);
}
attributes.put(phoneAttribute);
// Get the fully qualified DN
String dn = "uid="+employee.getUid() + "," + BASE_PATH;
// Add the entry
context.createSubcontext("dn", attributes);
}
catch(NamingException e) {
// Handle the exception properly
e.printStackTrace();
}
finally {
closeContext(context);
}
}
如您所见,该过程的第一步是创建一组需要添加到条目中的属性。JNDI 提供了 javax . naming . directory . attributes 接口及其实现 javax . naming . directory . basic attributes 来抽象属性集合。然后,我们使用 JNDI 的 javax . naming . directory . basic attribute 类将雇员的属性一次一个地添加到集合中。注意,我们在创建 BasicAttribute 类时采用了两种方法。在第一种方法中,我们通过将属性名和值传递给 BasicAttribute 的构造函数来添加单值属性。为了处理多值属性 telephone,我们首先通过传入名称来创建 BasicAttribute 实例。然后,我们分别将电话值添加到属性中。添加完所有属性后,我们在初始上下文中调用 createSubcontext 方法来添加条目。createSubcontext 方法要求添加条目的完全限定 DN。
注意,我们已经将上下文的关闭委托给了一个单独的方法 closeContext。清单 2-6 展示了它的实现。
清单 2-6。
private void closeContext(DirContext context) {
try {
if(null != context) {
context.close();
}
}
catch(NamingException e) {
// Ignore the exception
}
}
更新条目
修改现有 LDAP 条目可能涉及以下任何操作:
- 添加新的属性和值,或者向现有的多值属性添加新值。
- 替换现有属性值。
- 删除属性及其值。
为了允许修改条目,JNDI 提供了一个恰当命名的 javax . naming . directory . modification item 类。
ModificationItem 由要进行的修改的类型和正在修改的属性组成。下面的代码创建了一个用于添加新电话号码的修改项。
Attribute telephoneAttribute = new BasicAttribute("telephone", "80181001000");
ModificationItem modificationItem = new ModificationItem(DirContext.
ADD_ATTRIBUTE, telephoneAttribute);
注意,在上面的代码中,我们使用了常量 ADD_ATTRIBUTE 来表示我们需要一个 ADD 操作。表 2-2 提供了支持的修改类型及其描述。
表 2-2 。LDAP 修改类型
| 修改类型 | 描述 |
|---|---|
| 添加属性 | 将具有提供的一个或多个值的属性添加到条目中。如果该属性不存在,则将创建它。如果属性已经存在,并且属性是多值的,那么该操作只是将指定的值添加到现有列表中。但是,对现有单值属性的此操作将导致 AttributeInUseException。 |
| 替换属性 | 用提供的值替换条目的现有属性值。如果该属性不存在,则将创建它。如果属性已经存在,那么它的所有值都将被替换。 |
| 移除属性 | 从现有属性中移除指定的值。如果没有指定任何值,则整个属性将被删除。如果属性中不存在指定的值,操作将引发 NamingException。如果要删除的值是该属性的唯一值,则该属性也将被删除。 |
更新条目的代码在清单 2-7 中提供。modifyAttributes 方法接受要修改的条目的全限定 DN 和一个修改项数组。
清单 2-7。
public void update(String dn, ModificationItem[] items) {
DirContext context = null;
try {
context = getContext();
context.modifyAttributes(dn, items);
}
catch (NamingException e) {
e.printStackTrace();
}
finally {
closeContext(context);
}
}
删除条目
使用 JNDI 删除条目也是一个简单的过程,如清单 2-8 所示。destroySubcontext 方法获取需要删除的条目的全限定 DN。
清单 2-8。
public void remove(String dn) {
DirContext context = null;
try {
context = getContext();
context.destroySubcontext(dn);
}
catch(NamingException e) {
e.printStackTrace();
finally {
closeContext(context);
}
}
许多 LDAP 服务器不允许删除包含子条目的条目。在这些服务器中,删除非叶条目需要遍历子树并删除所有子条目。那么可以删除非叶条目。清单 2-9 显示了删除一个子树的代码。
清单 2-9。
public void removeSubTree(DirContext ctx, String root)
throws NamingException {
NamingEnumeration enumeration = null;
try {
enumeration = ctx.listBindings(root);
while (enumeration.hasMore()) {
Binding childEntry =(Binding)enumeration.next();
LdapName childName = new LdapName(root);
childName.add(childEntry.getName());
try {
ctx.destroySubcontext(childName);
}
catch (ContextNotEmptyException e) {
removeSubTree(ctx, childName.toString());
ctx.destroySubcontext(childName);
}
}
}
catch (NamingException e) {
e.printStackTrace();
}
finally {
try {
enumeration.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
注意OpenDJ LDAP 服务器支持特殊的子树删除控件,当附加到删除请求时,可以使服务器删除非叶条目及其所有子条目。我们将在第七章的中了解 LDAP 控件的使用。
搜索条目
搜索信息通常是针对 LDAP 服务器执行的最常见的操作。为了执行搜索,我们需要提供诸如搜索范围、我们在寻找什么以及需要返回什么属性之类的信息。在 JNDI,这种搜索元数据是使用 SearchControls 类提供的。清单 2-10 提供了一个带有子树范围的搜索控件的例子,并返回 givenName 和 telephoneNumber 属性。子树范围表示搜索应该从给定的基本条目开始,并且应该搜索它的所有子树条目。我们将在第六章的中详细了解不同的可用范围。
清单 2-10。
SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchControls.setReturningAttributes(new String[]{"givenName",
"telephoneNumber"});
一旦我们定义了搜索控件,下一步就是调用 DirContext 实例中的许多搜索方法之一。清单 2-11 提供了搜索所有雇员并打印他们的名字和电话号码的代码。
清单 2-11。
public void search() {
DirContext context = null;
NamingEnumeration<SearchResult> searchResults = null;
try
{
context = getContext();
// Setup Search meta data
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchControls.setReturningAttributes(new String[]
{"givenName", "telephoneNumber"});
searchResults = context.search("dc=inflinx,dc=com",
"(objectClass=inetOrgPerson)", searchControls);
while (searchResults.hasMore()) {
SearchResult result = searchResults.next();
Attributes attributes = result.getAttributes();
String firstName = (String)attributes.get("givenName").get();
// Read the multi-valued attribute
Attribute phoneAttribute = attributes. get("telephoneNumber");
String[] phone = new String[phoneAttribute.size()];
NamingEnumeration phoneValues = phoneAttribute.getAll();
for(int i = 0; phoneValues.hasMore(); i++) {
phone[i] = (String)phoneValues.next();
}
System.out.println(firstName + "> " + Arrays.toString(phone));
}
}
catch(NamingException e) {
e.printStackTrace();
}
finally {
try {
if (null != searchResults) {
searchResults.close();
}
closeContext(context);
} catch (NamingException e) {
// Ignore this
}
}
}
这里我们使用了带有三个参数的搜索方法:一个确定搜索起点的基数、一个缩小结果范围的过滤器和一个搜索控件。search 方法返回 SearchResults 的枚举。每个搜索结果都包含 LDAP 条目的属性。因此,我们遍历搜索结果并读取属性值。注意,对于多值属性,我们获得另一个枚举实例,并一次读取一个值。在代码的最后部分,我们关闭了结果枚举和上下文资源。
JNDI 的弊端
尽管 JNDI 为访问目录服务提供了一个很好的抽象,但它确实有以下几个缺点:
- 显式资源管理
- 开发人员负责关闭所有资源。这很容易出错,并可能导致内存泄漏。
- 管道规范
- 我们上面看到的方法有很多可以很容易抽象和重用的管道代码。这种管道代码使得测试更加困难,开发人员必须了解 API 的本质。
- 检查异常
- 使用检查异常尤其是在不可恢复的情况下是有问题的。在这些场景中,必须显式处理 NamingException 通常会导致空的 try catch 块。
三、Spring LDAP 简介
在本章中,我们将讨论
- Spring LDAP 的基础知识。
- 下载和设置 Spring LDAP。
- 设置 STS 开发环境。
- 设置测试 LDAP 服务器。
- 创建 Hello World 应用。
Spring LDAP 为 Java 中的 LDAP 编程提供了简单、干净和全面的支持。这个项目最初于 2006 年在 Sourceforge 上以 LdapTemplate 的名字启动,目的是使用 JNDI 简化对 LDAP 的访问。该项目后来成为 Spring Framework 组合的一部分,并且已经走过了漫长的道路。图 3-1 描述了一个基于 Spring LDAP 的应用的架构。
图 3-1 。Spring LDAP 架构目录
应用代码使用 Spring LDAP API 在 LDAP 服务器上执行操作。Spring LDAP 框架包含所有特定于 LDAP 的代码和抽象。然而,Spring LDAP 将依赖 Spring 框架来满足它的一些基础设施需求。
Spring 框架已经成为今天开发基于 Java 的企业应用的事实上的标准。除了其他方面,它还为 JEE 编程模型提供了一个基于依赖注入的轻量级替代方案。Spring 框架是 Spring LDAP 和所有其他 Spring 组合项目(如 Spring MVC 和 Spring Security)的基础。
动机
在前一章中,我们讨论了 JNDI API 的缺点。JNDI 的一个显著缺点是它非常冗长;第二章中几乎所有的代码都与管道有关,很少与应用逻辑有关。Spring LDAP 通过提供负责管道代码的模板和实用程序类来解决这个问题,这样开发人员就可以专注于业务逻辑。
JNDI 的另一个值得注意的问题是,它要求开发人员显式地管理 LDAP 上下文等资源。这很容易出错。忘记关闭资源可能会导致泄漏,并可能在高负载下迅速关闭应用。Spring LDAP 代表您管理这些资源,并在您不再需要它们时自动关闭它们。它还提供了池化 LDAP 上下文的能力,这可以提高性能。
在执行 JNDI 操作期间可能出现的任何问题都将被报告为 NamingException 或其子类的实例。NamingException 是一个检查过的异常,因此开发人员必须处理它。数据访问异常通常是不可恢复的,而且大多数情况下,我们无法捕捉这些异常。为了解决这个问题,Spring LDAP 提供了一个一致的未检查异常层次结构,模拟 NamingException。这允许应用设计人员选择何时何地处理这些异常。
最后,简单的 JNDI 编程对新开发人员来说很难,可能会令人望而生畏。Spring LDAP 及其抽象使得使用 JNDI 更加愉快。此外,它还提供了各种特性,如对象目录映射和对事务的支持,这使它成为任何企业 LDAP 开发人员的重要工具。
获取 Spring LDAP
在安装和开始使用 Spring LDAP 之前,确保 Java 开发工具包(JDK)已经安装在您的机器上是很重要的。最新的 Spring LDAP 1.3.2 版本需要 JDK 1.4 或更高版本以及 Spring 2.0 或更高版本。由于我在书中的例子中使用的是 Spring 3.2.4,所以强烈建议安装 JDK 6.0 或更高版本。
Spring 框架及其组合项目可以从www.springsource.org/download/co…下载。在 www.springsource.org/ldap[的 Spring LDAP 网站上有一个直接链接。Spring LDAP 下载页面](www.springsource.org/ldap)允许您下载框… 3-2 所示。
图 3-2 。春季 LDAP 下载
spring-LDAP-1 . 3 . 2 . release-dist . zip 包括框架二进制文件、源代码和文档。因为最新的 LDAP 分发包不包含 Spring 分发包,所以您需要单独下载 Spring Framework。图 3-3 显示了最新可用的 Spring 框架发行版,3.2.4.RELEASE .下载 Spring LDAP 和 Spring 发行版,如图图 3-3 所示,并在你的机器上解压。
图 3-3 。Spring 框架下载
春季 LDAP 打包
现在您已经成功下载了 Spring LDAP 框架,让我们深入研究它的子文件夹。libs 文件夹包含 Spring LDAP 二进制文件、源代码和 javadoc 发行版。LDAP 框架被打包成六个不同的组件。表 3-1 提供了每个组件的简要描述。docs 文件夹包含 API 的 javadoc 和不同格式的参考指南。
表 3-1 。Spring LDAP 分发模块
| 成分罐 | 描述 |
|---|---|
| spring-LDAP-核心 | 包含使用 LDAP 框架所需的所有类。所有应用都需要这个 jar。 |
| spring-LDAP-核心-tiger | 包含特定于 Java 5 和更高版本的类和扩展。在 Java 5 下运行的应用不应该使用这个 jar。 |
| spring LDAP 测试 | 包含使测试更容易的类和实用程序。它还包括启动和停止 ApacheDS LDAP 服务器的内存实例的类。 |
| spring-ldap-ldif-core | 包含用于分析 ldif 格式文件的类。 |
| spring-ldap-ldif-batch | 包含将 ldif 解析器与 Spring Batch Framework 集成所需的类。 |
| spring-ldap-odm | 包含用于启用和创建对象目录映射的类。 |
除了 Spring Framework,您还需要额外的 jar 文件来使用 Spring LDAP 编译和运行应用。表 3-2 列出了一些相关的 jars 文件以及为什么使用它们的描述。
表 3-2 。Spring LDAP 依赖 jar
| 图书馆罐子 | 描述 |
|---|---|
| 康芒斯-朗 | Spring LDAP 和 Spring Framework 内部使用的必需 jar。 |
| 公共日志记录 | Spring LDAP 和 Spring Framework 内部使用的日志抽象。这是应用中必须包含的 jar。另一种选择(也是 Spring 提倡的)是通过 SLF4J-JCL 桥使用 SLF4J 日志框架。 |
| log4j | 使用 Log4J 进行日志记录所需的库。 |
| 弹簧芯 | 包含 Spring LDAP 内部使用的核心实用程序的 Spring 库。这是使用 Spring LDAP 所必需的库。 |
| 春豆 | 用于创建和管理 Spring beans 的 Spring 框架库。Spring LDAP 需要的另一个库。 |
| 春天的背景 | 负责依赖注入的 Spring 库。当在 Spring 应用中使用 Spring LDAP 时,这是必需的。 |
| 春天-tx | 提供事务抽象的 Spring 框架库。当使用 Spring LDAP 事务支持时,这是必需的。 |
| spring-jdbc | 使用 JDBC 简化数据库访问的库。这是一个可选的库,应该用于事务支持。 |
| 公共游泳池 | Apache Commons 池库提供了对池的支持。当使用 Spring LDAP 池支持时,应该包括这一点。 |
| ldapbp | 包含附加 LDAP V3 服务器控件的 Sun LDAP 增强包。当您计划使用这些附加控件或者在 Java 5 或更低版本下运行时,这个 jar 是必需的。 |
下载 Spring LDAP 源代码
Spring LDAP 项目使用 Git 作为他们的源代码控制系统。源代码可以从github.com/SpringSource/spring-ldap下载。
Spring LDAP 源代码可以为框架架构提供有价值的见解。它还包括一个丰富的测试套件,可以作为额外的文档,帮助您理解框架。我强烈建议您下载并查看源代码。Git 存储库还包含一个沙箱文件夹,其中包含几个实验性的特性,这些特性可能会也可能不会出现在框架中。
使用 Maven 安装 Spring LDAP
Apache Maven 是一个开源的、基于标准的项目管理框架,它使得项目的构建、测试、报告和打包变得更加容易。如果你是 Maven 新手,对这个工具有疑问,Maven 网站,【maven.apache.org】的[提供了关于它的特性的信息以及大量有用的链接。以下是采用 Maven 的一些优势](maven.apache.org):
- 标准化的目录结构 : Maven 标准化了一个项目的布局和组织。每当一个新项目开始时,都要花费大量的时间来决定源代码应该放在哪里或者配置文件应该放在哪里。此外,这些决策在项目和团队之间会有很大的不同。Maven 的标准化目录结构使得开发人员甚至 ide 都很容易采用。
- 声明依赖关系管理:使用 Maven,您可以在一个单独的 pom.xml 文件中声明项目依赖关系。然后 Maven 自动从存储库中下载这些依赖项,并在构建过程中使用它们。Maven 还智能解析并下载传递依赖(依赖的依赖)。
- 原型 : Maven 原型是项目模板,可以用来轻松地生成新项目。这些原型是共享最佳实践和加强 Maven 标准目录结构之外的一致性的好方法。
- 插件 : Maven 遵循基于插件的架构,这使得添加或定制其功能变得容易。目前有数百个插件可用于执行从编译代码到创建项目文档的各种任务。激活和使用插件只需要在 pom.xml 文件中声明对插件的引用。
- 工具支持:今天所有主流的 ide 都为 Maven 提供工具支持。这包括生成项目、创建特定于 IDE 的文件的向导,以及用于分析依赖关系的图形化工具。
安装 Maven
要安装 Maven,只需从maven.apache.org/download.html下载最新版本。下载完成后,将发行版解压缩到您机器上的本地目录。然后对开发箱进行以下修改:
- 添加一个指向 maven 安装目录的 M2_HOME 环境变量。
- 添加一个值为–xmx 512m 的 MAVEN_OPTS 环境变量。
- 将 M2_HOME/bin 值添加到 Path 环境变量中。
注意 Maven 需要互联网连接来下载依赖项和插件。如果您或您的公司使用代理连接到 Internet,请更改 settings.xml 文件。否则,您可能会遇到“无法下载工件”的错误。
这就完成了 Maven 的安装。您可以通过在命令行上运行以下命令来验证安装:
$ mvn –v
该命令应该输出类似于以下内容的信息:
Apache Maven 3.1.0 (893ca28a1da9d5f51ac03827af98bb730128f9f2; 2013-06-27 20:15:32-0600)
Maven home: c:\tools\maven
Java version: 1.6.0_35, vendor: Sun Microsystems Inc.
Java home: C:\Java\jdk1.6.0_35\jre
Default locale: en_US, platform encoding: Cp1252
OS name: "windows 7", version: "6.1", arch: "x86", family: "windows"
Spring LDAP 原型
为了快速启动 Spring LDAP 开发,本书使用了以下两个原型:
- practical-ldap-empty-archetype:这个原型可以用来创建一个空的 Java 项目,包含所有必需的 LDAP 依赖项。
- practical-ldap-architect:与上面的原型类似,这个原型创建了一个 Java 项目,其中包含所有必需的 LDAP 依赖项。此外,它还包括 Spring LDAP 配置文件、示例代码和运行内存中 LDAP 服务器进行测试的依赖项。
在使用原型创建项目之前,您需要安装它们。如果您还没有这样做,请从 Apress 下载附带的源文件/下载文件。在下载的发行版中,您会发现 practical-LDAP-empty-archetype-1 . 0 . 0 . jar 和 practical-LDAP-archetype-1 . 0 . 0 . jar 原型。下载完 jar 文件后,在命令行运行以下两个命令:
mvn install:install-file \
-DgroupId=com.inflinx.book.ldap \
-DartifactId=practical-ldap-empty-archetype \
-Dversion=1.0.0 \
-Dpackaging=jar
-Dfile=<JAR_LOCATION_DOWNLOAD>/practical-ldap-empty-archetype-1.0.0.jar
mvn install:install-file \
-DgroupId=com.inflinx.book.ldap \
-DartifactId=practical-ldap-archetype \
-Dversion=1.0.0 \
-Dpackaging=jar
-Dfile=< JAR_LOCATION_DOWNLOAD >/practical-ldap-archetype-1.0.0.jar
这些 maven install 命令将在您的本地 maven 存储库中安装这两个原型。使用这些原型之一创建项目只需运行以下命令:
C:\practicalldap\code>mvn archetype:generate
-DarchetypeGroupId=com.inflinx.book.ldap \
-DarchetypeArtifactId=practical-ldap-empty-archetype \
-DarchetypeVersion=1.0.0 \
-DgroupId=com.inflinx.ldap \
-DartifactId=chapter3 \
-DinteractiveMode=false
注意,这个命令是在目录 c:/practicalldap/code 中执行的。该命令指示 maven 使用原型 practical-LDAP-empty-architect 并生成一个名为 chapter3 的项目。生成的项目目录结构如图图 3-4 所示。
图 3-4 。Maven 生成的项目结构
这个目录结构有一个 src 文件夹,保存所有代码和任何相关的资源,比如 XML 文件。目标文件夹包含生成的类和构建工件。src 下的主文件夹通常保存最终进入生产的代码。测试文件夹包含相关的测试代码。这两个文件夹都包含 java 和 resources 子文件夹。顾名思义,java 文件夹包含 Java 代码,resources 文件夹通常包含配置 xml 文件。
根文件夹中的 pom.xml 文件保存了 Maven 所需的配置信息。例如,它包含编译代码所需的所有依赖 jar 文件的信息(见清单 3-1 )。
清单 3-1。
<dependencies>
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
<version>${org.springframework.ldap.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
清单 3-1 中的 pom.xml 片段表明项目在编译期间需要 spring-ldap-core.jar 文件。
Maven 需要一个组 id 和工件 id 来惟一地标识一个依赖项。一个组 id 通常对一个项目或组织是唯一的,类似于 Java 包的概念。工件 id 通常是项目的名称或者项目的一个生成的组件。范围决定了类路径中应该包含依赖项的阶段。以下是几个可能的值:
- test :测试范围表示只有在测试过程中,依赖关系才应该包含在类路径中。JUnit 就是这种依赖性的一个例子。
- provided:provided 作用域表示工件应该仅在编译期间包含在类路径中。提供的范围依赖通常在运行时通过 JDK 或应用容器可用。
- compile :编译范围表示依赖项应该一直包含在类路径中。
pom.xml 文件中的一个附加部分包含关于 Maven 可以用来编译和构建代码的插件的信息。清单 3-2 中的显示了一个这样的插件声明。它指示 Maven 使用 2.0.2 版本的编译器插件来编译 Java 代码。finalName 表示生成的工件的名称。在这种情况下,应该是 chapter3.jar。
清单 3-2。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin
</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
<finalName>chapter3</finalName>
</build>
要构建这个生成的应用,只需从命令行运行以下命令。这个命令清理目标文件夹,编译源文件,并在目标文件夹中生成一个 jar 文件。
mvn clean compile package
这个设置和文本编辑器足以开始开发和打包基于 Java 的 LDAP 应用。然而,使用图形 IDE 开发和调试应用会更有效率,这是显而易见的。有几种 ide,最流行的是 Eclipse、NetBeans 和 IntelliJ IDEA。对于这本书,您将使用 Spring Tool Suite,一个来自 Spring Source 的基于 Eclipse 的 IDE。
设置 Spring IDE
STS 是一个免费的基于 Eclipse 的开发环境,为开发基于 Spring 的应用提供了最好的工具支持。以下是的一些特点:
- 创建 Spring 项目和 Spring beans 的向导
- 对 Maven 的集成支持
- 基于项目和文件创建最佳实践的模板
- Spring bean 和 AOP 切入点可视化
- 用于快速原型制作的 Spring ROO shell 集成
- 基于任务的用户界面,通过教程提供引导式帮助
- 支持 Groovy 和 Grails
在这一节中,你将看到 STS IDE 的安装和设置。
-
Download and initiate the STS installer from the Spring Tool Suite web site at www.springsource.com/developer/s…. The installation file for Windows is spring-tool-suite-3.3.0.RELEASE-e4.3-win32-installer.exe. Double-click the install file to start the installation (Figure 3-5).
图 3-5 。安装程序主屏幕
-
阅读并接受许可协议,然后单击下一步按钮。
-
在目标路径屏幕上,选择安装目录。
-
Leave the default selection and then click the Next button (see Figure 3-6).
图 3-6 。安装包
-
在下面的屏幕上,提供 JDK 安装的路径,然后单击“下一步”按钮。
-
这将开始安装;等待文件传输完成。
-
单击下面两个屏幕上的 Next 按钮,完成安装。
使用 STS 创建项目
在前面的“Spring LDAP 原型”一节中,您使用了 practical-ldap-empty-archetype 原型从命令行生成项目。现在让我们看看如何使用 STS 生成同一个项目。
-
From the File menu, select New
Project. It will launch the New Project wizard (see Figure 3-7). Select the Maven Project option and click the Next button.
图 3-7 。新项目向导
-
Uncheck “Use default Workspace location” and enter the path for the newly generated project, and then select the Next button (see Figure 3-8).
图 3-8 。项目路径设置
-
On the Select an Archetype screen (see Figure 3-9), click “Add Archetype.” This step assumes that you have already installed the archetype as mentioned in the earlier section. Fill the Add Archetype dialog with the details shown in Figure 3-9 and press OK. Do the same for the other archetype.
图 3-9 。原型细节
-
Enter ldap in the Filter field and select the practical-ldap-empty-archetype. Click the Next button (see Figure 3-10).
图 3-10 。原型选择
-
在接下来的屏幕上,提供关于新创建项目的信息并点击完成按钮(参见图 3-11 )。
图 3-11 。项目信息
这将生成一个与您之前看到的目录结构相同的项目。但是,它也会创建所有特定于 IDE 的文件,如。项目和。并将所有依赖的 jar 添加到项目的类路径中。完整的项目结构如图 3-12 所示。
图 3-12 。生成的项目结构
LDAP 服务器设置
在这一节中,您将看到如何安装 LDAP 服务器来测试您的 LDAP 代码。在可用的开源 LDAP 服务器中,我发现 OpenDJ 非常容易安装和配置。
注意即使你已经有一个可用的测试 LDAP 服务器,我也强烈建议你按照下面的步骤安装 OpenDJ LDAP 服务器。您将大量使用这个实例来测试本书中的代码。
从www.forgerock.org/opendj-arch…下载 OpenDJ 发行版文件 OpenDJ-2.4.6.zip 。将发行版解压缩到本地系统上的一个文件夹中。在我的 Windows box 上,我将提取的文件和文件夹放在 C:\practicalldap\opendj 下。然后按照这些步骤完成安装。
-
Start the installation by clicking the setup.bat file for Windows. This will launch the install screen.
注意在 Windows 8 下安装时,一定要以管理员身份运行安装程序。否则,在将服务器作为 Windows 服务启用时,您会遇到错误。
-
On the Server settings screen, enter the following values and press the Next button. I changed the Listener Port from 389 to 11389 and Administration Connector Port from 4444 to 4445. I also used opendj as the password. Please use these settings for running code examples used in this book (see Figure 3-13).
图 3-13 。LDAP 服务器设置
-
在拓扑选项屏幕中,保留“这将是一台独立服务器”选项,并点击下一步按钮。
-
在目录数据屏幕中,输入值“dc=inflinx,dc=com”作为目录基本 DN,其他选项保持不变,然后继续。
-
在查看屏幕中,确认“将服务器作为 Windows 服务运行”选项已选中,并点击完成按钮。
-
您将看到一个确认信息,表明安装成功(参见图 3-14 )。
图 3-14 。成功的 OpenDJ 确认
由于您已经将 OpenDJ 安装为 Windows 服务,您可以通过转到控制面板管理工具
服务并选择 OpenDJ 并单击开始来启动 LDAP 服务器(图 3-15 )。
图 3-15 。将 OpenDJ 作为 Windows 服务运行
注意如果你没有安装 OpenDJ 作为 Windows 服务,你可以使用/bat 文件夹下的 start-ds.bat 和 stop-ds.bat 文件启动和停止服务器。
安装 Apache Directory Studio
Apache Directory Studio 是一个流行的开源 LDAP 浏览器,可以帮助您非常容易地浏览 LDAP 目录。要安装 Apache Directory Studio,请从以下网址下载安装程序文件
http://directory.apache.org/studio/downloads.html.
工作室安装可以通过以下步骤完成。
-
在 Windows 上,双击安装文件开始安装(这将显示安装屏幕)。
-
阅读并接受许可协议以继续。
-
Choose your preferred installation directory, and select “Install” (see Figure 3-16).
图 3-16 。Apache 安装目录选择
-
您将看到安装和文件传输的状态。
-
传输完所有文件后,单击“完成”按钮完成安装。
安装完成后,下一步是创建到新安装的 OpenDJ LDAP 服务器的连接。在继续之前,请确保您的 OpenDJ 服务器正在运行。下面是建立新连接的步骤。
-
启动 ApacheDS 服务器。在 Windows 中,单击 Apache 目录 Studio.exe 文件。
-
Launch the New Connection wizard by right-clicking in the “Connections” section and selecting “New Connection.”
图 3-17。创建新连接
-
On the Network Parameter screen, enter the information displayed in Figure 3-18. This should match the OpenDJ information you entered during OpenDJ installation.
图 3-18 。LDAP 连接网络参数
-
On the Authentication screen, enter “cn=Directory Manager” as Bind DN or user and “opendj” as password (see Figure 3-19).
图 3-19 。LDAP 连接认证
-
接受浏览器选项部分的默认值,并选择完成按钮。
加载测试数据
在前面的小节中,您安装了 OpenDJ LDAP 服务器和 Apache Directory Studio 来访问 LDAP 服务器。设置开发/测试环境的最后一步是用测试数据加载 LDAP 服务器。
注意随附的源代码/下载包含两个 LDIF 文件,customers . ldif 和 employees . ldif。customers . ldif 文件包含模拟您的库的客户的测试数据。employees.ldif 文件包含模拟库雇员的测试数据。这两个文件大量用于测试本书中使用的代码。如果您还没有完成,请在继续之前下载这些文件。
下面是加载测试数据的步骤。
-
Right-click “Root DSE” in the LDAP browser pane and select Import
LDIF Import (see Figure 3-20).
图 3-20 。LDIF 进口
-
Browse for this patrons.ldif file (see Figure 3-21) and click the Finish button. Make sure that the “Update existing entries” checkbox is selected.
图 3-21 。LDIF 导入设置
-
成功导入后,您将看到 dc=inflinx,dc=com 条目下加载的数据(参见图 3-22 )。
图 3-22 。LDIF 成功导入
Spring LDAP Hello World
有了这些信息,让我们进入 Spring LDAP 的世界。您将从编写一个简单的搜索客户端开始,它读取 ou = customers LDAP 分支中的所有顾客姓名。这类似于你在第二章中看到的例子。清单 3-3 显示了搜索客户端代码。
清单 3-3。
public class SearchClient {
@SuppressWarnings("unchecked")
public List<String> search() {
LdapTemplate ldapTemplate = getLdapTemplate();
List<String> nameList = ldapTemplate.search( "dc=inflinx,dc=com",
"(objectclass=person)",
new AttributesMapper() {
@Override
public Object mapFromAttributes(Attributes attributes)
throws NamingException {
return (String)attributes.get("cn").get();
}
});
return nameList;
}
private LdapTemplate getLdapTemplate() { ....... }
}
Spring LDAP 框架的核心是 org . Spring framework . LDAP . core . LDAP template 类。基于模板方法设计模式(en.wikipedia.org/wiki/Template_method_pattern),LdapTemplate 类负责处理 LDAP 编程中不必要的管道工作。它提供了许多重载的搜索、查找、绑定、认证和解除绑定方法,使得 LDAP 开发变得轻而易举。LdapTemplate 是线程安全的,并发线程可以使用同一个实例。
简单 LDAP 模板
Spring LDAP 版本 1.3 引入了一个名为 SimpleLdapTemplate 的 LdapTemplate 变体。这是一个基于 Java 5 的传统 LdapTemplate 的便利包装器。SimpleLdapTemplate 为查找和搜索方法添加了 Java 5 泛型支持。这些方法现在将 ParameterizedContextMapper的实现作为参数,允许搜索和查找方法返回类型化的对象。
SimpleLdapTemplate 仅公开 LdapTemplate 中可用操作的子集。然而,这些操作是最常用的,因此 SpringLdapTemplate 在很多情况下就足够了。SimpleLdapTemplate 还提供 getLdapOperations()方法,该方法公开包装的 LdapOperations 实例,并可用于调用不常用的模板方法。
在本书中,您将使用 LdapTemplate 和 SimpleLdapTemplate 类来实现代码。
通过获取 LdapTemplate 类的一个实例来开始搜索方法的实现。然后调用 LdapTemplate 的搜索方法的变体。搜索方法的第一个参数是 LDAP base,第二个参数是搜索过滤器。search 方法使用 base 和 filter 来执行搜索,获得的每个 javax . naming . directory . search result 被提供给 org . spring framework . LDAP . core . attributes mapper 的一个实现,该实现作为第三个参数提供。在清单 3-3 中,AttributesMapper 实现是通过创建一个匿名类来实现的,这个匿名类读取每个 SearchResult 条目并返回条目的公共名称。
在清单 3-3 中,getLdapTemplate 方法 为空。现在让我们看看如何实现这个方法。要使 LdapTemplate 正确执行搜索,它需要 LDAP 服务器上的初始上下文。Spring LDAP 提供 org . spring framework . LDAP . core . context source 接口抽象及其实现 org . spring framework . LDAP . core . support . ldapcontextsource 用于配置和创建上下文实例。清单 3-4 展示了 getLdapTemplate 实现的完整方法。
清单 3-4。
private LdapTemplate getLdapTemplate() {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl("ldap://localhost:11389");
contextSource.setUserDn("cn=Directory Manager");
contextSource.setPassword("opendj");
try {
contextSource.afterPropertiesSet();
}
catch(Exception e) {
e.printStackTrace();
}
LdapTemplate ldapTemplate = new LdapTemplate();
ldapTemplate.setContextSource(contextSource);
return ldapTemplate;
}
通过创建一个新的 LdapContextSource 并用关于 LDAP 服务器的信息(如服务器 URL 和绑定凭证)填充它来开始方法实现。然后在上下文源上调用 afterPropertiesSet 方法,该方法允许 Spring LDAP 执行内务操作。最后,创建一个新的 LdapTemplate 实例,并传入新创建的上下文源。
这就完成了您的搜索客户端示例。清单 3-5 显示了调用搜索操作并将名称打印到控制台的主方法。
清单 3-5。
public static void main(String[] args) {
SearchClient client = new SearchClient();
List<String> names = client.search();
for(String name: names) {
System.out.println(name);
}
}
这个搜索客户端实现简单地使用了 Spring LDAP API,没有任何特定于 Spring 框架的范例。在接下来的几节中,您将看到这个应用的弹性化。但在此之前,让我们快速看一下 Spring ApplicationContext。
Spring ApplicationContext
每个 Spring 框架应用的核心是 ApplicationContext 的概念。该接口的实现负责创建和配置 Spring beans。应用上下文还充当 IoC 容器,负责执行依赖注入。Spring bean 只是一个标准的 POJO,带有在 Spring 容器中运行所需的元数据。
在标准的 Spring 应用中,ApplicationContext 是通过 XML 文件或 Java 注释配置的。清单 3-6 显示了一个带有一个 bean 声明的样例应用上下文文件。bean myBean 的类型是 com . inflinx . book . LDAP . SimplePojo,当应用加载上下文时,Spring 会创建一个 simple POJO 实例并管理它。
清单 3-6。
<?xml version="1.0" encoding="UTF-8"?>
<beans FontName3">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/
beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="myBean" class="com.inflinx.book.ldap.SimplePojo">
</bean>
</beans>
Spring 支持的搜索客户端
我们对搜索客户端实现的转换从 applicationContext.xml 文件开始,如清单 3-7 所示。
清单 3-7。
<beans FontName3">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context
/spring-context.xsd">
<bean id="contextSource"
class="org.springframework.ldap.core.support.LdapContextSource">
<property name="url" value="ldap://localhost:11389" />
<property name="userDn" value="cn=Directory Manager" />
<property name="password" value="opendj" />
</bean>
<bean id="ldapTemplate"
class="org.springframework.ldap.core.LdapTemplate">
<constructor-arg ref="contextSource" />
</bean>
<context:component-scan base-package="com.inflinx.book.ldap"/>
</beans>
在上下文文件中,您声明一个 contextSource bean 来管理到 LDAP 服务器的连接。为了让 LdapContextSource 正确地创建 DirContext 的实例,您需要向它提供关于 LDAP 服务器的信息。url 属性采用 ldap 服务器的全限定 URL (ldap://server:port 格式)。base 属性可用于指定所有 LDAP 操作的根后缀。userDn 和 password 属性用于提供身份验证信息。接下来,配置一个新的 LdapTemplate bean 并注入 contextSource bean。
在上下文文件中声明了所有的依赖项后,您可以继续重新实现搜索客户端,如清单 3-8 所示。
清单 3-8。
package com.inflinx.book.ldap;
import java.util.List;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support. ClassPathXmlApplicationContext;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.stereotype.Component;
@Component
public class SpringSearchClient {
@Autowired
@Qualifier("ldapTemplate")
private LdapTemplate ldapTemplate;
@SuppressWarnings("unchecked")
public List<String> search() {
List<String> nameList = ldapTemplate.search("dc=inflinx,dc=com",
"(objectclass=person)",
new AttributesMapper() {
@Override
public Object mapFromAttributes(Attributes attributes)
throws NamingException {
return (String)attributes.get("cn").get();
}
});
return nameList;
}
}
你会注意到这段代码与你在清单 3-4 中看到的 SearchClient 代码没有什么不同。您只是将 LdapTemplate 的创建提取到一个外部配置文件中。 @Autowired 注释指示 Spring 注入 ldapTemplate 依赖项。这极大地简化了搜索客户端类,并帮助您关注搜索逻辑。
运行新搜索客户端的代码如清单 3-9 所示。首先创建 ClassPathXmlApplicationContext 的一个新实例。ClassPathXmlApplicationContext 将 applicationContext.xml 文件作为其参数。然后,从上下文中检索 SpringSearchClient 的一个实例,并调用 search 方法。
清单 3-9。
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
SpringSearchClient client = context.getBean(SpringSearchClient.class);
List<String> names = client.search();
for(String name: names) {
System.out.println(name);
}
}
弹簧 l 模板操作
在上一节中,您利用了 LdapTemplate 来实现搜索。现在,让我们看看如何使用 LdapTemplate 在 LDAP 中添加、删除和修改信息。
添加操作
LdapTemplate 类提供了几个绑定方法,允许您创建新的 LDAP 条目。这些方法中最简单的如下:
public void bind(String dn, Object obj, Attributes attributes)
此方法的第一个参数是需要绑定的对象的唯一可分辨名称。第二个参数是要绑定的对象,通常是 DirContext 接口的实现。第三个参数是要绑定的对象的属性。在这三个参数中,只有第一个参数是必需的,您可以为其余两个参数传递 null。
清单 3-10 显示了用最少的信息创建一个新顾客条目的代码。您通过创建一个新的 BasicAttributes 类实例来保存 patron 属性,从而开始方法实现。通过将属性名和值传递给 put 方法来添加单值属性。要添加多值属性 objectclass,需要创建 BasicAttribute 的新实例。然后,将条目的 objectClass 值添加到 objectClassAttribute,并将其添加到属性列表。最后,使用顾客信息和顾客的全限定 DN 调用 LdapTemplate 上的 bind 方法。这将顾客条目添加到 LDAP 服务器。
清单 3-10。
public void addPatron() {
// Set the Patron attributes
Attributes attributes = new BasicAttributes();
attributes.put("sn", "Patron999");
attributes.put("cn", "New Patron999");
// Add the multi-valued attribute
BasicAttribute objectClassAttribute = new BasicAttribute("objectclass");
objectClassAttribute.add("top");
objectClassAttribute.add("person");
objectClassAttribute.add("organizationalperson");
objectClassAttribute.add("inetorgperson");
attributes.put(objectClassAttribute);
ldapTemplate.bind("uid=patron999,ou=patrons,dc=inflinx,dc=com",null, attributes);
}
修改操作
考虑这样一个场景,您想要为新添加的顾客添加一个电话号码。为此,LdapTemplate 提供了一个方便的 modifyAttributes 方法,具有以下签名:
public void modifyAttributes(String dn, ModificationItem[] mods)
modifyAttributes 方法的这种变体将以要修改的条目的完全限定的惟一 DN 作为其第一个参数。第二个参数接受一个 ModificationItems 数组,其中每个修改项保存需要修改的属性信息。
清单 3-11 显示了向顾客添加新电话号码的代码。
清单 3-11。
public void addTelephoneNumber() {
Attribute attribute = new BasicAttribute("telephoneNumber", "801 100 1000");
ModificationItem item = new ModificationItem(DirContext.ADD_ATTRIBUTE, attribute);
ldapTemplate.modifyAttributes("uid=patron999," + "ou=patrons,dc=inflinx,dc=com", new ModificationItem[] {item});
}
在这个实现中,您只需创建一个包含电话信息的新 BasicAttribute。然后创建一个新的 ModificationItem 并传入 ADD_ATTRIBUTE 代码,表明您正在添加一个属性。最后,使用顾客 DN 和修改项调用 modifyAttributes 方法。DirContext 有一个 REPLACE_ATTRIBUTE 代码,使用该代码时将替换属性值。类似地,REMOVE_ATTRIBUTE 代码将从属性中移除指定的值。
删除操作
与添加和修改类似,LdapTemplate 使使用 unbind 方法删除条目变得很容易。清单 3-12 提供了实现 unbind 方法和删除顾客的代码。如您所见,unbind 方法接受需要删除的条目的 DN。
清单 3-12。
public void removePatron() {
ldapTemplate.unbind("uid=patron999," + "ou=patrons,dc=inflinx,dc=com");
}
摘要
Spring LDAP 框架旨在简化 Java 中的 LDAP 编程。在这一章中,您对 Spring LDAP 和一些与 Spring Framework 相关的概念有了一个高层次的概述。您还了解了启动和运行 Spring LDAP 所需的设置。在下一章中,您将关注于测试 Spring LDAP 应用。
四、测试 LDAP 代码
在本章中,您将学习
- 单元/模拟/集成测试的基础。
- 使用嵌入式 LDAP 服务器进行测试。
- 使用 EasyMock 进行模拟测试。
- 生成测试数据。
测试 是任何软件开发过程的一个重要方面。除了检测错误之外,它还有助于验证所有需求是否得到满足,以及软件是否按预期工作。今天,正式或非正式地,测试几乎包含在软件开发过程的每个阶段。根据测试的内容和测试背后的目的,我们最终会有几种不同类型的测试。开发人员最常做的测试是单元测试,它确保单个单元按预期工作。集成测试通常在单元测试之后,并关注之前测试的组件之间的交互。开发人员通常参与创建自动化集成测试,尤其是处理数据库和目录的测试。接下来是系统测试,对完整的集成系统进行评估,以确保满足所有要求。非功能性需求,如性能和效率,也作为系统测试的一部分进行测试。验收测试通常在最后进行,以确保交付的软件满足客户/企业用户的需求。
单元测试
单元测试是一种测试方法,在这种方法中,应用的最小部分,称为单元,被独立地单独验证和确认。在结构化编程中,这个单元可以是一个单独的方法或函数。在面向对象编程(OOP) 中,对象是最小的可执行单元。对象之间的交互是任何面向对象设计的核心,通常通过调用方法来完成。因此,OOP 中的单元测试可以从测试单个方法到测试一组对象。
编写单元测试需要开发人员的时间和精力。但事实证明,这项投资带来了几个不可否认的好处。
注意衡量单元测试覆盖了多少代码是很重要的。像 Clover 和 Emma 这样的工具提供了代码覆盖率的度量。这些度量标准也可以用来突出任何由很少的单元测试(或者根本没有单元测试)执行的路径。
单元测试的最大优势是它可以帮助在开发的早期阶段识别错误。只有在 QA 或生产中发现的错误会消耗更多的调试时间和金钱。此外,一组好的单元测试就像一个安全网,当代码被重构时会给人信心。单元测试可以帮助改进设计,甚至可以作为文档。
好的单元测试具有以下特征:
- 每个单元测试必须独立于其他测试。这种原子性非常重要,每个测试都不能对其他测试产生任何副作用。单元测试也应该是顺序独立的。
- 单元测试必须是可重复的。对于一个有价值的单元测试来说,它必须产生一致的结果。否则,它不能在重构期间用作健全性检查。
- 单元测试必须易于设置和清理。所以他们不应该依赖外部系统,比如数据库和服务器。
- 单元测试必须快速并提供即时反馈。在做出另一个改变之前等待长时间运行的测试是没有意义的。
- 单元测试必须是自我验证的。每个测试应该包含足够的信息来自动确定测试是通过还是失败。不需要人工干预来解释结果。
企业应用通常使用外部系统,如数据库、目录和 web 服务。在道层更是如此。例如,单元测试数据库代码可能涉及启动数据库服务器、加载模式和数据、运行测试以及关闭服务器。这很快变得棘手和复杂。一种方法是使用模拟对象并隐藏外部依赖。在这还不够的地方,可能有必要使用集成测试,并在外部依赖完整无损的情况下测试代码。让我们更详细地看一下每个案例。
模拟测试
模拟测试的目标是使用模拟对象以可控的方式模拟真实对象。模拟对象实现了与真实对象相同的接口,但是被编写成模仿/伪造并跟踪它们的行为。
例如,考虑一个 UserAccountService ,它有一个创建新用户帐户的方法。这种服务的实现通常包括根据业务规则验证帐户信息,将新创建的帐户存储在数据库中,并发送确认电子邮件。持久化数据和电子邮件信息通常被抽象到其他层的类中。现在,当编写单元测试来验证与帐户创建相关的业务规则时,您可能并不真正关心电子邮件通知部分所涉及的复杂性。但是,您确实想验证是否生成了一封电子邮件。这正是模拟对象派上用场的地方。要实现这一点,您只需要为 UserAccountService 提供一个负责发送电子邮件的 EmailService 的模拟实现。模拟实现将简单地标记电子邮件请求,并返回硬编码的结果。模拟对象是将测试从复杂的依赖关系中分离出来的一种很好的方式,允许它们运行得更快。
有几个开源框架使得使用模拟对象更加容易。比较流行的有 Mockito,EasyMock,JMock。这些框架的完整对比列表可以在code . Google . com/p/jmockit/wiki/MockingToolkitComparisonMatrix找到。
其中一些框架允许为没有实现任何接口的类创建模拟。不管使用什么框架,使用模拟对象的单元测试通常包括以下步骤:
- 创建一个新的模拟实例。
- 设置模拟。这包括指导模仿者期望什么和返回什么。
- 运行测试,将模拟实例传递给被测试的组件。
- 验证结果。
集成测试
尽管模仿对象是很好的占位符,但是很快你就会发现伪装是不够的。对于 DAO 层代码来说尤其如此,在那里您需要验证 SQL 查询的执行并验证对数据库记录的修改。测试这种代码属于集成测试的范畴。如前所述,集成测试侧重于测试组件之间的交互以及它们的依赖关系。
开发人员使用单元测试工具编写自动化集成测试已经变得很常见,从而模糊了两者之间的区别。然而,重要的是要记住,集成测试不会孤立地运行,通常会更慢。像 Spring 这样的框架为编写和执行集成测试提供了容器支持。嵌入式数据库、目录和服务器可用性的提高使开发人员能够编写更快的集成测试。
JUnit〔??〕
JUnit 已经成为 Java 应用单元测试的事实标准。JUnit 4.x 中注释的引入使得创建测试和断言预期值的测试结果变得更加容易。JUnit 可以很容易地与 ANT 和 Maven 等构建工具集成。它在所有流行的 ide 中都有很好的工具支持。
对于 JUnit,标准的做法是编写一个单独的类来保存测试方法。这个类通常被称为测试用例,每种测试方法都是为了测试一个工作单元。也可以将测试用例组织成称为测试套件的组。
学习 JUnit 的最好方法是编写一个测试方法。清单 4-1 展示了一个简单的 StringUtils 类和一个 isEmpty 方法。该方法将字符串作为参数,如果该字符串为 null 或空字符串,则返回 true。
清单 4-1。
public class StringUtils {
public static boolean isEmpty(String text) {
return test == null || "".equals(test);
}
}
清单 4-2 是带有测试代码方法的 JUnit 类。
清单 4-2。
public class StringUtilsTest {
@Test
public void testIsEmpty() {
Assert.assertTrue(StringUtils.isEmpty(null));
Assert.assertTrue(StringUtils.isEmpty(""));
Assert.assertFalse(StringUtils.isEmpty("Practical Spring Ldap"));
}
}
注意,我遵循了惯例 Test 来命名测试类。在 JUnit 4.x 之前,测试方法需要以单词“test”开头。在 4.x 中,测试方法只需要用注释@Test 来标记。还要注意 testIsEmpty 方法包含几个用于测试 IsEmpty 方法逻辑的断言。
表 4-1 列出了 JUnit 4 中一些重要的注释。
表 4-1 。JUnit 4 注解
| 注释 | 描述 |
|---|---|
| @测试 | 将方法注释为 JUnit 测试方法。该方法应该是公共范围的,并且具有 void 返回类型。 |
| @以前 | 将方法标记为在每个测试方法之前运行。对于设置测试夹具很有用。超类的@Before 方法在当前类之前运行。 |
| @之后 | 将方法标记为在每个测试方法之后运行。用于拆除测试夹具。超类的@After 方法在当前类之前运行。 |
| @忽略 | 标记测试运行期间要忽略的方法。这有助于避免评论半成品测试方法的需要。 |
| @BeforeClass | 在任何测试方法运行之前注释要运行的方法。对于测试用例,该方法只运行一次,可用于提供类级别的设置工作。 |
| @课后 | 注释一个在所有测试方法运行后运行的方法。这对于在类级别执行任何清理非常有用。 |
| @RunWith | 指定用于运行 JUnit 测试用例的类。 |
使用嵌入式 LDAP 服务器进行测试
ApacheDS、OpenDJ 和 UnboundID 是可以嵌入到 Java 应用中的开源 LDAP 目录。嵌入式目录是应用的 JVM 的一部分,使得启动和关闭等任务的自动化变得容易。它们启动时间短,通常运行速度快。嵌入式目录还消除了对每个开发人员或构建机器的专用、独立 LDAP 服务器的需求。
注注这里讨论的概念是 LdapUnit 开源项目的基础。在以后的章节中,您将使用 LdapUnit 来测试代码。请访问
ldapunit.org下载项目工件并浏览完整的源代码。
嵌入 LDAP 服务器包括以编程方式创建服务器并启动/停止它。然而,尽管 ApacheDS 或 OpenDJ 已经很成熟,但是以编程方式与它们进行交互还是很麻烦。在下一节中,您将看到配置和使用 ApacheDS LDAP 服务器所必需的设置。
设置嵌入式 ApacheDS
ApacheDS 的核心是存储数据和支持搜索操作的目录服务。因此,启动 ApacheDS LDAP 服务器首先要创建和配置一个目录服务。清单 4-3 显示了与创建目录服务相关的代码。请注意,您只是在使用 DefaultDirectoryServiceFactory 并对其进行初始化。
清单 4-3。
DirectoryServiceFactory dsf = DefaultDirectoryServiceFactory.DEFAULT;
dsf.init( "default" + UUID.randomUUID().toString() );
directoryService = dsf.getDirectoryService();
ApacheDS 使用分区来存储 LDAP 条目。(一个分区可以被看作是一个保存整个 DIT 的逻辑容器)。一个 ApacheDS 实例可能有多个分区。与每个分区相关联的是一个根识别名(DN) ,称为分区后缀。该分区中的所有条目都存储在该根 DN 下。清单 4-4 中的代码创建了一个分区,并将其添加到清单 4-3 中的目录服务中。
清单 4-4。
PartitionFactory partitionFactory =
DefaultDirectoryServiceFactory.DEFAULT.getPartitionFactory();
/* Create Partition takes id, suffix, cache size, working directory*/
Partition partition = partitionFactory.createPartition("dc=inflinx,dc=com", "dc=inflinx,dc=com", 1000, new File(
directoryService.getWorkingDirectory(),rootDn));
partition.setSchemaManager(directoryService.getSchemaManager());
// Inject the partition into the DirectoryService
directoryService.addPartition( partition );
您可以使用分区工厂来创建分区。为了创建新分区,您必须提供以下信息:唯一标识分区的名称、分区后缀或 rootDn、高速缓存大小和工作目录。在清单 4-4 中,您也使用了 rootDn 作为分区名。
创建并配置了目录服务后,下一步是创建 LDAP 服务器。清单 4-5 显示了与之相关的代码。向新创建的 LDAP 服务器提供一个名称。然后创建一个 TcpTransport 对象,它将监听端口 12389。TcpTransport 实例允许客户端与 LDAP 服务器通信。
清单 4-5。
// Create the LDAP server
LdapServer ldapServer = new LdapServer();
ldapServer.setServiceName("Embedded LDAP service");
TcpTransport ldapTransport = new TcpTransport(12389); ldapServer.setTransports(ldapTransport);
ldapServer.setDirectoryService( directoryService );
最后一步是启动服务,,这是通过以下代码实现的:
directoryService.startup();
ldapServer.start();
这就完成了启动方法的实现。关闭方法的实现在清单 4-6 中描述。
清单 4-6。
public void stopServer() {
try {
System.out.println("Shutting down LDAP Server ....");
ldapServer.stop();
directoryService.shutdown();
FileUtils.deleteDirectory( directoryService.getWorkingDirectory() );
System.out.println("LDAP Server shutdown" + " successful ....");
}
catch(Exception e) {
throw new RuntimeException(e);
}
}
除了调用 stop/shutdown 方法之外,请注意您已经删除了 DirectoryService 的工作目录。嵌入式 ApacheDS 实现的完整代码如清单 4-7 所示。
清单 4-7。
package org.ldapunit.server;
import java.io.File;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.directory.server.core.DirectoryService;
import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory;
import org.apache.directory.server.core.factory. DirectoryServiceFactory;
import org.apache.directory.server.core.factory.PartitionFactory;
import org.apache.directory.server.core.partition.Partition;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.protocol.shared. transport.TcpTransport;
public class ApacheDSConfigurer implements EmbeddedServerConfigurer {
private DirectoryService directoryService;
private LdapServer ldapServer;
private String rootDn;
private int port;
public ApacheDSConfigurer(String rootDn, int port) {
this.rootDn = rootDn;
this.port = port;
}
public void startServer() {
try {
System.out.println("Starting Embedded " +
"ApacheDS LDAP Server ....");
DirectoryServiceFactory dsf = DefaultDirectoryServiceFactory.
DEFAULT;
dsf.init( "default" + UUID.randomUUID().toString());
directoryService = dsf.getDirectoryService();
PartitionFactory partitionFactory = DefaultDirectoryServiceFactory.
DEFAULT.getPartitionFactory();
/* Create Partition takes id, suffix, cache size, working
directory*/
Partition partition = partitionFactory.
createPartition(rootDn,rootDn, 1000, new File(directoryService.
getWorkingDirectory(), rootDn));
partition.setSchemaManager(directoryService.getSchemaManager());
// Inject the partition into the DirectoryService
directoryService.addPartition( partition );
// Create the LDAP server ldapServer = new LdapServer();
ldapServer.setServiceName("Embedded LDAP service");
TcpTransport ldapTransport = new TcpTransport(port);
ldapServer.setTransports(ldapTransport);
ldapServer.setDirectoryService( directoryService );
directoryService.startup();
ldapServer.start();
System.out.println("Embedded ApacheDS LDAP server" + "has started
successfully ....");
}
catch(Exception e) {
throw new RuntimeException(e);
}
}
public void stopServer() {
try {
System.out.println("Shutting down Embedded " + "ApacheDS LDAP
Server ....");
ldapServer.stop();
directoryService.shutdown();
FileUtils.deleteDirectory( directoryService.getWorkingDirectory() );
System.out.println("Embedded ApacheDS LDAP " + "Server shutdown
successful ....");
}
catch(Exception e) {
throw new RuntimeException(e);
}
}
}
创建嵌入式上下文工厂
有了上面的代码,下一步是自动启动服务器并创建可以用来与嵌入式服务器交互的上下文。在 Spring 中,可以通过实现创建 ContextSource 新实例的自定义 FactoryBean 来实现这一点。在清单 4-8 中,您开始创建上下文工厂。
清单 4-8。
package com.practicalspring.ldap.test;
import org.springframework.beans.factory.config. AbstractFactoryBean;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.ldapunit.server.ApacheDSConfigurer;
import org.apache.directory.server.ldap.LdapServer;
public class EmbeddedContextSourceFactory extends
AbstractFactoryBean<ContextSource> {
private int port;
private String rootDn;
private ApacheDSConfigurer apacheDsConfigurer;
@Override
public Class<?> getObjectType() {
return ContextSource.class;
}
@Override
protected ContextSource createInstance() throws Exception {
// To be implemented later.
return null;
}
public void setRootDn(String rootDn) {
this.rootDn = rootDn;
}
public void setPort(int port) {
this.port = port;
}
}
请注意,EmbeddedContextSourceFactory bean 使用了两个 setter 方法:setPort 和 setRootDn。setPort 方法可用于设置嵌入式服务器运行的端口。setRootDn 方法可用于提供根上下文的名称。清单 4-9 展示了 createInstance 方法的实现,它创建了 ApacheDSConfigurer 的一个新实例并启动了服务器。然后,它创建一个新的 LdapContenxtSource,并用嵌入的 LDAP 服务器信息填充它。
清单 4-9。
apacheDsConfigurer = new ApacheDSConfigurer(rootDn, port);
apacheDsConfigurer.startServer();
LdapContextSource targetContextSource = new LdapContextSource();
targetContextSource.setUrl("ldap://localhost:" + port);
targetContextSource.setUserDn(ADMIN_DN);
targetContextSource.setPassword(ADMIN_PWD);
targetContextSource.setDirObjectFactory(DefaultDirObjectFactory.class);
targetContextSource.afterPropertiesSet();
return targetContextSource;
destroyInstance 的实现在清单 4-10 中提供。它只需要清理创建的上下文并停止嵌入式服务器。
清单 4-10。
@Override
protected void destroyInstance(ContextSource instance) throws Exception {
super.destroyInstance(instance);
apacheDsConfigurer.stopServer();
}
最后一步是创建一个使用新上下文工厂的 Spring 上下文文件。这显示在清单 4-11 中。注意,嵌入的上下文源被注入到 ldapTemplate 中。
清单 4-11。
<?xml version="1.0" encoding="UTF-8"?>
<beans FontName3">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="contextSource" class="com.inflinx.ldap.test.
EmbeddedContextSourceFactory">
<property name="port" value="12389" />
<property name="rootDn" value="dc=inflinx,dc=com" />
</bean>
<bean id="ldapTemplate" class="org.springframework.ldap.core.
LdapTemplate">
<constructor-arg ref="contextSource" />
</bean>
</beans>
现在您已经拥有了编写 JUnit 测试用例所需的整个基础设施。清单 4-12 显示了一个简单的 JUnit 测试用例。这个测试用例有一个在每个测试方法之前运行的设置方法。在 setup 方法中,您加载数据,以便 LDAP 服务器处于已知状态。在清单 4-12 中,您正在从 employees.ldif 文件中加载数据。teardown 方法在每个测试方法运行后运行。在 teardown 方法中,您将删除 LDAP 服务器中的所有条目。这将允许您开始新的测试。这三种测试方法非常简单,只是在控制台上打印信息。
清单 4-12。
package com.inflinx.book.ldap.test;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration(locations= {"classpath:repositoryContext-test.xml"})
public class TestRepository {
@Autowired
ContextSource contextSource;
@Autowired
LdapTemplate ldapTemplate;
@Before
public void setup() throws Exception {
System.out.println("Inside the setup");
LdapUnitUtils.loadData(contextSource, new ClassPathResource
("employees.ldif"));
}
@After
public void teardown() throws Exception {
System.out.println("Inside the teardown");
LdapUnitUtils.clearSubContexts(contextSource, new DistinguishedName
("dc=inflinx,dc=com"));
}
@Test
public void testMethod() {
System.out.println(getCount(ldapTemplate));
}
@Test
public void testMethod2() {
ldapTemplate.unbind(new DistinguishedName("uid=employee0,ou=employees,
dc=inflinx,dc=com"));
System.out.println(getCount(ldapTemplate));
}
@Test
public void testMethod3() {
System.out.println(getCount(ldapTemplate));
}
private int getCount(LdapTemplate ldapTemplate) {
List results = ldapTemplate.search("dc=inflinx,dc=com",
"(objectClass=inetOrgPerson)", new ContextMapper() {
@Override
public Object mapFromContext(Object ctx) {
return ((DirContextAdapter)ctx).getDn();
}
});
return results.size();
}
}
使用 EasyMock 模仿 LDAP】
在上一节中,您了解了如何使用嵌入式 LDAP 服务器测试 LDAP 代码。现在让我们看看使用 EasyMock 框架测试 LDAP 代码。
EasyMock 是一个开源库,它使得创建和使用模拟对象变得容易。从 3.0 版本开始,EasyMock 本机支持模仿接口和具体类。EasyMock 的最新版本可以从 easymock.org/Downloads.h… 下载。为了模仿具体的类,需要两个额外的库,即 CGLIB 和 Objenesis。 Maven 用户只需在他们的 pom.xml 中添加以下依赖项,就可以获得所需的 jar 文件:
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.2</version>
<scope>test</scope>
</dependency>
使用 EasyMock 创建一个 mock 需要调用 EasyMock 类上的 createMock 方法。以下示例为 LdapTemplate 创建一个模拟对象:
LdapTemplate ldapTemplate = EasyMock.createMock(LdapTemplate. class);
每个新创建的模拟对象都以记录模式启动。在这种模式下,您记录了模拟的预期行为或期望。例如,您可以告诉 mock,如果这个方法被调用,就返回这个值。例如,以下代码向 LdapTemplate 模拟添加了一个新的期望:
EasyMock.expect(ldapTemplate.bind(isA(DirContextOperations. class)));
在这段代码中,您将指示 mock 调用一个 bind 方法,并将 DirContextOperations 的一个实例作为它的参数传入。
一旦记录了所有的期望,mock 需要能够重放这些期望。这是通过调用 EasyMock 上的 replay 方法并传入需要作为参数重放的模拟对象来完成的。
EasyMock.replay(ldapTemplate);
现在可以在测试用例中使用模拟对象了。一旦被测试的代码完成了它的执行,您就可以验证是否满足了 mock 上的所有期望。这是通过调用 EasyMock 上的验证方法来完成的。
EasyMock.verify(ldapTemplate);
模拟对于验证搜索方法中使用的上下文行映射器特别有用。正如您之前看到的,行映射器实现将 LDAP 上下文/条目转换成 Java 域对象。下面是执行转换的 ContextMapper 接口中的方法签名:
public Object mapFromContext(Object ctx)
此方法中的 ctx 参数通常是 DirContextOperations 实现的一个实例。因此,为了对 ContextMapper 实现进行单元测试,您需要向 mapFromContext 方法传递一个模拟 DirContextOperations 实例。mock DirContextOperations 应返回虚拟但有效的数据,以便 ContextMapper 实现可以从中创建域对象。清单 4-13 显示了模拟和填充 DirContextOperations 实例的代码。mockContextOperations 遍历传入的伪属性数据,并添加对单值和多值属性的期望。
清单 4-13。
public static DirContextOperations mockContextOperations(Map<String, Object>
attributes) {
DirContextOperations contextOperations = createMock(DirContextOperations.
class);
for(Entry<String, Object> entry : attributes.entrySet()){
if(entry.getValue() instanceof String){
expect(contextOperations.getStringAttribute(eq(entry.
getKey()))).andReturn((String)entry.getValue());
expectLastCall().anyTimes();
}
else if(entry.getValue() instanceof String[]){
expect(contextOperations.
getStringAttributes(eq(entry.getKey()))).andReturn((String[])
entry.getValue());
expectLastCall().anyTimes();
}
}
return contextOperations;
}
有了这些代码后,清单 4-14 显示了使用 mockContextOperations 方法模拟测试上下文行映射器的代码。
清单 4-14 。
public class ContextMapperExample {
@Test
public void testConextMapper() {
Map<String, Object> attributes = new HashMap<String, Object>();
attributes.put("uid", "employee1");
attributes.put("givenName", "John"); attributes.put("surname", "Doe");
attributes.put("telephoneNumber", new String[]
{"8011001000","8011001001"});
DirContextOperations contextOperations = LdapMockUtils.mockContextOperations(attributes);
replay(contextOperations);
//Now we can use the context operations to test a mapper
EmployeeContextMapper mapper = new EmployeeContextMapper();
Employee employee = (Employee)mapper.mapFromContext(contextOperations);
verify(contextOperations);
// test the employee object
assertEquals(employee.getUid(), "employee1");
assertEquals(employee.getFirstName(), "John");
}
}
测试数据生成
出于测试目的,您通常需要生成初始测试数据。OpenDJ 提供了一个很棒的命令行实用程序 make- ldif,它使生成测试 LDAP 数据变得轻而易举。关于安装 OpenDJ 的说明,请参考第三章。Windows 操作系统的命令行工具位于 OpenDJ 安装下的 bat 文件夹中。
make-ldif 工具需要一个模板来创建测试数据。您将使用清单 4-15 中所示的 patron.template 文件来生成 patron 条目。
清单 4-15 。
define suffix=dc=inflinx,dc=com
define maildomain=inflinx.com
define numusers=101
branch: [suffix]
branch: ou=patrons,[suffix]
subordinateTemplate: person:[numusers]
template: person
rdnAttr: uid
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: <first>
sn: <last>
cn: {givenName} {sn}
initials: {givenName:1}<random:chars:ABCDEFGHIJKLMNOPQRSTUVWXYZ:1>{sn:1}
employeeNumber: <sequential:0>
uid: patron<sequential:0>
mail: {uid}@[maildomain]
userPassword: password
telephoneNumber: <random:telephone>
homePhone: <random:telephone>
mobile: <random:telephone>
street: <random:numeric:5> <file:streets> Street
l: <file:cities>
st: <file:states>
postalCode: <random:numeric:5>
postalAddress: {cn}${street}${l}, {st} {postalCode}
这是对安装时附带的 example.template 文件的简单修改。example.template 位于 <opendj_install>\config\MakeLDIF 文件夹中。uid 已修改为使用前缀“patron”而不是“user”。此外,numUsers 值已更改为 101。这表示您希望脚本生成的测试用户的数量。要生成测试数据,请在命令行中运行以下命令:</opendj_install>
C:\ practicalldap\opendj\bat>make-ldif --ldifFile
c:\ practicalldap\testdata\patrons.ldif --templateFile
c:\ practicalldap\templates\patron.template --randomSeed 1
-
- ldifFile 选项用于指定目标文件的位置。在这里,您将它存储在 testdata 目录中的 customers . ldif 下
-
- templateFile 用于指定要使用的模板文件。
-
- randomSeed 是一个整数,需要用来为数据生成过程中使用的随机数生成器提供种子。
创建成功后,您将看到类似于图 4-1 的屏幕。除了 101 个测试条目之外,该脚本还创建了两个额外的基本条目。
图 4-1 。让 LDIF 指挥结果
摘要
在本章中,您深入研究了测试 LDAP 代码。您从测试概念的概述开始。然后,您花时间为嵌入式测试设置 ApacheDS。尽管嵌入式测试简化了事情,但有时您希望测试代码,从而最大限度地减少对外部基础设施的依赖。您可以使用模拟测试来解决这些情况。最后,您使用了 OpenDJ 工具来生成测试数据。
在下一章中,您将看到如何使用对象工厂创建与 LDAP 交互的数据访问对象(Dao)。
五、高级 Spring LDAP
在本章中,您将学习
- JNDI 对象工厂基础。
- 使用对象工厂的 DAO 实现。
JNDI 对象工厂
JNDI 提供了对象工厂的概念,这使得处理 LDAP 信息更加容易。顾名思义,对象工厂将目录信息转换成对应用有意义的对象。例如,使用对象工厂可以让搜索操作返回像 Patron 或 Employee 这样的对象实例,而不是普通的 javax.naming.NamingEnumeration。
图 5-1 描述了当一个应用与一个对象工厂一起执行 LDAP 操作时所涉及的流程。流程从应用调用搜索或查找操作开始。JNDI API 将执行请求的操作,并从 LDAP 中检索条目。这些结果然后被传递给注册的对象工厂,后者将它们转换成对象。这些对象被移交给应用。
图 5-1 。JNDI/对象工厂流程
处理 LDAP 的对象工厂需要实现 javax . naming . SPI . dirobject factory 接口。清单 5-1 显示了一个顾客对象工厂的实现,它接受传入的信息并创建一个顾客实例。getObjectInstance 方法的 obj 参数保存关于对象的引用信息。name 参数保存对象的名称。attrs 参数包含与对象相关联的属性。在 getObjectInstance 中,您读取所需的属性并填充新创建的 Patron 实例。
清单 5-1。
package com.inflinx.book.ldap;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.spi.DirObjectFactory
import com.inflinx.book.ldap.domain.Patron;
public class PatronObjectFactory implements DirObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,Hashtable<?, ?> environment, Attributes attrs) throws Exception {
Patron patron = new Patron();
patron.setUid(attrs.get("uid").toString());
patron.setFullName(attrs.get("cn").toString());
return patron;
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,Hashtable<?, ?> environment) throws Exception {
return getObjectInstance(obj, name, nameCtx, environment, new BasicAttributes());
}
}
在您可以开始使用这个对象工厂之前,它必须在初始上下文创建期间注册。清单 5-2 展示了一个在查找过程中使用 PatronObjectFactory 的例子。使用 DirContext 注册 PatronObjectFactory 类。OBJECT _ FACTORIES 属性。注意,上下文的查找方法现在返回一个 Patron 实例。
清单 5-2。
package com.inflinx.book.ldap;
import java.util.Properties;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import com.inflinx.book.ldap.domain.Patron;
public class JndiObjectFactoryLookupExample {
private LdapContext getContext() throws NamingException {
Properties environment = new Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:11389");
environment.setProperty(DirContext.SECURITY_PRINCIPAL,"cn=Directory Manager");
environment.setProperty(DirContext.SECURITY_CREDENTIALS, "opends");
environment.setProperty(DirContext.OBJECT_FACTORIES, "com.inflinx.book.ldap.PatronObjectFactory");
return new InitialLdapContext(environment, null);
}
public Patron lookupPatron(String dn) {
Patron patron = null;
try {
LdapContext context = getContext();
patron = (Patron) context.lookup(dn);
}
catch(NamingException e) {
e.printStackTrace();
}
return patron;
}
public static void main(String[] args) {
JndiObjectFactoryLookupExample jle = new JndiObjectFactoryLookupExample();
Patron p = jle.lookupPatron("uid=patron99,ou=patrons," + "dc=inflinx,dc=com");
System.out.println(p);
}
}
Spring 和对象工厂
Spring LDAP 提供了 DirObjectFactory 的现成实现,名为 org . spring framework . LDAP . core . support . defaultdirobjectfactory。类似地,DefaultDirObjectFactory 从找到的上下文创建 org . spring framework . LDAP . core . dircontextadapter 的实例。
DirContextAdapter 类本质上是通用的,可以被视为 LDAP 条目数据的持有者。DirContextAdapter 类提供了各种实用方法,极大地简化了属性的获取和设置。正如您将在后面的小节中看到的,当对属性进行更改时,DirContextAdapter 会自动跟踪这些更改,并简化 LDAP 条目数据的更新。DirContextAdapter 和 DefaultDirObjectFactory 的简单性使您能够轻松地将 LDAP 数据转换为域对象,减少了编写和注册大量对象工厂的需要。
在接下来的小节中,您将使用 DirContextAdapter 来创建一个 Employee DAO,它抽象出 Employee LDAP 条目的读写访问。
道设计模式
今天,大多数 Java 和 JEE 应用在日常活动中都访问某种类型的持久性存储。持久性存储从流行的关系数据库到 LDAP 目录,再到遗留的大型机系统。根据持久性存储的类型,获取和操作数据的机制会有很大的不同。这可能导致应用和数据访问代码之间的紧密耦合,使实现之间的切换变得困难。这就是数据访问对象或 DAO 模式可以提供帮助的地方。
数据访问对象 是一种流行的核心 JEE 模式,它封装了对数据源的访问。低级别的数据访问逻辑(比如连接到数据源和操作数据)被 DAO 清晰地抽象到一个单独的层。一个 DAO 实现通常包括以下内容:
- 一个提供 CRUD 方法契约的 DAO 接口。
- 使用特定于数据源的 API 的接口的具体实现。
- 由 DAO 返回的域对象或传输对象。
有了 DAO,应用的其余部分就不需要担心底层的数据实现,可以专注于高级业务逻辑。
使用对象工厂的 DAO 实现
通常,您在 Spring 应用中创建的 DAO 有一个充当 DAO 契约的接口和一个包含访问数据存储或目录的实际逻辑的实现。清单 5-3 显示了您将要实现的雇员道的雇员道接口。DAO 有创建、更新和删除方法来修改雇员信息。它还有两个 finder 方法,一个根据 id 检索雇员,另一个返回所有雇员。
清单 5-3 。
package com.inflinx.book.ldap.repository;
import java.util.List;
import com.inflinx.book.ldap.domain.Employee;
public interface EmployeeDao {
public void create(Employee employee);
public void update(Employee employee);
public void delete(String id);
public Employee find(String id);
public List<Employee> findAll();
}
之前的 EmployeeDao 接口使用了一个雇员域对象。清单 5-4 展示了这个雇员域对象。雇员实现拥有一个库雇员的所有重要属性。请注意,您将使用 uid 属性作为对象的唯一标识符,而不是使用完全限定的 DN。
清单 5-4 。
package com.inflinx.book.ldap.domain;
public class Employee {
private String uid;
private String firstName;
private String lastName;
private String commonName;
private String email;
private int departmentNumber;
private String employeeNumber;
private String[] phone;
// getters and setters omitted for brevity
}
您从 EmployeeDao 的基本实现开始,如清单 5-5 所示。
清单 5-5。
package com.inflinx.book.ldap.repository;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.simple.SimpleLdapTemplate;
import com.practicalspring.springldap.domain.Employee;
@Repository("employeeDao" )
public class EmployeeDaoLdapImpl implements EmployeeDao {
@Autowired
@Qualifier("ldapTemplate" )
private SimpleLdapTemplate ldapTemplate;
@Override
public List<Employee> findAll() { return null; }
@Override
public Employee find(String id) { return null; }
@Override
public void create(Employee employee) {}
@Override
public void delete(String id) {}
@Override
public void update(Employee employee) {}
}
在这个实现中,您将注入 SimpleLdapTemplate 的一个实例。SimpleLdapTemplate 的实际创建将在外部配置文件中完成。清单 5-6 显示了带有 SimpleLdapTemplate 和相关 bean 声明的 repositoryContext.xml 文件。
清单 5-6 。
<?xml version="1.0" encoding="UTF-8"?>
<beans FontName3">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.inflinx.book.ldap" />
<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
<property name="url" value="ldap://localhost:11389" />
<property name="base" value="ou=employees,dc=inflinx,dc=com"/>
<property name="userDn" value="uid=admin,ou=system" />
<property name="password" value="secret" />
</bean>
<bean id="ldapTemplate" class="org.springframework.ldap.core.simple.SimpleLdapTemplate">
<constructor-arg ref="contextSource" />
</bean>
</beans>
这个配置文件类似于你在第三章中看到的那个。您向 LdapContextSource 提供 LDAP 服务器信息来创建 contextSource bean。通过将基础设置为“ou=employees,dc=inflinx,dc=com”,您已经将所有 LDAP 操作限制到 LDAP 树的 employee 分支。重要的是要理解,使用这里创建的上下文不可能对分支“ou = customers”进行搜索操作。如果要求搜索 LDAP 树的所有分支,那么 base 属性需要是一个空字符串。
LdapContextSource 的一个重要属性是 DirObjectFactory,可以用来设置要使用的 dirObjectFactory。然而,在清单 5-6 中,您没有使用该属性来指定您使用 DefaultDirObjectFactory 的意图。这是因为默认情况下,LdapContextSource 将 DefaultDirObjectFactory 注册为其 DirObjectFactory。
在配置文件的最后一部分,有 SimpleLdapTemplate bean 声明。您已经将 LdapContextSource bean 作为构造函数参数传递给了 SimpleLdapTemplate。
实现查找器方法
实现 employee DAO 的 findAll 方法需要在 LDAP 中搜索所有的 Employee 条目,并使用返回的条目创建 Employee 实例。为此,您将在 SimpleLdapTemplate 类中使用以下方法:
public <T> List<T> search(String base, String filter, ParameterizedContextMapper<T> mapper)
因为您使用的是 DefaultDirObjectFactory ,所以每次执行搜索或查找时,在 LDAP 树中找到的每个上下文都将作为 DirContextAdapter 的一个实例返回。就像你在清单 3-8 中看到的搜索方法,上面的搜索方法需要一个基础和过滤参数。此外,它还采用了 ParameterizedContextMapper的一个实例。上面的搜索方法会将返回的 DirContextAdapters 传递给 ParameterizedContextMapper实例进行转换。
ParameterizedContextMapper及其父接口 ContextMapper 包含从传入的 DirContextAdapter 填充域对象所需的映射逻辑。清单 5-7 提供了用于映射雇员实例的上下文映射器实现。如您所见,EmployeeContextMapper 扩展了 AbstractParameterizedContextMapper,这是一个实现 ParameterizedContextMapper 的抽象类。
清单 5-7。
package com.inflinx.book.ldap.repository.mapper;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.simple.AbstractParameterizedContextMapper;
import com.inflinx.book.ldap.domain.Employee;
public class EmployeeContextMapper extends AbstractParameterizedContextMapper<Employee> {
@Override
protected Employee doMapFromContext(DirContextOperations context) {
Employee employee = new Employee();
employee.setUid(context.getStringAttribute("UID"));
employee.setFirstName(context.getStringAttribute("givenName"));
employee.setLastName(context.getStringAttribute("surname"));
employee.setCommonName(context.getStringAttribute("commonName"));
employee.setEmployeeNumber(context.getStringAttribute("employeeNumber"));
employee.setEmail(context.getStringAttribute("mail"));
employee.setDepartmentNumber(Integer.parseInt(context.getStringAttribute("departmentNumber")));
employee.setPhone(context.getStringAttributes("telephoneNumber"));
return employee;
}
}
在清单 5-7 中,doMapFromContext 方法的 DirContextOperations 参数是 DirContextAdapter 的一个接口。如您所见,doMapFromContext 实现包括创建一个新的 Employee 实例,并从提供的上下文中读取您感兴趣的属性。
有了 EmployeeContextMapper,findAll 方法实现就变得简单了。因为所有的雇员条目都有对象类 inetOrgPerson,所以您将使用“(objectClass=inetOrgPerson)”作为搜索过滤器。清单 5-8 展示了 findAll 的实现。
清单 5-8。
@Override
public List<Employee> findAll() {
return ldapTemplate.search("", "(objectClass=inetOrgPerson)", new EmployeeContextMapper());
}
另一种查找方法可以通过两种方式实现:使用过滤器(uid= )搜索 LDAP 树,或者使用员工 DN 执行 LDAP 查找。由于使用过滤器的搜索操作比查找 DN 更昂贵,所以您将使用查找来实现 find 方法。清单 5-9 展示了查找方法的实现。
清单 5-9。
@Override
public Employee find(String id) {
DistinguishedName dn = new DistinguishedName();
dn.add("uid", id);
return ldapTemplate.lookup(dn, new EmployeeContextMapper());
}
您通过为雇员构建一个 DN 来开始实现。由于初始上下文库仅限于 employee 分支,因此您只需指定 employee 条目的 RDN 部分。然后使用 lookup 方法查找雇员条目,并使用 EmployeeContextMapper 创建一个雇员实例。
这就结束了两个查找器方法的实现。让我们创建一个 JUnit 测试类来测试您的 finder 方法。测试用例如清单 5-10 所示。
清单 5-10 。
package com.inflinx.book.ldap.repository;
import java.util.List;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ldapunit.util.LdapUnitUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.inflinx.book.ldap.domain.Employee;
@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration(locations={"classpath:repositoryContext-test.xml"})
public class EmployeeDaoLdapImplTest {
private static final String PORT = "12389";
private static final String ROOT_DN = "dc=inflinx,dc=com";
@Autowired
@Qualifier("employeeDao" )
private EmployeeDao employeeDao;
@Before
public void setup() throws Exception {
System.out.println("Inside the setup");
LdapUnitUtils.loadData(new ClassPathResource("employees.ldif"), PORT);
}
@After
public void teardown() throws Exception {
System.out.println("Inside the teardown");
LdapUnitUtils.clearSubContexts(new DistinguishedName(ROOT_DN), PORT);
}
@Test
public void testFindAll() {
List<Employee> employeeList = employeeDao.findAll();
Assert.assertTrue(employeeList.size() > 0);
}
@Test
public void testFind() {
Employee employee = employeeDao.find("employee1");
Assert.assertNotNull(employee);
}
}
请注意,您已经在 ContextConfiguration 中指定了 repositoryContext-test.xml。这个测试上下文文件显示在清单 5-11 中。在配置文件中,您已经使用 LdapUnit 框架的 EmbeddedContextSourceFactory 类创建了一个嵌入式上下文源。嵌入式 LDAP 服务器是 OpenDJ 的一个实例(由属性 serverType 指定),将在端口 12389 上运行。
JUnit 测试用例中的 setup 和 teardown 方法用于加载和删除测试员工数据。employee.ldif 文件包含您将在本书中使用的测试数据。
清单 5-11 。
<?xml version="1.0" encoding="UTF-8"?>
<beans FontName3">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.inflinx.book.ldap" />
<bean id="contextSource" class="org.ldapunit.context.EmbeddedContextSourceFactory">
<property name="port" value="12389" />
<property name="rootDn" value="dc=inflinx,dc=com" />
<property name="base" value="ou=employees,dc=inflinx,dc=com" />
<property name="serverType" value="OPENDJ" />
</bean>
<bean id="ldapTemplate" class="org.springframework.ldap.core.simple.SimpleLdapTemplate">
<constructor-arg ref="contextSource" />
</bean>
</beans>
创建方法
SimpleLdapTemplate 提供了几个向 LDAP 添加条目的绑定方法。要创建新员工,您将使用以下绑定方法变体:
public void bind(DirContextOperations ctx)
此方法将 DirContextOperations 实例作为其参数。bind 方法调用传入的 DirContextOperations 实例上的 getDn 方法,并检索条目的完全限定 Dn。然后,它将所有属性绑定到 DN,并创建一个新条目。
雇员 DAO 中创建方法的实现如清单 5-12 中的所示。如您所见,首先创建一个 DirContextAdapter 的新实例。然后用雇员信息填充上下文的属性。请注意,departmentNumber 的 int 值被显式转换为字符串。如果没有完成这种转换,该方法将最终抛出“org . spring framework . LDAP . invalidattributevalueexception”异常。方法中的最后一行执行实际的绑定。
清单 5-12。
@Override
public void create(Employee employee) {
DistinguishedName dn = new DistinguishedName();
dn.add("uid", employee.getUid());
DirContextAdapter context = new DirContextAdapter();
context.setDn(dn); context.setAttributeValues("objectClass", new String[]
{"top", "person", "organizationalPerson", "inetOrgPerson"});
context.setAttributeValue("givenName", employee.getFirstName());
context.setAttributeValue("surname", employee.getLastName());
context.setAttributeValue("commonName", employee.getCommonName());
context.setAttributeValue("mail", employee.getEmail());
context.setAttributeValue("departmentNumber",
Integer.toString(employee.getDepartmentNumber()));
context.setAttributeValue("employeeNumber", employee.getEmployeeNumber());
context.setAttributeValues("telephoneNumber",employee.getPhone());
ldapTemplate.bind(context);
}
注比较清单 5-12 中的代码和清单 3-10 中的代码。您可以清楚地看到,DirContextAdapter 在简化属性操作方面做得非常好。
让我们用清单 5-13 中的 JUnit 测试用例快速验证一下 create 方法的实现。
清单 5-13。
@Test
public void testCreate() {
Employee employee = new Employee();
employee.setUid("employee1000");
employee.setFirstName("Test");
employee.setLastName("Employee1000");
employee.setCommonName("Test Employee1000");
employee.setEmail("employee1000@inflinx.com" );
employee.setDepartmentNumber(12356);
employee.setEmployeeNumber("45678");
employee.setPhone(new String[]{"801-100-1200"});
employeeDao.create(employee);
}
更新方法
更新条目包括添加、替换或删除其属性。实现这一点的最简单的方法是删除整个条目,并用一组新的属性创建它。这种技术被称为重新绑定。删除和重新创建一个条目显然效率不高,只对更改后的值进行操作更有意义。
在第三章中,您使用了 modifyAttributes 和 ModificationItem 实例来更新 LDAP 条目。尽管 modifyAttributes 是一种不错的方法,但是手动生成 ModificationItem 列表确实需要大量的工作。令人欣慰的是,DirContextAdapter 自动完成了这项工作,使得更新条目变得轻而易举。清单 5-14 显示了使用 DirContextAdapter 实现的更新方法。
清单 5-14。
@Override
public void update(Employee employee) {
DistinguishedName dn = new DistinguishedName();
dn.add("uid", employee.getUid());
DirContextOperations context = ldapTemplate.lookupContext(dn);
context.setAttributeValues("objectClass", new String[] {"top", "person", "organizationalPerson", "inetOrgPerson"});
context.setAttributeValue("givenName", employee.getFirstName());
context.setAttributeValue("surname", employee.getLastName());
context.setAttributeValue("commonName", employee.getCommonName());
context.setAttributeValue("mail", employee.getEmail());
context.setAttributeValue("departmentNumber", Integer.toString(employee.getDepartmentNumber()));
context.setAttributeValue("employeeNumber", employee.getEmployeeNumber());
context.setAttributeValues("telephoneNumber", employee.getPhone());
ldapTemplate.modifyAttributes(context);
}
在这个实现中,您会注意到您首先使用雇员的 DN 查找现有的上下文。然后像在 create 方法中一样设置所有属性。(区别在于 DirContextAdapter 跟踪对条目所做的值更改。)最后,将更新后的上下文传递给 modifyAttributes 方法。modifyAttributes 方法将从 DirContextAdapter 中检索已修改的条目列表,并对 LDAP 中的条目执行这些修改。清单 5-15 显示了更新雇员名字的相关测试用例。
清单 5-15。
@Test
public void testUpdate() {
Employee employee1 = employeeDao.find("employee1");
employee1.setFirstName("Employee New");
employeeDao.update(employee1);
employee1 = employeeDao.find("employee1");
Assert.assertEquals(employee1.getFirstName(),"Employee New");
}
删除方法
Spring LDAP 使用 LdapTemplate/SimpleLdapTemplate 中的 unbind 方法使解除绑定变得简单。清单 5-16 显示了删除一个雇员所涉及的代码。
清单 5-16。
@Override
public void delete(String id) {
DistinguishedName dn = new DistinguishedName();
dn.add("uid", id);
ldapTemplate.unbind(dn);
}
因为您的操作都是相对于初始上下文的,基本是“ou=employees,dc=inflinx,dc=com”,所以您创建的 DN 只有 uid,即条目的 RDN。调用 unbind 操作将删除条目及其所有相关属性。
清单 5-17 显示了验证条目删除的相关测试用例。成功删除条目后,对该名称的任何查找操作都将导致 NameNotFoundException。测试用例验证了这一假设。
清单 5-17。
@Test(expected=org.springframework.ldap.NameNotFoundException.class)
public void testDelete() {
String empUid = "employee11";
employeeDao.delete(empUid);
employeeDao.find(empUid);
}
摘要
在这一章中,你将了解 JNDI 对象工厂的世界。然后您查看了 DefaultDirObjectFactory,Spring LDAP 的对象工厂实现。在本章的剩余部分,您使用 DirContextAdapter 和 SimpleLdapTemplate 实现了一个雇员 DAO。
在下一章中,您将深入 LDAP 搜索和搜索过滤器的世界。