Spring-LDAP-实践教程-全-

Spring LDAP 实践教程(全)

原文:Practical Spring LDAP

协议:CC BY-NC-SA 4.0

零、简介

实用的 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 显示了这种客户端/服务器交互。

9781430263975_Fig01-01.jpg

图 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 条目

员工条目
对象类
给定名称
邮件
jsmith@inflix.com
可动的

image 注意属性名默认不区分大小写。但是,建议在 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 显示了对象继承。

9781430263975_Fig01-02.jpg

图 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 模式。

9781430263975_Fig01-03.jpg

图 1-3 。LDAP 通用模式

像数据库一样,目录模式需要很好地设计,以解决数据冗余等问题。在开始实现您自己的模式之前,有必要看一下几个公开可用的标准模式。这些标准模式通常包含所有的定义来存储所需的数据,更重要的是,确保跨其他目录的互操作性。

命名模型

LDAP 命名模型定义了目录中条目的组织方式。它还决定了如何唯一地标识特定条目。命名模型建议条目以分层的方式进行逻辑存储。这种条目树通常被称为目录信息树(DIT)。图 1-4 提供了一个 通用目录树的例子。

9781430263975_Fig01-04.jpg

图 1-4 。一般曰

树的根通常被称为目录的基或后缀。此条目表示拥有该目录的组织。后缀的格式可以因实施而异,但一般来说,有三种推荐的方法,如图 1-5 中所列。

9781430263975_Fig01-05.jpg

图 1-5 。目录后缀 命名约定

image 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。

9781430263975_Fig01-06.jpg

图 1-6 。组织目录的例子

image 注意识别名不是条目中的实际属性。它只是一个与条目相关联的逻辑名称。

重要的是要记住,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

image 注意通常不鼓励多值 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.txt)定义了 LDAP V3 服务器必须支持的一组认证方法。身份验证成功后,访问控制模型将被查询,以确定客户端是否有足够的权限执行所请求的操作。不幸的是,在访问控制模型方面不存在标准,每个供应商都提供自己的实现。

LDAP 供应商

LDAP 获得了各种供应商的广泛支持。还有一个强大的开源运动来生产 LDAP 服务器。表 1-5 列出了一些流行的目录服务器。

表 1-5 。LDAP 供应商

Tab01-05.jpg

ApacheDS 和 OpenDJ 是 LDAP 目录的纯 Java 实现。在本书中,您将使用这两台服务器对代码进行单元和集成测试。

LDIF 格式

LDAP 数据交换格式(LDIF) 是一种基于文本的标准格式,用于表示目录内容和更新请求。RFC 2849(www.ietf.org/rfc/rfc2849.txt)中定义了 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 目录树。

9781430263975_Fig01-07.jpg

图 1-7 。库曰

在这个目录树中,我使用了 RFC 2247(www.ietf.org/rfc/rfc2247.txt)约定来命名基本条目。基本条目有两个保存雇员和顾客信息的组织单位条目。树的 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 的需要。

9781430263975_Fig02-01.jpg

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

image 注意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 的应用的架构。

9781430263975_Fig03-01.jpg

图 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/community下载。在 www.springsource.org/ldap的 Spring LDAP 网站上有一个直接链接。Spring LDAP 下载页面允许您下载框架的最新版本以及以前的版本,如图图 3-2 所示。

9781430263975_Fig03-02.jpg

图 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 所示,并在你的机器上解压。

9781430263975_Fig03-03.jpg

图 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 网站,【http://maven.apache.org】的提供了关于它的特性的信息以及大量有用的链接。以下是采用 Maven 的一些优势:

  • 标准化的目录结构 : 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 环境变量中。

image 注意 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 所示。

9781430263975_Fig03-04.jpg

图 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 的安装和设置。

  1. Download and initiate the STS installer from the Spring Tool Suite web site at www.springsource.com/developer/sts. 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).

    9781430263975_Fig03-05.jpg

    图 3-5 。安装程序主屏幕

  2. 阅读并接受许可协议,然后单击下一步按钮。

  3. 在目标路径屏幕上,选择安装目录。

  4. Leave the default selection and then click the Next button (see Figure 3-6).

    9781430263975_Fig03-06.jpg

    图 3-6 。安装包

  5. 在下面的屏幕上,提供 JDK 安装的路径,然后单击“下一步”按钮。

  6. 这将开始安装;等待文件传输完成。

  7. 单击下面两个屏幕上的 Next 按钮,完成安装。

使用 STS 创建项目

在前面的“Spring LDAP 原型”一节中,您使用了 practical-ldap-empty-archetype 原型从命令行生成项目。现在让我们看看如何使用 STS 生成同一个项目。

  1. From the File menu, select New arrow.jpg Project. It will launch the New Project wizard (see Figure 3-7). Select the Maven Project option and click the Next button.

    9781430263975_Fig03-07.jpg

    图 3-7 。新项目向导

  2. Uncheck “Use default Workspace location” and enter the path for the newly generated project, and then select the Next button (see Figure 3-8).

    9781430263975_Fig03-08.jpg

    图 3-8 。项目路径设置

  3. 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.

    9781430263975_Fig03-09.jpg

    图 3-9 。原型细节

  4. Enter ldap in the Filter field and select the practical-ldap-empty-archetype. Click the Next button (see Figure 3-10).

    9781430263975_Fig03-10.jpg

    图 3-10 。原型选择

  5. 在接下来的屏幕上,提供关于新创建项目的信息并点击完成按钮(参见图 3-11 )。

9781430263975_Fig03-11.jpg

图 3-11 。项目信息

这将生成一个与您之前看到的目录结构相同的项目。但是,它也会创建所有特定于 IDE 的文件,如。项目和。并将所有依赖的 jar 添加到项目的类路径中。完整的项目结构如图 3-12 所示。

9781430263975_Fig03-12.jpg

图 3-12 。生成的项目结构

LDAP 服务器设置

在这一节中,您将看到如何安装 LDAP 服务器来测试您的 LDAP 代码。在可用的开源 LDAP 服务器中,我发现 OpenDJ 非常容易安装和配置。

image 注意即使你已经有一个可用的测试 LDAP 服务器,我也强烈建议你按照下面的步骤安装 OpenDJ LDAP 服务器。您将大量使用这个实例来测试本书中的代码。

www.forgerock.org/opendj-archive.html下载 OpenDJ 发行版文件 OpenDJ-2.4.6.zip 。将发行版解压缩到本地系统上的一个文件夹中。在我的 Windows box 上,我将提取的文件和文件夹放在 C:\practicalldap\opendj 下。然后按照这些步骤完成安装。

  1. Start the installation by clicking the setup.bat file for Windows. This will launch the install screen.

    image 注意在 Windows 8 下安装时,一定要以管理员身份运行安装程序。否则,在将服务器作为 Windows 服务启用时,您会遇到错误。

  2. 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).

    9781430263975_Fig03-13.jpg

    图 3-13 。LDAP 服务器设置

  3. 在拓扑选项屏幕中,保留“这将是一台独立服务器”选项,并点击下一步按钮。

  4. 在目录数据屏幕中,输入值“dc=inflinx,dc=com”作为目录基本 DN,其他选项保持不变,然后继续。

  5. 在查看屏幕中,确认“将服务器作为 Windows 服务运行”选项已选中,并点击完成按钮。

  6. 您将看到一个确认信息,表明安装成功(参见图 3-14 )。

9781430263975_Fig03-14.jpg

图 3-14 。成功的 OpenDJ 确认

由于您已经将 OpenDJ 安装为 Windows 服务,您可以通过转到控制面板arrow.jpg管理工具arrow.jpg服务并选择 OpenDJ 并单击开始来启动 LDAP 服务器(图 3-15 )。

9781430263975_Fig03-15.jpg

图 3-15 。将 OpenDJ 作为 Windows 服务运行

image 注意如果你没有安装 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.

工作室安装可以通过以下步骤完成。

  1. 在 Windows 上,双击安装文件开始安装(这将显示安装屏幕)。

  2. 阅读并接受许可协议以继续。

  3. Choose your preferred installation directory, and select “Install” (see Figure 3-16).

    9781430263975_Fig03-16.jpg

    图 3-16 。Apache 安装目录选择

  4. 您将看到安装和文件传输的状态。

  5. 传输完所有文件后,单击“完成”按钮完成安装。

安装完成后,下一步是创建到新安装的 OpenDJ LDAP 服务器的连接。在继续之前,请确保您的 OpenDJ 服务器正在运行。下面是建立新连接的步骤。

  1. 启动 ApacheDS 服务器。在 Windows 中,单击 Apache 目录 Studio.exe 文件。

  2. Launch the New Connection wizard by right-clicking in the “Connections” section and selecting “New Connection.”

    9781430263975_Fig03-17.jpg

    图 3-17。创建新连接

  3. On the Network Parameter screen, enter the information displayed in Figure 3-18. This should match the OpenDJ information you entered during OpenDJ installation.

    9781430263975_Fig03-18.jpg

    图 3-18 。LDAP 连接网络参数

  4. On the Authentication screen, enter “cn=Directory Manager” as Bind DN or user and “opendj” as password (see Figure 3-19).

    9781430263975_Fig03-19.jpg

    图 3-19 。LDAP 连接认证

  5. 接受浏览器选项部分的默认值,并选择完成按钮。

加载测试数据

在前面的小节中,您安装了 OpenDJ LDAP 服务器和 Apache Directory Studio 来访问 LDAP 服务器。设置开发/测试环境的最后一步是用测试数据加载 LDAP 服务器。

image 注意随附的源代码/下载包含两个 LDIF 文件,customers . ldif 和 employees . ldif。customers . ldif 文件包含模拟您的库的客户的测试数据。employees.ldif 文件包含模拟库雇员的测试数据。这两个文件大量用于测试本书中使用的代码。如果您还没有完成,请在继续之前下载这些文件。

下面是加载测试数据的步骤。

  1. Right-click “Root DSE” in the LDAP browser pane and select Import arrow.jpg LDIF Import (see Figure 3-20).

    9781430263975_Fig03-20.jpg

    图 3-20 。LDIF 进口

  2. 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.

    9781430263975_Fig03-21.jpg

    图 3-21 。LDIF 导入设置

  3. 成功导入后,您将看到 dc=inflinx,dc=com 条目下加载的数据(参见图 3-22 )。

9781430263975_Fig03-22.jpg

图 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 服务器的需求。

image 注这里讨论的概念是 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 的最新版本可以从 http://easymock.org/Downloads.html 下载。为了模仿具体的类,需要两个额外的库,即 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 个测试条目之外,该脚本还创建了两个额外的基本条目。

9781430263975_Fig04-01.jpg

图 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 中检索条目。这些结果然后被传递给注册的对象工厂,后者将它们转换成对象。这些对象被移交给应用。

9781430263975_Fig05-01.jpg

图 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 实现通常包括以下内容:

  1. 一个提供 CRUD 方法契约的 DAO 接口。
  2. 使用特定于数据源的 API 的接口的具体实现。
  3. 由 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);
}

image 比较清单 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 搜索和搜索过滤器的世界。

六、搜索 LDAP

在本章中,您将学习

  • LDAP 搜索的基础
  • 使用过滤器的 LDAP 搜索
  • 创建自定义搜索过滤器

搜索信息是对 LDAP 执行的最常见的操作。客户端应用通过传递搜索标准来启动 LDAP 搜索,搜索标准是决定在哪里搜索和搜索什么的信息。收到请求后,LDAP 服务器执行搜索并返回所有符合条件的条目。

LDAP 搜索标准

LDAP 搜索标准由三个强制参数(基本、范围和过滤器)和几个可选参数组成。让我们详细看看这些参数。

基本参数

搜索的基本部分是标识将被搜索的树的分支的可分辨名称(DN)。例如,基数“ou =顾客,dc=inflinx,dc=com”表示搜索将从顾客分支开始并向下移动。也可以指定一个空的基,这将导致搜索根 DSE 条目。

image 注意根 DSE 或 DSA 特定条目是 LDAP 服务器中的一个特殊条目。它通常保存特定于服务器的数据,如供应商名称、供应商版本以及它支持的不同控件和功能。

范围参数

scope 参数确定需要执行的 LDAP 搜索相对于基准的深度。LDAP 协议定义了三种可能的搜索范围:基本、一级和子树。图 6-1 显示了在不同搜索范围内被评估的条目。

9781430263975_Fig06-01.jpg

图 6-1 。搜索范围

  • 基本范围将搜索限制到由基本参数标识的 LDAP 条目。搜索中不会包含其他条目。在您的库应用模式中,使用基本 DN dc=inflinx,dc=com 和基本范围,搜索将只返回根组织条目,如图 6-1 所示。

一级范围表示搜索直接在基础下一级的所有条目。搜索中不包括基本条目本身。因此,使用 base dc=inflinx,dc=com 和 scope one 级别,搜索所有条目将返回雇员和顾客组织单位。

最后,子树范围包括搜索中的基本条目及其所有后代条目。这是三个选项中最慢、最贵的一个。在您的库示例中,使用这个范围和 base dc=inflinx,dc=com 进行搜索将返回所有条目。

过滤参数

在您的图书馆应用 LDAP 服务器中,假设您想要查找住在 Midvale 地区的所有顾客。从 LDAP 模式中,您知道 patron 条目具有 city 属性,该属性保存他们居住的城市名称。所以这个需求本质上可以归结为检索所有具有值为“Midvale”的 city 属性的条目。这正是搜索过滤器的作用。搜索过滤器定义了所有返回条目拥有的特征。从逻辑上讲,过滤器应用于由 base 和 scope 标识的集合中的每个条目。只有与过滤器匹配的条目才成为返回的搜索结果的一部分。

LDAP 搜索过滤器由三部分组成:属性类型、操作符和属性值(或值的范围)。根据运算符的不同,值部分可以是可选的。这些组件必须始终用括号括起来,就像这样:

Filter =  (attributetype  operator value)

有了这些信息,查找住在 Midvale 的所有顾客的搜索过滤器应该是这样的:

(city=Midvale)

现在,假设你想找到所有住在 Midvale 地区的顾客,他们都有一个电子邮件地址,这样你就可以给他们发送一些图书馆活动的新闻。结果搜索过滤器实际上是两个过滤器项目的组合:一个项目标识 Midvale 市的顾客,另一个项目标识有电子邮件地址的顾客。您已经看到了过滤器的第一项。这是过滤器的另一部分:

(mail=*)

=运算符指示属性的存在。因此,表达式 mail=将返回所有在邮件属性中有值的条目。LDAP 规范定义了可用于组合多个过滤器和创建复杂过滤器的过滤器操作符。以下是组合过滤器的格式:

Filter =  (operator filter1 filter2)

注意前缀符号的使用,其中运算符写在操作数之前,用于组合两个过滤器。以下是您的用例所需的过滤器:

(&(city=Midvale)(mail=*))

这个过滤器中的&是一个操作符。LDAP 规范定义了各种搜索过滤器操作符。表 6-1 列出了一些常用的运算符。

表 6-1 。搜索过滤运算符

Tab06-01.jpg

可选参数

除了上述三个参数之外,还可以包括几个可选参数来控制搜索行为。例如,timelimit 参数指示允许完成搜索的时间。类似地,sizelimit 参数对可以作为结果的一部分返回的条目数量设置了上限。

一个非常常用的可选参数包括提供属性名列表。执行搜索时,缺省情况下,LDAP 服务器会返回与搜索中找到的条目相关联的所有属性。有时这可能并不理想。在这些场景中,您可以提供一个属性名称列表作为搜索的一部分,LDAP 服务器将只返回具有这些属性的条目。下面是 LdapTemplate 中的一个搜索方法示例,它采用一个属性名称数组(ATTR_1、ATTR_2 和 ATTR_3):

ldapTemplate.search("SEARCH_BASE", "uid=USER_DN", 1, new String[]{"ATTR_1", "ATTR_2", ATTR_3}, new SomeContextMapperImpl());

执行此搜索时,返回的条目将只有 ATTR_1、ATTR_2 和 ATTR_3。这可以减少从服务器传输的数据量,在高流量情况下非常有用。

从版本 3 开始,LDAP 服务器可以维护每个条目的属性,这完全是出于管理目的。这些属性被称为操作属性,不是条目对象类的一部分。执行 LDAP 搜索时,默认情况下返回的条目将不包含操作属性。为了检索操作属性,您需要在搜索条件中提供操作属性名称列表。

image 注意操作属性的例子包括 createTimeStamp 和 pwdAccountLockedTime,前者保存条目创建的时间,后者记录用户帐户被锁定的时间。

LDAP 注入

LDAP 注入是一种技术,攻击者通过改变 LDAP 查询来对目录服务器运行任意 LDAP 语句。LDAP 注入可能导致未经授权的数据访问或对 LDAP 树的修改。不执行正确的输入验证或清理输入的应用容易受到 LDAP 注入。这种技术类似于流行的针对数据库的 SQL 注入攻击。

为了更好地理解 LDAP 注入,考虑一个使用 LDAP 进行身份验证的 web 应用。这种应用通常提供一个网页,让用户输入自己的用户名和密码。为了验证用户名和密码是否匹配,应用将构建一个 LDAP 搜索查询,大致如下所示:

(&(uid =用户输入 UID)(密码=用户输入 PWD))

让我们假设应用简单地信任用户输入,并且不执行任何验证。现在,如果您输入文本 jdoe)(&)(作为用户名,并输入任意随机文本作为密码,则搜索查询过滤器将如下所示:

(&(uid=jdoe)(&)(密码=随机)

如果用户名 jdoe 是 LDAP 中的一个有效用户 id,那么不管输入的密码是什么,该查询将始终计算为 true。这种 LDAP 注入将允许攻击者绕过身份验证进入应用。www . black hat . com/presentations/BH-Europe-08/alon so-Parada/white paper/BH-eu-08-alon so-Parada-WP . pdf 上的“LDAP 注入和盲 LDAP 注入”文章详细讨论了各种 LDAP 注入技术。

一般来说,防止 LDAP 注入和任何其他注入技术都是从正确的输入验证开始的。在搜索过滤器中使用输入的数据之前,对其进行净化和正确编码是非常重要的。

Spring LDAP 过滤器

在上一节中,您了解了 LDAP 搜索过滤器对于缩小搜索范围和识别条目非常重要。然而,动态创建 LDAP 过滤器可能会很繁琐,尤其是在尝试组合多个过滤器时。确保所有的大括号都正确闭合是容易出错的。适当转义特殊字符也很重要。

Spring LDAP 提供了几个过滤器类,使得创建和编码 LDAP 过滤器变得容易。所有这些过滤器都实现了过滤器接口,并且是 org . spring framework . LDAP . Filter 包的一部分。清单 6-1 显示了过滤器 API 接口。

清单 6-1。

package org.springframework.ldap.filter;

public interface Filter {
   String encode();
   StringBuffer encode(StringBuffer buf);
   boolean equals(Object o);
   int hashCode();
}

该接口中的第一个编码方法返回过滤器的字符串表示。第二个 encode 方法接受 StringBuffer 作为其参数,并将过滤器的编码版本作为 StringBuffer 返回。对于常规的开发过程,您使用返回 String 的 encode 方法的第一个版本。

过滤界面层次如图图 6-2 所示。从层次结构中,您可以看到 AbstractFilter 实现了过滤器接口,并作为所有其他过滤器实现的根类。BinaryLogicalFilter 是二进制逻辑运算(如 AND 和 or)的抽象超类。CompareFilter 是过滤器的抽象超类,用于比较 EqualsFilter 和 LessThanOrEqualsFilter 等值。

9781430263975_Fig06-02.jpg

图 6-2 。过滤器层次结构

image 注意默认情况下,大多数 LDAP 属性值在搜索时不区分大小写。

在接下来的章节中,你将会看到图 6-2 中的每一个过滤器。在此之前,让我们创建一个可重用的方法来帮助您测试您的过滤器。清单 6-2 显示了 searchAndPrintResults 方法,它使用传入的过滤器实现参数并使用它执行搜索。然后,它将搜索结果输出到控制台。注意,您将搜索 LDAP 树的 Patron 分支。

清单 6-2。

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.simple.AbstractParameterizedContextMapper;
import org.springframework.ldap.core.simple.SimpleLdapTemplate;
import org.springframework.ldap.filter.Filter;
import org.springframework.stereotype.Component;

@Component("searchFilterDemo" )
public class SearchFilterDemo {

   @Autowired
   @Qualifier("ldapTemplate" )
   private SimpleLdapTemplate ldapTemplate;

   public void searchAndPrintResults(Filter filter) {
      List<String> results = ldapTemplate.search("ou=patrons,dc=inflinx,dc=com", filter.encode(),
             new AbstractParameterizedContextMapper<String>() {
            @Override
            protected String doMapFromContext(DirContextOperations context) {
              return context.getStringAttribute("cn");
            }
          });

       System.out.println("Results found in search: " + results.size());
         for(String commonName: results) {
            System.out.println(commonName);
         }
       }
   }
}

等于过滤器

EqualsFilter 可用于检索具有指定属性和值的所有条目。假设您想要检索名字为 Jacob 的所有顾客。为此,您需要创建一个新的 EqualsFilter 实例。

EqualsFilter filter =  new  EqualsFilter("givenName", "Jacob");

构造函数的第一个参数是属性名,第二个参数是属性值。对此过滤器调用 encode 方法会产生字符串(givenName=Jacob)。

清单 6-3 显示了调用 searchAndPrintResults 的测试用例,上面的 EqualsFilter 作为参数。该方法的控制台输出也显示在清单中。注意,结果中有名字为 jacob 的顾客(注意小写的 j)。这是因为 sn 属性和大多数 LDAP 属性一样,在模式中被定义为不区分大小写。

清单 6-3。

@Test
public void testEqualsFilter() {
   Filter filter = new EqualsFilter("givenName", "Jacob");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  2
Jacob  Smith
jacob  Brady

lieffilter〔??〕

当只知道属性的一部分值时,LikeFilter 对于搜索 LDAP 很有用。LDAP 规范允许使用通配符*来描述这些部分值。假设您想要检索名字以“Ja”开头的所有用户为此,创建 LikeFilter 的一个新实例,并将通配符子字符串作为属性值传入。

LikeFilter  filter =  new  LikeFilter("givenName", "Ja*");

在这个过滤器上调用 encode 方法会产生字符串(givenName=Ja*)。清单 6-4 显示了使用 LikeFilter 调用 searchAndPrintResults 方法的测试用例及结果。

清单 6-4。

@Test
public void testLikeFilter() {
   Filter filter = new LikeFilter("givenName", "Ja*");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  3
Jacob Smith
Jason Brown
jacob  Brady

子字符串中的通配符*用于匹配零个或多个字符。然而,了解 LDAP 搜索过滤器不支持正则表达式是非常重要的。表 6-2 列出了一些子串示例。

表 6-2 。LDAP 子字符串示例

LDAP 子字符串 描述
(givenName=*son) 匹配所有名字以 son 结尾的顾客。
(给定名称 = J *n) 匹配名字以 J 开头以 n 结尾的所有顾客。
(给定名称=a) 匹配名字中包含字符 a 的所有顾客。
(给定名称=Jsn) 匹配名字以 J 开头、包含字符 s 并以 n 结尾的顾客。

您可能想知道 LikeFilter 的必要性,因为您可以通过简单地使用 EqualsFilter 来完成相同的筛选表达式,如下所示:

EqualsFilter filter =  new  EqualsFiler("uid", "Ja*");

在这种情况下使用 EqualsFilter 不起作用,因为 EqualsFilter 中的 encode 方法将 Ja中的通配符视为特殊字符,并正确地对其进行转义。因此,当用于搜索时,上面的过滤器将产生名字以 Ja*开头的所有条目。

演示过滤器

PresentFilters 对于检索在给定属性中至少有一个值的 LDAP 条目很有用。考虑前面的场景,您希望检索所有拥有电子邮件地址的顾客。为此,您需要创建一个 PresentFilter,如下所示:

PresentFilter presentFilter =  new  PresentFilter("email");

在 presentFilter 实例上调用 encode 方法会产生字符串(email=*)。清单 6-5 显示了使用上面的 presentFilter 调用 searchAndPrintResults 方法时的测试代码和结果。

清单 6-5。

@Test
public void testPresentFilter() {
   Filter filter = new PresentFilter("mail");
   searchFilterDemo.searchAndPrintResults(filter);
}
Results  found in  search:  97
Jacob  Smith
Aaren  Atp
Aarika  Atpco
Aaron Atrc
Aartjan  Aalders
Abagael  Aasen
Abagail  Abadines
.........
.........

notes present filter

NotPresentFilters 用于检索没有指定属性的条目。条目中没有任何值的属性被视为不存在。现在,假设您想要检索所有没有电子邮件地址的顾客。为此,创建 NotPresentFilter 的一个实例,如下所示:

NotPresentFilter notPresentFilter  =  new NotPresentFilter("email");

notPresentFilter 的编码版本产生表达式!(邮箱=*)。运行 searchAndPrintResults 会产生如清单 6-6 所示的输出。第一个空值用于组织单位条目“ou = customers,dc=inflinx,dc=com”。

清单 6-6。

@Test
public void testNotPresentFilter() {
   Filter filter = new NotPresentFilter("mail");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  5
null
Addons Achkar
Adeniyi Adamowicz
Adoree Aderhold
Adorne  Adey

不过滤

NotFilter 对于检索与给定条件不匹配的条目很有用。在“LikeFilter”一节中,您看到了检索所有以 Ja 开头的条目。现在假设您想要检索所有不以 Ja 开头的条目。这就是 NotFilter 发挥作用的地方。下面是实现这一要求的代码:

NotFilter notFilter  =  new  NotFilter(new LikeFilter("givenName", "Ja*"));

对该过滤器进行编码会产生字符串!(givenName=Ja*)。如您所见,NotFilter 只是添加了否定符号(!)传递给传递给其构造函数的筛选器。调用 searchAndShowResults 方法会产生清单 6-7 中的输出。

清单 6-7。

@Test
public void testNotFilter() {
   NotFilter notFilter = new NotFilter(new LikeFilter("givenName", "Ja*"));
   searchFilterDemo.searchAndPrintResults(notFilter);
}
Results  found in  search:  99
Aaren Atp  Aarika
Atpco Aaron Atrc
Aartjan  Aalders
Abagael  Aasen
Abagail  Abadines
.........................

也可以将 NotFilter 和 PresentFilter 组合起来创建与 NotPresentFilter 等效的表达式。下面是一个新的实现,它获取所有没有电子邮件地址的条目:

NotFilter notFilter  =  new  NotFilter(new PresentFilter("email"));

greater than requialsfilter〔??〕

GreaterThanOrEqualsFilter 对于匹配所有在字典上等于或大于给定属性值的条目非常有用。例如,可以使用搜索表达式(给定名称> = Jacob)来检索除 Jacob 之外按字母顺序位于 Jacob 之后的给定名称的所有条目。清单 6-8 显示了这个实现以及输出结果。

清单 6-8。

@Test
public void testGreaterThanOrEqualsFilter() {
   Filter filter = new GreaterThanOrEqualsFilter("givenName", "Jacob");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  3
Jacob Smith
jacob Brady
Jason Brown

lesthanorequalfilter〔??〕牌

LessThanOrEqualsFilter 可用于匹配在字典上等于或低于给定属性的条目。因此,搜索表达式(givenName <=Jacob) will return all entries with first name alphabetically lower or equal to Jacob. 清单 6-9 显示了调用该需求的 searchAndPrintResults 实现的测试代码以及输出。

清单 6-9。

@Test
public void testLessThanOrEqualsFilter() {
   Filter filter = new LessThanOrEqualsFilter("givenName", "Jacob");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  100
Jacob  Smith
Aaren  Atp
Aarika  Atpco
Aaron Atrc
Aartjan  Aalders
Abagael  Aasen
Abagail  Abadines
Abahri Abazari
....................

如上所述,搜索包括名字为 James 的条目。LDAP 规范不提供小于(

NotFilter lessThanFilter = new NotFilter(new GreaterThanOrEqualsFilter("givenName", "James"));

安过滤器〔??〕

AndFilter 用于组合多个搜索过滤器表达式,以创建复杂的搜索过滤器。结果过滤器将匹配满足所有子过滤器条件的条目。例如,AndFilter 适合于实现一个更早的要求,即获取所有居住在 Midvale 地区并有电子邮件地址的顾客。以下代码显示了这种实现:

AndFilter andFilter  =  new  AndFilter();
andFilter.and(new EqualsFilter("postalCode",  "84047"));
andFilter.and(new PresentFilter("email"));

在这个过滤器上调用 encode 方法会产生(&(city=Midvale)(email=*))。清单 6-10 显示了创建 AndFilter 并调用 searchAndPrintResults 方法的测试用例。

清单 6-10。

@Test
public void testAndFilter() {
   AndFilter andFilter = new AndFilter();
   andFilter.and(new EqualsFilter("postalCode", "84047"));
   andFilter.and(new PresentFilter("mail"));
   searchFilterDemo.searchAndPrintResults(andFilter);
}

Results  found in  search:  1
Jacob  Smith

orf filter〔??〕

和 AndFilter 一样,OrFilter 可以用来组合多个搜索筛选器。但是,结果过滤器将匹配满足任何子过滤器条件的条目。下面是 OrFilter 的一个实现:

OrFilter orFilter  =  new  OrFilter();
orFilter.add(new EqualsFilter("postalcode",  "84047"));
orFilter.add(new EqualsFilter("postalcode",  "84121"));

这个 OrFilter 将检索所有居住在 84047 或 84121 邮政编码的顾客。encode 方法返回表达式(|(postal code = 84047)(postal code = 84121))。OrFilter 的测试用例如清单 6-11 所示。

清单 6-11。

@Test
public void testOrFilter() {
   OrFilter orFilter = new OrFilter();
   orFilter.or(new EqualsFilter("postalCode", "84047"));
   orFilter.or(new EqualsFilter("postalCode", "84121"));
   searchFilterDemo.searchAndPrintResults(orFilter);
}

Results  found in  search:  2
Jacob  Smith
Adriane  Admin-mtv

硬编码过滤器

HardcodedFilter 是一个方便的类,它使得在构建搜索过滤器时添加静态过滤器文本变得容易。假设您正在编写一个允许管理员在文本框中输入搜索表达式的管理应用。如果要将此表达式与其他筛选器一起用于搜索,可以使用 HardcodedFilter,如下所示:

AndFilter filter  =  new  AndFilter();
filter.add(new HardcodedFilter(searchExpression));
filter.add(new EqualsFilter("givenName", "smith"));

在这段代码中,searchExpression 变量包含用户输入的搜索表达式。当搜索过滤器的静态部分来自属性文件或配置文件时,HardcodedFilter 也非常方便。记住这个过滤器不对传入的文本进行编码是很重要的。所以请谨慎使用,尤其是直接处理用户输入的时候。

white space wild cardfilter

WhitespaceWildcardsFilter 是另一个方便的类,它使得创建子字符串搜索过滤器更加容易。像它的超类 EqualsFilter 一样,这个类接受一个属性名和值。然而,顾名思义,它将属性值中的所有空格都转换为通配符。考虑以下示例:

WhitespaceWildcardsFilter filter = new WhitespaceWildcardsFilter("cn", "John Will");

该过滤器产生以下表达式:(cn=JohnWill*)。在开发搜索和查找应用时,此过滤器会很有用。

创建自定义过滤器

尽管 Spring LDAP 提供的过滤器类在大多数情况下已经足够了,但是可能会出现当前设置不够的情况。谢天谢地,Spring LDAP 使得创建新的过滤器类变得很容易。在本节中,您将看到如何创建一个定制的近似过滤器。

近似过滤器用于检索属性值大约等于指定值的条目。近似表达式使用∾=运算符创建。因此,( given name∞= Adeli)过滤器将匹配名字为 Adel 或 Adele 的条目。当用户在搜索时不知道值的实际拼写时,近似筛选器在搜索应用中非常有用。查找发音相似值的算法的实现因 LDAP 服务器实现的不同而不同。

Spring LDAP 不提供任何现成的类来创建近似过滤器。在清单 6-12 中,你创建了这个过滤器的一个实现。注意,ApproximateFilter 类扩展了 AbstractFilter。构造函数被定义为接受属性类型和属性值。在 encode 方法中,通过连接属性类型、运算符和值来构造过滤器表达式。

清单 6-12。

import org.springframework.ldap.filter.AbstractFilter;

private class ApproximateFilter extends AbstractFilter {

   private static final String APPROXIMATE_SIGN = "∼=";
   private String attribute;
   private String value;

   public ApproximateFilter(String attribute, String value) {
      this.attribute = attribute;
      this.value = value;
   }

   @Override
   public StringBuffer encode(StringBuffer buff) {
      buff.append('(');
      buff.append(attribute).append(APPROXIMATE_SIGN).append(value);
      buff.append(')');

          return buff;
   }
}

清单 6-13 显示了使用 ApproximateFilter 类运行 searchAndPrintResults 方法的测试代码。

清单 6-13。

@Test
public void testApproximateFilter() {
   ApproximateFilter approx = new ApproximateFilter("givenName", "Adeli");
   searchFilterDemo.searchAndPrintResults(approx);
}

下面是运行测试用例的输出:

Results  found in  search:  6
Adel  Acker
Adela Acklin
Adele Acres
Adelia  Actionteam
Adella  Adamczyk
Adelle Adamkowski

处理特殊字符

有时候,您需要使用在 LDAP 中有特殊含义的字符(或 a *)来构建搜索过滤器。为了成功地执行这些过滤器,正确地对特殊字符进行转义是很重要的。转义使用格式\xx 完成,其中 xx 表示字符的十六进制表示。表 6-3 列出了所有特殊字符及其转义值。

表 6-3 。特殊字符和转义值

特殊字符 逸出值
\28
) \29
* \2a
\ \5c
/ \2f

除了上述字符之外,如果在 DN 中使用了以下任何字符,也需要对它们进行适当的转义:逗号(,)、等号(=)、加号(+)、小于()、井号(#)和分号(;).

摘要

在本章中,您学习了如何使用搜索过滤器简化 LDAP 搜索。我以 LDAP 搜索概念的概述开始了这一章。然后,您查看了不同的搜索过滤器,您可以使用这些过滤器以各种方式检索数据。您还看到了 Spring LDAP 如何使创建定制搜索过滤器变得容易。

在下一章中,您将看到从 LDAP 服务器获得的结果的排序和分页。

七、排序和分页结果

在本章中,您将学习

  • LDAP 控件的基础。
  • 对 LDAP 结果进行排序。
  • 分页 LDAP 结果。

LDAP 控件

LDAP 控制提供了一种标准化的方法来修改 LDAP 操作的行为。控件可以简单地看作是客户端发送给 LDAP 服务器的消息(反之亦然)。作为客户端请求的一部分发送的控件可以向服务器提供附加信息,指示应该如何解释和执行操作。例如,可以在 LDAP 删除操作中指定删除子树控件。收到删除请求后,LDAP 服务器的默认行为是删除条目。但是,当 delete subtree 控件附加到 delete 请求时,服务器会自动删除该条目及其所有从属条目。这种控制被称为请求控制 。

LDAP 服务器也可以将控制作为其响应消息的一部分发送,以指示操作是如何处理的。例如,LDAP 服务器可能会在绑定操作期间返回密码策略控制,指示客户端的密码已经过期或即将过期。由服务器发送的这种控制被称为响应控制 。可以随操作一起发送任意数量的请求或响应控制。

LDAP 控制,包括请求和响应,由以下三部分组成:

  • 唯一标识控件的对象标识符(OID)。这些 oid 防止控件名称之间的冲突,通常由创建控件的供应商定义。这是控件的必需组件。
  • 指明控制对于操作是关键还是非关键。这也是一个必需组件,可以是真或假。
  • 特定于控件的可选信息。例如,用于分页搜索结果的分页控件需要页面大小来确定页面中要返回的条目数。

RFC 2251(www.ietf.org/rfc/rfc2251.txt)中规定的 LDAP 控件的正式定义如图 7-1 中的所示。然而,这个 LDAP 规范没有定义任何具体的控制。控制定义通常由 LDAP 供应商提供,它们的支持因服务器而异。

9781430263975_Fig07-01.jpg

图 7-1 。LDAP 控制规范

当 LDAP 服务器在操作中接收控件时,其行为取决于控件及其相关信息。图 7-2 中的流程图显示了接收请求控制时的服务器行为。

9781430263975_Fig07-02.jpg

图 7-2 。LDAP 服务器控制交互

一些通常支持的 LDAP 控件及其 OID 和描述在表 7-1 中显示。

表 7-1 。常用控件

控件名称 似…的 说明(RFC)
分类控制 1.2.840.113556.1.4.473 请求服务器在将搜索结果发送给客户端之前对它们进行排序。这是 RFC 2891 的一部分。
分页结果控制 1.2.840.113556.1.4.319 请求服务器在包含指定数量条目的页面中返回搜索结果。只允许搜索结果的顺序迭代。这被定义为 RFC 2696 的一部分。
子树删除控件 1.2.840.113556.1.4.805 请求服务器删除该条目及其所有后代条目。
虚拟列表视图控件 2.16.840.1.113730.3.4.9 这类似于页面搜索结果,但允许客户端请求任意条目子集。在因特网草案文件 VLV 04 中描述了这种控制。
密码策略控制 1.3.6.1.4.1.42.2.27.8.5.1 服务器发送的控件,保存有关由于密码策略问题(如密码需要重置、帐户已被锁定或密码已过期或即将过期)而导致的失败操作(如身份验证)的信息。
管理 DSA/IT 控制 2.16.840.1.113730.3.4.2 请求服务器将“ref”属性条目(引用)视为常规 LDAP 条目。
持续搜索控制 2.16.840.1.113730.3.4.3 此控件允许客户端接收 LDAP 服务器中与搜索条件匹配的条目的更改通知。

识别支持的控件

在使用特定控件之前,确保您使用的 LDAP 服务器支持该控件是很重要的。LDAP 规范要求每个符合 LDAP v3 的服务器在根 DSA 特定条目 (DSE)的 supportedControl 属性中发布所有支持的控件。因此,在根 DSE 条目中搜索 supportedControl 属性将列出所有控件。清单 7-1 显示了连接到运行在端口 11389 上的 OpenDJ 服务器并将控制列表打印到控制台的代码。

清单 7-1

package com.inflinx.book.ldap;

import java.util.Properties;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

public class SupportedControlApplication {

   public void displayControls() {

      String ldapUrl = "ldap://localhost:11389";
      try {
         Properties environment = new Properties();
         environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
         environment.setProperty(DirContext.PROVIDER_URL,ldapUrl);
         DirContext context = new InitialDirContext(environment);
         Attributes attributes = context.getAttributes("", new String[]{"supportedcontrol"});
         Attribute supportedControlAttribute = attributes.get("supportedcontrol");
         NamingEnumeration controlOIDList = supportedControlAttribute.getAll();
         while(controlOIDList != null && controlOIDList.hasMore()) {
            System.out.println(controlOIDList.next());
         }
         context.close();
      }
      catch(NamingException e) {
         e.printStackTrace();
      }
   }

   public static void main(String[] args) throws NamingException {
      SupportedControlApplication supportedControlApplication = new SupportedControlApplication();
      supportedControlApplication.displayControls();
   }
}

下面是运行清单 7-1 中的代码后的输出:

1.2.826.0.1.3344810.2.3
1.2.840.113556.1.4.1413
1.2.840.113556.1.4.319
1.2.840.113556.1.4.473
1.2.840.113556.1.4.805
1.3.6.1.1.12
1.3.6.1.1.13.1
1.3.6.1.1.13.2
1.3.6.1.4.1.26027.1.5.2
1.3.6.1.4.1.42.2.27.8.5.1
1.3.6.1.4.1.42.2.27.9.5.2
1.3.6.1.4.1.42.2.27.9.5.8
1.3.6.1.4.1.4203.1.10.1
1.3.6.1.4.1.4203.1.10.2
2.16.840.1.113730.3.4.12
2.16.840.1.113730.3.4.16
2.16.840.1.113730.3.4.17
2.16.840.1.113730.3.4.18
2.16.840.1.113730.3.4.19
2.16.840.1.113730.3.4.2
2.16.840.1.113730.3.4.3
2.16.840.1.113730.3.4.4
2.16.840.1.113730.3.4.5
2.16.840.1.113730.3.4.9

OpenDJ 安装提供了一个命令行 ldapsearch 工具,也可以用来列出支持的控件。假设 OpenDJ 安装在 Windows 的 c:\practicalldap\opendj 下,下面是获取支持的控件列表的命令:

ldapsearch --baseDN "" --searchScope base --port 11389 "(objectclass=*)" supportedControl

图 7-3 显示了运行该命令的结果。请注意,为了搜索根 DSE,您使用了作用域 base,但没有提供基本 DN。此外,图中支持的控件 oid 与运行清单 7-1 中的 Java 代码后收到的 oid 相匹配。

9781430263975_Fig07-03.jpg

图 7-3 。OpenDJ ldapsearch 命令

JNDI 和控制和

JNDI API 中的 javax.naming.ldap 包包含对 LDAP V3 特定特性的支持,比如控件和扩展操作。当控制修改或增加现有操作的行为时,扩展操作允许定义额外的操作。图 7-4 中的 UML 图突出了 javax.naming.ldap 包中一些重要的控件类。

9781430263975_Fig07-04.jpg

图 7-4 。Java LDAP 控件类层次结构

javax.naming.ldap.Control 接口为请求和响应控件提供了抽象。此接口的几个实现(如 SortControl 和 PagedResultsControl)作为 JDK 的一部分提供。其他控件,如 Virtual- ListViewControl 和 PasswordExpiringResponseControl,可作为 LDAP booster pack 的一部分。

javax.naming.ldap 包中的核心组件是 LdapContext 接口。该接口扩展了 javax.naming.DirContext 接口,并提供了执行 LDAP V3 操作的其他方法。javax.naming.ldap 包中的 InitialLdapContext 类提供了该接口的具体实现。

在 JNDI API 中使用控件非常简单。清单 7-2 中的代码提供了使用控件的算法。

清单 7-2

   LdapContext context = new InitialLdapContext();
   Control[] requestControls = // Concrete control instance array
   context.setRequestControls(requestControls);
   /* Execute a search operation using the context*/
   context.search(parameters);
   Control[] responseControls = context.getResponseControls();
   // Analyze the response controls

在该算法中,首先创建希望包含在请求操作中的控件的实例。然后执行操作并处理操作的结果。最后,分析服务器发送的任何响应控制。在接下来的部分中,您将看到该算法与排序和分页控件的具体实现。

Spring LDAP 和控件

当使用 LdapTemplate 的搜索方法时,Spring LDAP 不提供对目录上下文的访问。因此,您无法将请求控件添加到上下文或流程响应控件中。为了解决这个问题,Spring LDAP 提供了一个目录上下文处理器,可以自动向上下文添加和分析 LDAP 控件。清单 7-3 显示了 DirContextProcessor API 代码。

清单 7-3

package org.springframework.ldap.core;

import javax.naming.NamingException;
import javax.naming.directory.DirContext;

public interface DirContextProcessor {
   void preProcess(DirContext ctx) throws NamingException;
   void postProcess(DirContext ctx) throws NamingException;
}

DirContextProcessor 接口的具体实现被传递给 LdapTemplate 的搜索方法。在执行搜索之前调用预处理方法。因此,具体的实现将在预处理方法中包含逻辑,以将请求控制添加到上下文中。执行搜索后将调用后处理方法。因此,具体的实现将在后处理方法中有逻辑来读取和分析 LDAP 服务器发送的任何响应控制。

图 7-5 显示了 DirContextProcessor 及其所有实现的 UML 表示。

9781430263975_Fig07-05.jpg

图 7-5 。DirContextProcessor 类层次结构

AbstractRequestControlDirContextProcessor 实现 DirContextProcessor 的预处理方法,并在 LdapContext 上应用单个 RequestControl。AbstractRequestDirContextProcessor 通过 createRequestControl 模板方法将请求控件的实际创建委托给子类。

AbstractFallbackRequestAndResponseControlDirContextProcessor 类扩展了 AbstractRequestControlDirContextProcessor,并大量使用反射来自动化 DirContext 处理。它执行加载控件类、创建它们的实例以及将它们应用到上下文的任务。它还负责响应控件的大部分后处理,将模板方法委托给执行实际值检索的子类。

PagedResultsDirContextProcessor 和 SortControlDirContextProcessor 用于管理分页和排序控件。在接下来的部分中,您将会看到它们。

分类控制

sort 控件提供了一种机制,请求 LDAP 服务器在将搜索结果发送给客户机之前对它们进行排序。RFC 2891(www.ietf.org/rfc/rfc2891.txt)中规定了这种控制。排序请求控件接受一个或多个 LDAP 属性名,并将其提供给服务器来执行实际的排序。

让我们看看如何在普通的 JNDI API 中使用排序控件。清单 7-4 显示了按照姓氏对所有搜索结果进行排序的代码。首先创建 javax.naming.ldap.SortControl 的一个新实例,并为它提供 sn 属性,表明您打算按姓氏排序。您还通过向同一个构造函数提供 critical 标志来表明这是一个关键控件。然后,使用 setRequestControls 方法将该请求控件添加到上下文中,并执行 LDAP 搜索操作。然后遍历返回的结果,并将它们打印到控制台。最后,你看看反应控制。排序响应控件保存排序操作的结果。如果服务器未能对结果进行排序,您可以通过抛出异常来表明这一点。

清单 7-4。

public void sortByLastName() {
   try {
      LdapContext context = getContext();
      Control lastNameSort = new SortControl("sn", Control.CRITICAL);
      context.setRequestControls(new Control[]{lastNameSort});
      SearchControls searchControls = new SearchControls();
      searchControls.setSearchScope( SearchControls.SUBTREE_SCOPE);
      NamingEnumeration results = context.search("dc=inflinx,dc=com", "(objectClass=inetOrgPerson)", searchControls);

          /* Iterate over search results and display
      * patron entries
      */
      while (results != null && results.hasMore()) {
         SearchResult entry = (SearchResult)results.next();
         System.out.println(entry.getAttributes().get("sn") + " ( " + (entry.getName()) + " )");
      }

      /* Now that we have looped, we need to look at the response controls*/
      Control[] responseControls = context.getResponseControls();
      if(null != responseControls) {
         for(Control control : responseControls) {
            if(control instanceof SortResponseControl) {
               SortResponseControl sortResponseControl = (SortResponseControl) control;
               if(!sortResponseControl.isSorted()) {
                  // Sort did not happen. Indicate this with an exception
                  throw sortResponseControl.getException();
               }
            }
        }
      }
      context.close();
   }
   catch(Exception e) {
      e.printStackTrace();
   }
}

The output should display the sorted patrons as shown below:
sn: Aalders ( uid=patron4,ou=patrons )
sn: Aasen ( uid=patron5,ou=patrons )
sn: Abadines ( uid=patron6,ou=patrons )
sn: Abazari ( uid=patron7,ou=patrons )
sn: Abbatantuono ( uid=patron8,ou=patrons )
sn: Abbate ( uid=patron9,ou=patrons )
sn: Abbie ( uid=patron10,ou=patrons )
sn: Abbott ( uid=patron11,ou=patrons )
sn: Abdalla ( uid=patron12,ou=patrons )
......................................

现在让我们看看使用 Spring LDAP 实现相同的排序行为。清单 7-5 显示了相关的代码。在这个实现中,首先创建一个新的 org . spring framework . LDAP . control . sortcontroldircontextprocessor 实例。SortControlDirContextProcessor 构造函数采用 LDAP 属性名称,该名称应在控件创建期间用作排序键。下一步是创建 SearchControls 和一个过滤器来限制搜索。最后,调用 search 方法,传递创建的实例和映射数据的映射器。

清单 7-5。

public List<String> sortByLastName() {
   DirContextProcessor scdcp = new SortControlDirContextProcessor("sn");
   SearchControls searchControls = new SearchControls();
   searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

   EqualsFilter equalsFilter = new EqualsFilter("objectClass", "inetOrgPerson");

   @SuppressWarnings("unchecked")
   ParameterizedContextMapper<String> lastNameMapper = new AbstractParameterizedContextMapper<String>() {
      @Override
      protected String doMapFromContext(DirContextOperations context) {
         return context.getStringAttribute("sn");
      }
   };

   List<String> lastNames = ldapTemplate.search("", equalsFilter.encode(), searchControls, lastNameMapper, scdcp);
   for (String ln : lastNames){
      System. out .println(ln);
   }
   return lastNames;
}

调用此方法后,您应该会在控制台中看到以下输出:

Aalders
Aasen
Abadines
Abazari
Abbatantuono
Abbate
Abbie
Abbott
Abdalla
Abdo
Abdollahi
Abdou
Abdul-Nour
................

实现自定义 DirContextProcessor

从 Spring LDAP 1.3.2 开始,SortControlDirContextProcessor 只能用于对一个 LDAP 属性进行排序。然而,JNDI API 允许你对多个属性进行排序。因为在某些情况下,您可能希望根据多个属性对搜索结果进行排序,所以让我们实现一个新的 DirContextProcessor,它将允许您在 Spring LDAP 中实现这一点。

到目前为止,您已经看到,排序操作需要一个请求控件,并将发送一个响应控件。所以实现这个功能最简单的方法就是扩展 AbstractFallbackRequestAndResponseControlDirContextProcessor。清单 7-6 显示了用空抽象方法实现的初始代码。正如您将看到的,您使用了三个实例变量来保存控件的状态。顾名思义,sortKeys 将保存将要排序的属性名。sorted 和 resultCode 变量将保存从响应控件中提取的信息。

清单 7-6。

package com.inflinx.book.ldap.control;

import javax.naming.ldap.Control;
import org.springframework.ldap.control.AbstractFallbackRequestAndResponseControlDirContextProcessor;

public class SortMultipleControlDirContextProcessor extends AbstractFallbackRequestAndResponseControlDirContextProcessor {

   //The keys to sort on
   private String[] sortKeys;

   //Did the results actually get sorted?
   private boolean sorted;

   //The result code of the sort operation
   private int resultCode;

   @Override
   public Control createRequestControl() {
      return null;
   }

   @Override
   protected void handleResponse(Object control) {
   }

   public String[] getSortKeys() {
      return sortKeys;
   }

   public boolean isSorted() {
      return sorted;
   }

   public int getResultCode() {
      return resultCode;
   }
}

下一步是向 AbstractFallbackRequestAndResponseControlDirContextProcessor 提供加载控件所需的信息。abstractfallbackrequestandresponsecontroldicontextprocessor 需要来自子类的两条信息:要使用的请求和响应控件的完全限定类名,以及应该用作后备的控件的完全限定类名。清单 7-7 显示了完成这项工作的构造器代码。

清单 7-7

public SortMultipleControlDirContextProcessor(String ... sortKeys) {

   if(sortKeys.length == 0) {
      throw new IllegalArgumentException("You must provide " + "atlease one key to sort on");
   }

   this.sortKeys = sortKeys;
   this.sorted = false;
   this.resultCode = -1;
   this.defaultRequestControl = "javax.naming.ldap.SortControl";
   this.defaultResponseControl = "javax.naming.ldap.SortResponseControl";
   this.fallbackRequestControl = "com.sun.jndi.ldap.ctl.SortControl";
   this.fallbackResponseControl = "com.sun.jndi.ldap.ctl.SortResponseControl";

   loadControlClasses();
}

请注意,您已经提供了 JDK 附带的控件类作为要使用的默认控件,以及 LDAP booster pack 附带的控件作为后备控件。在构造函数的最后一行,您指示 AbstractFallbackRequestAndResponseControlDirContextProcessor 类将这些类加载到 JVM 中以供使用。

流程的下一步是提供 createRequestControl 方法的实现。由于超类 abstractfallbackrequestandresponsecontroldicontextprocessor 将负责控件的实际创建,所以您只需提供创建控件所需的信息。以下代码说明了这一点:

@Override
public Control createRequestControl() {
   return super.createRequestControl(new Class[] {String[].class, boolean.class }, new Object[] { sortKeys, critical });
}

实施的最后一步是分析响应控制并检索关于已完成操作的信息。清单 7-8 显示了相关的代码。请注意,您正在使用反射从响应控件中检索排序和结果代码信息。

清单 7-8。

@Override
protected void handleResponse(Object control) {

   Boolean result = (Boolean) invokeMethod("isSorted", responseControlClass, control);
   this.sorted = result;

   Integer code = (Integer) invokeMethod("getResultCode", responseControlClass, control);
   this.resultCode = code;
}

现在您已经创建了一个新的 DirContextProcessor 实例,它允许您对多个属性进行排序,让我们来试一试。清单 7-9 显示了一个排序方法,它使用了 SortMultipleControlDirContextProcessor。该方法使用属性 st 和 l 对结果进行排序。

清单 7-9。

public void sortByLocation() {

   String[] locationAttributes = {"st", "l"};
   SortMultipleControlDirContextProcessor smcdcp = new SortMultipleControlDirContextProcessor(locationAttributes);
   SearchControls searchControls = new SearchControls();
   searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

   EqualsFilter equalsFilter = new EqualsFilter("objectClass","inetOrgPerson");

   @SuppressWarnings("unchecked")
   ParameterizedContextMapper<String> locationMapper = new AbstractParameterizedContextMapper<String>() {

      @Override
      protected String doMapFromContext(DirContextOperations context) {
         return context.getStringAttribute("st") + "," + context.getStringAttribute("l");
      }
   };

   List<String> results = ldapTemplate.search("", equalsFilter.encode(), searchControls, locationMapper, smcdcp);
   for(String r : results) {
      System.out.println(r);
   }
}

调用该方法后,排序后的位置将显示在控制台上,如图所示:

AK,Abilene
AK,Florence
AK,Sioux Falls
AK,Wilmington
AL,Glendive
AR,Gainesville
AR,Green Bay
AZ,Gainesville
AZ,Moline
AZ,Reno
AZ,Saint Joseph
AZ,Wilmington
CA,Buffalo
CA,Ottumwa
CO,Charlottesville
CO,Lake Charles
CT,Quincy
CT,Youngstown
...............

分页搜索控件

分页结果控件允许 LDAP 客户端控制 LDAP 搜索操作结果的返回速率。LDAP 客户端创建具有指定页面大小的页面控件,并将其与搜索请求相关联。收到请求后,LDAP 服务器将分块返回结果,每个块包含指定数量的结果。在处理大型目录或构建具有分页功能的搜索应用时,分页结果控件非常有用。这种控制在 RFC 2696(www.ietf.org/rfc/rfc2696.txt)中有描述。

图 7-6 描述了使用页面控件的 LDAP 客户端和服务器之间的交互。

9781430263975_Fig07-06.jpg

图 7-6 。页面控制交互

image 注意 LDAP 服务器经常使用 sizeLimit 指令来限制搜索操作返回的结果数量。如果搜索产生的结果多于指定的大小限制,则会引发大小限制超出异常 javax . naming . sizelimitexceededededexception。分页方法不会让您超过这个限制。

第一步,LDAP 客户端发送搜索请求和页面控件。收到请求后,LDAP 服务器执行搜索操作并返回第一页结果。此外,它发送一个 cookie ,需要用它来请求下一个分页的结果集。这个 cookie 使 LDAP 服务器能够维护搜索状态。客户端不得对 cookie 的内部结构做出任何假设。当客户端请求下一批结果时,它会发送相同的搜索请求和页面控件以及 cookie。服务器用新的结果集和新的 cookie 进行响应。当没有更多的搜索结果返回时,服务器发送一个空的 cookie。

使用分页搜索控件的分页是单向和顺序的。客户端不可能在页面之间跳转或返回。现在你已经知道了分页控制的基本知识,清单 7-10 显示了使用普通 JNDI API 的实现。

清单 7-10。

public void pageAll() {

   try {
      LdapContext context = getContext();
      PagedResultsControl prc = new PagedResultsControl(20, Control.CRITICAL);
      context.setRequestControls(new Control[]{prc});
      byte[] cookie = null;
      SearchControls searchControls = new SearchControls();
      searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
      do {
         NamingEnumeration results = context.search("dc=inflinx,dc=com","(objectClass=inetOrgPerson)",searchControls);
         // Iterate over search results
         while(results != null && results.hasMore()) {
           // Display an entry
           SearchResult entry = (SearchResult)results.next();
           System.out.println(entry.getAttributes().get("sn") + " ( " + (entry.getName())+ " )");
         }
         // Examine the paged results control response
         Control[] controls = context.getResponseControls();
         if (controls != null) {
           for(int i = 0; i < controls.length; i++) {
             if(controls[i] instanceof PagedResultsResponseControl) {
               PagedResultsResponseControl prrc =(PagedResultsResponseControl)controls[i];
               int resultCount = prrc.getResultSize();
               cookie = prrc.getCookie();
             }
           }
        }
        // Re-activate paged results
        context.setRequestControls(new Control[]{
        new PagedResultsControl(20, cookie, Control.CRITICAL)});
      } while(cookie != null);

      context.close();
   }
   catch(Exception e) {
      e.printStackTrace();
   }
}

在清单 7-10 中,您通过获取 LDAP 服务器上的上下文来开始实现。然后创建 PagedResultsControl ,并将页面大小指定为其构造函数参数。您将控件添加到上下文中,并执行了搜索操作。然后循环搜索结果,并在控制台上显示信息。下一步,您将检查响应控件以识别服务器发送的 PagedResultsResponseControl。从该控件中,您提取 cookie 和该搜索的估计结果总数。结果计数是可选的信息,并且服务器可以简单地返回零来指示未知的计数。最后,创建一个新的 PagedResultsControl,将页面大小和 cookie 作为其构造函数参数。这个过程一直重复,直到服务器发送一个空的(null) cookie,表示不再有要处理的结果。

Spring LDAP 抽象了清单 7-10 中的大部分代码,并使用 PagedResultsDirContextProcessor 简化了页面控件的处理。清单 7-11 显示了 Spring LDAP 代码。

清单 7-11。

public void pagedResults() {

   PagedResultsCookie cookie = null;
   SearchControls searchControls = new SearchControls();
   searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
   int page = 1;
   do {
      System.out.println("Starting Page: " + page);
      PagedResultsDirContextProcessor processor = new PagedResultsDirContextProcessor(20,cookie);
      EqualsFilter equalsFilter = new EqualsFilter("objectClass","inetOrgPerson");
      List<String> lastNames = ldapTemplate.search("", equalsFilter.encode(), searchControls, new LastNameMapper(), processor);
      for(String l : lastNames) {
         System.out.println(l);
      }
      cookie = processor.getCookie();
      page = page + 1;
   } while(null != cookie.getCookie());
}

在这个实现中,使用页面大小和一个 cookie 创建 PagedResultsDirContextProcessor。请注意,您使用 org . spring framework . LDAP . control . pagedresultsCookie 类来抽象服务器发送的 cookie。cookie 值最初以空值开始。然后执行搜索并遍历结果。服务器发送的 cookie 是从 DirContextProcessor 中提取的,用于检查未来的搜索请求。您还使用 LastNameMapper 类从结果上下文中提取姓氏。清单 7-12 给出了 LastNameMapper 类的实现。

清单 7-12。

private class LastNameMapper extends AbstractParameterizedContextMapper<String> {

   @Override
   protected String doMapFromContext(DirContextOperations context) {
      return context.getStringAttribute("sn");
   }
}

摘要

在本章中,您学习了与 LDAP 控件相关的基本概念。然后查看了排序控件,该控件可用于对结果执行服务器端排序。您看到了 Spring LDAP 如何显著简化排序控件的使用。分页控件可用于分页 LDAP 结果,这在流量很大的情况下非常有用。

在下一章中,您将看到使用 Spring LDAP ODM 技术来实现数据访问层。

八、对象目录映射

在本章中,您将学习

  • ODM 的基础。
  • Spring LDAP ODM 实现。

企业 Java 开发人员采用面向对象(OO)技术来创建模块化的复杂应用。在 OO 范式中,对象是系统的核心,代表现实世界中的实体。每个对象都有一个身份、状态和行为。对象可以通过继承或组合与其他对象相关联。另一方面,LDAP 目录以分层树结构表示数据和关系。这种差异导致了对象-目录范式的不匹配,并可能导致面向对象和目录环境之间的通信出现问题。

Spring LDAP 提供了一个对象-目录映射(ODM) 框架,在对象和目录模型之间架起了一座桥梁。ODM 框架允许我们在两个模型之间映射概念,并编排自动将 LDAP 目录条目转换成 Java 对象的过程。ODM 类似于人们更熟悉的对象关系映射(ORM)方法,它在对象和关系数据库世界之间架起了一座桥梁。Hibernate 和 Toplink 之类的框架使得 ORM 变得流行,并且成为开发人员工具集的重要组成部分。

尽管 Spring LDAP ODM 与 ORM 共享相同的概念,但它确实有以下不同之处:

  • 不可能缓存 LDAP 条目。
  • ODM 元数据是通过类级注释来表达的。
  • 没有可用的 XML 配置。
  • 条目的惰性加载是不可能的。
  • 像 HQL 这样的查询语言并不存在。对象的加载是通过 DN 查找和标准 LDAP 搜索查询来完成的。

Spring ODM 基础

Spring LDAP ODM 作为一个独立于核心 LDAP 项目的模块分发。为了在项目中包含 Spring LDAP ODM,需要将下面的依赖项添加到项目的 pom.xml 文件中:

<dependency>
    <groupId>org.springframework.ldap</groupId>
    <artifactId>spring-ldap-odm</artifactId>
    <version>${org.springframework.ldap.version}</version>
    <exclusions>
        <exclusion>
            <artifactId>commons-logging</artifactId>
            <groupId>commons-logging</groupId>
        </exclusion>
    </exclusions>
</dependency>

Spring LDAP ODM 可以在 org.springframework.ldap.odm 包及其子包中找到。Spring LDAP ODM 的核心类如图 8-1 中的所示。在这一章中,你将会详细地看到每一个类。

9781430263975_Fig08-01.jpg

图 8-1 。Spring LAP ODM 核心类

LDAP ODM 的核心是提供通用搜索和 CRUD 操作的 OdmManager。它充当中介,在 LDAP 条目和 Java 对象之间转换数据。Java 对象被注释以提供转换元数据。清单 8-1 展示了 OdmManager API 。

清单 8-1。

Package org.springframeworkldap.odm.core;

import java.util.List;
import javax.naming.Name;
import javax.naming.directory.SearchControls;

public interface OdmManager {

   void create(Object entry);
   <T> T read(Class<T> clazz, Name dn);
   void update(Object entry);
   void delete(Object entry);
   <T> List<T> findAll(Class<T> clazz, Name base, SearchControls searchControls);
   <T> List<T> search(Class<T> clazz, Name base, String filter, SearchControls searchControls);
}

OdmManager 的 create、update 和 delete 方法接受一个 Java 对象,并使用其中的信息来执行相应的 LDAP 操作。read 方法有两个参数,一个确定返回类型的 Java 类和一个用于查找 LDAP 条目的全限定 DN。OdmManager 可以看作是你在第五章中看到的通用 DAO 模式的一个微小变化。

Spring LDAP ODM 提供了 OdmManager 的现成实现,名为 OdmManagerImpl。为了正常运行,OdmManagerImpl 使用以下三个对象:

  • 用于与 LDAP 服务器通信的 ContextSource 实现。
  • 一个 ConverterManager 实现,用于将 LDAP 数据类型转换为 Java 数据类型,反之亦然。
  • 需要由 ODM 实现管理的一组域类。

为了简化 OdmManagerImpl 实例的创建,框架提供了一个工厂 bean OdmManagerImplFactoryBean。下面是创建 OdmManager 实例的必要配置:

<bean  id="odmManager" class="org.springframework.ldap.odm. core.impl.OdmManagerImplFactoryBean">
    <property  name="converterManager" ref="converterManager"  />
    <property  name="contextSource" ref="contextSource" />
    <property  name="managedClasses">
        <set>
            <value>FULLY_QUALIFIED_CLASS_NAME</value>
        </set>
    </property>
</bean>

OdmManager 将 LDAP 属性到 Java 字段的转换管理(反之亦然)委托给 ConverterManager。ConverterManager 本身依赖于一组用于实际转换目的的转换器实例。清单 8-2 显示了转换器接口 API 。convert 方法接受一个对象作为其第一个参数,并将其转换为由 toClass 参数指定的类型的实例。

清单 8-2。

package org.springframework.ldap.odm.typeconversion.impl;

public interface Converter {
   <T> T convert(Object source, Class<T> toClass) throws Exception;
}

转换器的通用特性使得创建特定的实现变得容易。Spring LDAP ODM 提供了转换器接口的 ToStringConverter 实现,它将给定的源对象转换为字符串。清单 8-3 提供了 ToStringConverter API 实现。正如您所看到的,只需在源对象上调用 toString 方法就可以进行转换。

清单 8-3。

package org.springframework.ldap.odm.typeconversion.impl.converters;

import org.springframework.ldap.odm.typeconversion.impl.Converter;

public final class ToStringConverter implements Converter {

   public <T> T convert(Object source, Class<T> toClass) {
      return toClass.cast(source.toString());
   }
}

这个实现的逆过程是 FromStringConverter,它将 java.lang.String 对象转换为任何指定的 toClass 类型。清单 8-4 提供了 FromStringConverter API 实现。转换器实现通过调用 toClass 参数的构造函数并传入 String 对象来创建新的实例。toClass 类型参数必须有一个接受单个 java.lang.String 类型参数的公共构造函数。例如,FromStringConverter 可以将字符串数据转换为整数或长数据类型。

清单 8-4。

package org.springframework.ldap.odm.typeconversion.impl.converters;

import java.lang.reflect.Constructor;
import org.springframework.ldap.odm.typeconversion.impl.Converter;

public final class FromStringConverter implements Converter {

   public <T> T convert(Object source, Class<T> toClass) throws Exception {
      Constructor<T> constructor = toClass.getConstructor(java.lang.String.class);
      return constructor.newInstance(source);
   }
}

这两个转换器类应该足以将大多数 LDAP 数据类型转换为常见的 Java 字段类型,如 java.lang.Integer、java.lang.Byte 等。反之亦然。清单 8-5 显示了创建 FromStringConverter 和 ToStringConverter 实例所涉及的 XML 配置。

清单 8-5。

<bean id="fromStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter" />
<bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter" />

现在您已经准备好创建 ConverterManager 的一个实例,并向它注册上述两个转换器。注册转换器包括指定转换器本身、指示转换器预期的源对象类型的 fromClass 和指示转换器将返回的类型的 toClass。为了简化转换器注册过程,Spring ODM 提供了一个 ConverterConfig 类。清单 8-6 显示了注册 toStringConverter 实例的 XML 配置。

清单 8-6。

<bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
   <property name="converter" ref="toStringConverter"/>
   <property name="fromClasses">
      <set>
         <value>java.lang.Integer</value>
      </set>
   </property>
   <property name="toClasses">
      <set>
         <value>java.lang.String</value>
      </set>
   </property>
</bean>

如您所见,ConverterConfig 是 org . spring framework . LDAP . ODM . type conversion . impl . convertermanagerfactorybean 类的内部类。此配置告诉 ConverterManager 使用 toStringConverter bean 将 java.lang.Integer 类型转换为 String 类型。在内部,转换器注册在使用以下算法计算的密钥下:

key = fromClass.getName() + ":" + syntax + ":" + toClass. getName();

有时,您可能希望使用同一个转换器实例来转换各种数据类型。例如,ToStringConverter 可用于转换其他类型,如 java.lang.Long、java.lang.Byte、java.lang.Boolean 等。为了处理这种情况,ConverterConfig 接受一组转换器可以处理的 from 和 To 类。清单 8-7 显示了修改后的 ConverterConfig ,它接受几个 fromClasses。

清单 8-7。

<bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
   <property name="converter" ref="toStringConverter" />
   <property name="fromClasses">
      <set>
         <value>java.lang.Byte</value>
         <value>java.lang.Integer</value>
         <value>java.lang.Boolean</value>
      </set>
   </property>
   <property name="toClasses">
      <set>
         <value>java.lang.String</value>
      </set>
   </property>
</bean>

上述 fromClasses 集合中指定的每个类都将与 toClasses 集合中的一个类成对出现,以便进行转换器注册。因此,如果指定 n 个 fromClasses 和 m 个 toClasses,将导致转换器有 n*m 个注册。清单 8-8 显示了 fromStringConverterConfig ,它与之前的配置非常相似。

清单 8-8。

<bean id="fromStringConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
   <property name="converter" ref="fromStringConverter" />
   <property name="fromClasses">
      <set>
         <value>java.lang.String</value>
      </set>
   </property>
   <property name="toClasses">
      <set>
         <value>java.lang.Byte</value>
         <value>java.lang.Integer</value>
         <value>java.lang.Boolean</value>
      </set>
   </property>
</bean>

拥有必要的转换器配置后,可以使用 ConverterManagerFactoryBean 创建新的 ConverterManager 实例。清单 8-9 显示了所需的 XML 声明。

清单 8-9。

<bean id="converterManager" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean">
   <property name="converterConfig">
      <set>
         <ref bean="fromStringConverterConfig"/>
         <ref bean="toStringConverterConfig"/>
      </set>
   </property>
</bean>

使用 ODM 框架所需的设置到此结束。在接下来的小节中,您将看到如何注释域类,以及如何使用这个配置进行 LDAP 读写。在此之前,让我们回顾一下到目前为止你做了什么(见图 8-2 )。

9781430263975_Fig08-02.jpg

图 8-2 。OdmManager 内部工作方式

  1. OdmManager 实例是由 OdmManagerImplFactoryBean 创建的。
  2. OdmManager 使用 ConverterManager 实例在 LDAP 和 Java 类型之间进行转换。
  3. 对于从一种特定类型到另一种特定类型的转换,ConverterManager 使用转换器。
  4. ConverterManager 实例由 ConverterManagerFactoryBean 创建。
  5. ConverterManagerFactoryBean 使用 ConverterConfig 实例来简化转换器注册。ConverterConfig 类接受 fromClasses、toClasses 和伴随关系的转换器。

ODM 元数据

org . spring framework . LDAP . odm . annotations 包包含可用于将简单的 Java POJOs 转换成 ODM 可管理实体的注释。清单 8-10 展示了 Patron Java 类,您将把它转换成一个 ODM 实体。

清单 8-10。

public class Patron {

   private String lastName;
   private String firstName;
   private String telephoneNumber;
   private String fullName;
   private String mail;
   private int employeeNumber;

   // Getters and setters

   @Override
   public String toString() {
      return "Dn: " + dn + ", firstName: " + firstName + ", fullName: " +       fullName + ", Telephone Number: " + telephoneNumber;
   }
}

您将通过用@Entry 注释该类来开始转换。这个标记注释告诉 ODM 管理器这个类是一个实体。它还用于提供实体映射到的 LDAP 中的对象类定义。清单 8-11 显示了带注释的 Patron 类。

清单 8-11。

@Entry(objectClasses= { "inetorgperson", "organizationalperson", "person", "top" })
public class Patron {
   // Fields and getters and setters
}

您需要添加的下一个注释是@Id 。该注释指定条目的 DN,并且只能放在 javax.naming.Name 类的派生字段上。为了解决这个问题,您将在 Patron 类中创建一个名为 dn 的新字段。清单 8-12 显示了修改后的顾客类。

清单 8-12。

@Entry(objectClasses= { "inetorgperson", "organizationalperson", "person", "top" })
public class Patron {

   @Id
   private Name dn;
   // Fields and getters and setters
}

Java 持久性 API 中的@Id 注释指定了实体 bean 的标识符属性。此外,它的位置决定了 JPA 提供者将用于映射的默认访问策略。如果将@Id 放在字段上,则使用字段访问。如果将它放在 getter 方法上,将使用属性访问。然而,Spring LDAP ODM 只允许字段访问。

@Entry 和@Id 是使 Patron 类成为 ODM 实体的唯一两个必需的注释。默认情况下,Patron 实体类中的所有字段都将自动变为可持久的。默认策略是在持久化或读取时使用实体字段的名称作为 LDAP 属性名称。在 Patron 类中,这适用于 telephoneNumber 或 mail 等属性,因为字段名和 LDAP 属性名是相同的。但是这会导致 firstName 和 fullName 等字段出现问题,因为它们的名称不同于 LDAP 属性名称。为了解决这个问题,ODM 提供了@Attribute 注释,将实体字段映射到对象类字段。该注释允许您指定 LDAP 属性的名称、可选的语法 OID 和可选的类型声明。清单 8-13 显示了完全注释的顾客实体类。

清单 8-13。

@Entry(objectClasses = { "inetorgperson", "organizationalperson", "person", "top" })
public class Patron {

   @Id
   private Name dn;

   @Attribute(name = "sn")
   private String lastName;

   @Attribute(name = "givenName")
   private String firstName;
   private String telephoneNumber;

   @Attribute(name = "cn")
   private String fullName;
   private String mail;

   @Attribute(name = "objectClass")
   private List<String> objectClasses;

   @Attribute(name = "employeeNumber", syntax = "2.16.840.1.113730.3.1.3")
   private int employeeNumber;

   // Getters and setters

   @Override
   public String toString() {
      return "Dn: " + dn + ", firstName: " + firstName + "," + " fullName: " + fullName + ", Telephone Number: " + telephoneNumber;
   }
}

有些时候你不希望保存实体类的某些字段。通常,这些涉及到计算的字段。这样的字段可以用@Transient annotation 进行注释,表示该字段应该被 OdmManager 忽略。

ODM 服务类别

基于 Spring 的企业应用通常有一个保存应用业务逻辑的服务层。服务层中的类将持久性细节委托给 DAO 或存储库层。在第五章中,你用 LdapTemplate 实现了一个 DAO。在本节中,您将创建一个新的服务类,它使用 OdmManager 作为 DAO 的替代。清单 8-14 显示了您将要实现的服务类的接口。

清单 8-14。

package com.inflinx.book.ldap.service;

import com.inflinx.book.ldap.domain.Patron;

public interface PatronService {

   public void create(Patron patron);
   public void delete(String id);
   public void update(Patron patron);
   public Patron find(String id);
}

服务类实现在清单 8-15 中给出。在实现中,您注入一个 OdmManager 实例。create 和 update 方法实现只是将调用委托给 OdmManager。find 方法将传入的 id 参数转换为完全限定的 DN,并将实际的检索委托给 OdmManager 的 read 方法。最后,delete 方法使用 find 方法读取 patron,并使用 OdmManager 的 delete 方法删除它。

清单 8-15。

package com.inflinx.book.ldap.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.odm.core.OdmManager;
import org.springframework.stereotype.Service;
import com.inflinx.book.ldap.domain.Patron;

@Service("patronService" )
public class PatronServiceImpl implements PatronService {

   private static final String PATRON_BASE = "ou=patrons,dc=inflinx,dc=com";

   @Autowired
   @Qualifier("odmManager" )
   private OdmManager odmManager;

   @Override
   public void create(Patron patron) {
      odmManager.create(patron);
   }
   @Override
   public void update(Patron patron) {
      odmManager.update(patron);
   }
   @Override
   public Patron find(String id) {
      DistinguishedName dn = new DistinguishedName(PATRON_BASE);
      dn.add("uid", id);
      return odmManager.read(Patron.class, dn);
   }
   @Override
   public void delete(String id) {
      odmManager.delete(find(id));
   }
}

验证 PatronService 实现的 JUnit 测试如清单 8-16 所示。

清单 8-16。

package com.inflinx.book.ldap.service;

import org.junit.After;
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.core.io.ClassPathResource;
import org.springframework.ldap.NameNotFoundException;
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.Patron;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration("classpath:repositoryContext-test.xml" )
public class PatronServiceImplTest {

   @Autowired
   private PatronService patronService;
   private static final String PORT = "12389";
   private static final String ROOT_DN = "dc=inflinx,dc=com";

   @Before
   public void setup() throws Exception {
      System.out.println("Inside the setup");
      LdapUnitUtils.loadData(new ClassPathResource("patrons.ldif"), PORT);
   }

   @After
   public void teardown() throws Exception {
      System.out.println("Inside the teardown");
      LdapUnitUtils.clearSubContexts(new DistinguishedName(ROOT_DN), PORT);
   }

   @Test
   public void testService() {
      Patron patron = new Patron();

      patron.setDn(new DistinguishedName("uid=patron10001," + "ou=patrons,dc=inflinx,dc=com"));
      patron.setFirstName("Patron");
      patron.setLastName("Test 1");
      patron.setFullName("Patron Test 1");
      patron.setMail("balaji@inflinx.com" );
      patron.setEmployeeNumber(1234);
      patron.setTelephoneNumber("8018640759");
      patronService.create(patron);

      // Lets read the patron
      patron = patronService.find("patron10001");
      assertNotNull(patron);

      patron.setTelephoneNumber("8018640850");
      patronService.update(patron);
      patron = patronService.find("patron10001");
      assertEquals(patron.getTelephoneNumber(), "8018640850");
      patronService.delete("patron10001");

      try {
         patron = patronService.find("patron10001");
         assertNull(patron);
      }
      catch(NameNotFoundException e) {
      }
   }
}

repositoryContext-test.xml 文件包含到目前为止您所看到的配置片段。清单 8-17 给出了 XML 文件的完整内容。

清单 8-17。

<?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="serverType" value="OPENDJ" />
   </bean>
   <bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImpl">
      <constructor-arg name="converterManager" ref="converterManager" />
      <constructor-arg name="contextSource" ref="contextSource" />
      <constructor-arg name="managedClasses">
         <set>
            <value>com.inflinx.book.ldap.domain.Patron</value>
         </set>
      </constructor-arg>
   </bean>
   <bean id="fromStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter" />
   <bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter" />

   <!-- Configuration information for a single instance of FromString -->
   <bean id="fromStringConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="fromStringConverter" />
      <property name="fromClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.Byte</value>
            <value>java.lang.Integer</value>
            <value>java.lang.Boolean</value>
         </set>
      </property>
   </bean>
   <bean id="toStringCoverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="toStringConverter" />
      <property name="fromClasses">
         <set>
            <value>java.lang.Byte</value>
            <value>java.lang.Integer</value>
            <value>java.lang.Boolean</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
   </bean>
   <bean id="converterManager" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean">
      <property name="converterConfig">
         <set>
            <ref bean="fromStringConverterConfig"/>
            <ref bean="toStringCoverterConfig"/>
         </set>
      </property>
   </bean>
</beans>

配置简化

清单 8-17 中的配置乍一看可能令人望而生畏。因此,为了解决这个问题,让我们创建一个新的 ConverterManager 实现来简化配置过程。清单 8-18 显示了 DefaultConverterManagerImpl 类。如您所见,它使用了其实现内部的 ConverterManagerImpl 类。

清单 8-18。

package com.inflinx.book.ldap.converter;

import org.springframework.ldap.odm.typeconversion.ConverterManager;
import org.springframework.ldap.odm.typeconversion.impl.Converter;
import org.springframework.ldap.odm.typeconversion.impl.ConverterManagerImpl;
import org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter;
import org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter;

public class DefaultConverterManagerImpl implements ConverterManager {

   private static final Class[] classSet = { java.lang.Byte.class, java.lang.Integer.class, java.lang.Long.class, java.lang.Double.class, java.lang.Boolean.class };
   private ConverterManagerImpl converterManager;

   public DefaultConverterManagerImpl() {
      converterManager = new ConverterManagerImpl();
      Converter fromStringConverter = new FromStringConverter();
      Converter toStringConverter = new ToStringConverter();
      for(Class clazz : classSet) {
         converterManager.addConverter(String.class, null, clazz, fromStringConverter);
         converterManager.addConverter(clazz, null, String.class, toStringConverter);
      }
   }

   @Override
   public boolean canConvert(Class<?> fromClass, String syntax, Class<?> toClass) {
      return converterManager.canConvert(fromClass, syntax, toClass);
   }

   @Override
   public <T> T convert(Object source, String syntax, Class<T> toClass) {
      return converterManager.convert(source,syntax,toClass);
   }
}

使用这个类可以大大减少所需的配置,如清单 8-19 所示。

清单 8-19。

<?xml version="1.0" encoding="UTF-8"?>
<beans http://www.springframework.org/schema/beans">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="serverType" value="OPENDJ" />
   </bean>
   <bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImplFactoryBean">
      <property name="converterManager" ref="converterManager" />
      <property name="contextSource" ref="contextSource" />
      <property name="managedClasses">
         <set>
            <value>com.inflinx.book.ldap.domain.Patron</value>
         </set>
      </property>
   </bean>
   <bean id="converterManager" class="com.inflinx.book.ldap.converter.DefaultConverterManagerImpl" />
</beans>

创建自定义转换器

考虑这样一个场景,您的顾客类使用一个定制的 PhoneNumber 类来存储顾客的电话号码。现在,当需要持久化一个 Patron 类时,您需要将 PhoneNumber 类转换为 String 类型。类似地,当从 LDAP 中读取 Patron 类时,需要将电话属性中的数据转换成 PhoneNumber 类。默认的 ToStringConverter 和 FromStringConverter 对此类转换没有用。清单 8-20 和清单 8-21 分别显示了电话号码和修改后的顾客类。

清单 8-20。

package com.inflinx.book.ldap.custom;

public class PhoneNumber {

   private int areaCode;
   private int exchange;
   private int extension;

   public PhoneNumber(int areaCode, int exchange, int extension) {
      this.areaCode = areaCode;
      this.exchange = exchange;
      this.extension = extension;
   }

   public boolean equals(Object obj) {
      if(obj == null || obj.getClass() != this.getClass())
      { return false; }

      PhoneNumber p = (PhoneNumber) obj;
         return (this.areaCode == p.areaCode) && (this.exchange == p.exchange) && (this.extension == p.extension);
   }

   public String toString() {
      return String.format("+1 %03d %03d %04d", areaCode, exchange, extension);
   }

   // satisfies the hashCode contract
   public int hashCode() {
      int result = 17;
      result = 37 * result + areaCode;
      result = 37 * result + exchange;
      result = 37 * result + extension;

          return result;
   }
}

清单 8-21

package com.inflinx.book.ldap.custom;

import java.util.List;
import javax.naming.Name;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.Entry;
import org.springframework.ldap.odm.annotations.Id;

@Entry(objectClasses = { "inetorgperson", "organizationalperson", "person", "top" })
public class Patron {

   @Id
   private Name dn;

   @Attribute(name= "sn")
   private String lastName;

   @Attribute(name= "givenName")
   private String firstName;

   @Attribute(name= "telephoneNumber")
   private PhoneNumber phoneNumber;

   @Attribute(name= "cn")
   private String fullName;
   private String mail;

   @Attribute(name= "objectClass")
   private List<String> objectClasses;

   @Attribute(name= "employeeNumber", syntax = "2.16.840.1.113730.3.1.3")
    private int employeeNumber;

   // Getters and setters

   @Override
   public String toString() {
      return "Dn: " + dn + ", firstName: " + firstName + "," + " fullName: " + fullName + ", " + "Telephone Number: " + phoneNumber;
   }
}

若要将 PhoneNumber 转换为字符串,您需要创建一个新的 FromPhoneNumberConverter 转换器。清单 8-22 显示了实现。实现只需要调用 toString 方法来执行转换。

清单 8-22。

package com.inflinx.book.ldap.custom;

import org.springframework.ldap.odm.typeconversion.impl.Converter;

public class FromPhoneNumberConverter implements Converter {

   @Override
   public <T> T convert(Object source, Class<T> toClass) throws Exception {
      T result = null;
      if(PhoneNumber.class.isAssignableFrom(source.getClass()) && toClass.equals(String.class)) {
         result = toClass.cast(source.toString());
      }
      return result;
   }
}

接下来,您需要一个实现来将 LDAP 字符串属性转换为 Java PhoneNumber 类型。为此,您创建了 ToPhoneNumberConverter ,如清单 8-23 中的所示。

清单 8-23。

package com.inflinx.book.ldap.custom;

import org.springframework.ldap.odm.typeconversion.impl.Converter;

public class ToPhoneNumberConverter implements  Converter {

   @Override
   public <T> T convert(Object source, Class<T> toClass) throws Exception {
      T result = null;
      if(String.class.isAssignableFrom(source.getClass()) && toClass == PhoneNumber.class) {
      // Simple implementation
      String[] tokens = ((String)source).split(" ");
      int i = 0;
      if(tokens.length == 4) {
         i = 1;
      }
      result = toClass.cast(new PhoneNumber(
         Integer.parseInt(tokens[i]),
         Integer.parseInt(tokens[i+1]),
         Integer.parseInt(tokens[i+2])));
      }
      return result;
   }
}

最后,你在配置中绑定所有东西,如清单 8-24 所示。

清单 8-24。

<?xml version="1.0" encoding="UTF-8"?>
<beans http://www.springframework.org/schema/beans">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="serverType" value="OPENDJ" />
   </bean>
   <bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImpl">
      <constructor-arg name="converterManager" ref="converterManager" />
      <constructor-arg name="contextSource" ref="contextSource" />
      <constructor-arg name="managedClasses">
         <set>
            <value>com.inflinx.book.ldap.custom.Patron</value>
         </set>
      </constructor-arg>
   </bean>
   <bean id="fromStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter" />
   <bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter" />
   <bean id="fromPhoneNumberConverter" class="com.inflinx.book.ldap.custom.FromPhoneNumberConverter" />
   <bean id="toPhoneNumberConverter" class="com.inflinx.book.ldap.custom.ToPhoneNumberConverter" />

   <!-- Configuration information for a single instance of FromString -->
   <bean id="fromStringConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="fromStringConverter" />
      <property name="fromClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.Byte</value>
            <value>java.lang.Integer</value>
            <value>java.lang.Boolean</value>
         </set>
      </property>
   </bean>
   <bean id="fromPhoneNumberConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="fromPhoneNumberConverter" />
      <property name="fromClasses">
         <set>
            <value>com.inflinx.book.ldap.custom.PhoneNumber</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
   </bean>
   <bean id="toPhoneNumberConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="toPhoneNumberConverter" />
      <property name="fromClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>com.inflinx.book.ldap.custom.PhoneNumber</value>
         </set>
      </property>
   </bean>
   <bean id="toStringConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="toStringConverter"/>
      <property name="fromClasses">
         <set>
            <value>java.lang.Byte</value>
            <value>java.lang.Integer</value>
            <value>java.lang.Boolean</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
   </bean>
   <bean id="converterManager" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean">
      <property name="converterConfig">
         <set>
            <ref bean="fromPhoneNumberConverterConfig"/>
            <ref bean="toPhoneNumberConverterConfig"/>
            <ref bean="fromStringConverterConfig"/>
            <ref bean="toStringConverterConfig"/>
         </set>
      </property>
   </bean>
</beans>

用于测试新增转换器的修改后的测试用例如清单 8-25 所示。

清单 8-25。

@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration("classpath:repositoryContext-test3.xml")
public class PatronServiceImplCustomTest {

   @Autowired
   private PatronService patronService;
   private static final String PORT = "12389";
   private static final String ROOT_DN = "dc=inflinx,dc=com";

   @Before
   public void setup() throws Exception {
      System.out.println("Inside the setup");
      LdapUnitUtils.loadData(new ClassPathResource("patrons.ldif"), PORT);
   }

   @After
   public void teardown() throws Exception {
      System.out.println("Inside the teardown");
      LdapUnitUtils.clearSubContexts(new DistinguishedName(ROOT_DN), PORT);
   }

   @Test
   public void testService() {
      Patron patron = new Patron();
      patron.setDn(new DistinguishedName("uid=patron10001," + "ou=patrons,      dc=inflinx,dc=com"));
      patron.setFirstName("Patron"); patron.setLastName("Test 1");
      patron.setFullName("Patron Test 1");
      patron.setMail("balaji@inflinx.com" );
      patron.setEmployeeNumber(1234);
      patron.setPhoneNumber(new PhoneNumber(801, 864, 8050));
      patronService.create(patron);

      // Lets read the patron
      patron = patronService.find("patron10001");
      assertNotNull(patron);

          System.out.println(patron.getPhoneNumber());
      patron.setPhoneNumber(new PhoneNumber(435, 757, 9369));
      patronService.update(patron);

          System.out.println("updated phone: " + patron.getPhoneNumber());
      patron = patronService.find("patron10001");

          System.out.println("Read the phone number: " + patron.getPhoneNumber());
      assertEquals(patron.getPhoneNumber(), new PhoneNumber(435, 757, 9369));

          patronService.delete("patron10001");
      try {
         patron = patronService.find("patron10001");
         assertNull(patron);
      }
      catch(NameNotFoundException e) {
      }
   }
}

摘要

Spring LDAP 的对象-目录映射(ODM)在对象和目录模型之间架起了一座桥梁。在这一章中,你学习了 ODM 的基础知识,并且看了定义 ODM 映射的注释。然后,您深入研究了 ODM 框架,构建了顾客服务和定制转换器。

到目前为止,您已经创建了几种不同的服务和 DAO 实现。在下一章中,您将探索 Spring LDAP 对事务的支持。

九、LDAP 事务

在本章中,您将学习

  • 事务的基础。
  • Spring 事务抽象。
  • 对事务的 Spring LDAP 支持。

事务基础

事务是企业应用不可或缺的一部分。简而言之,事务是一系列一起执行的操作。要完成或提交事务,其所有操作都必须成功。如果由于任何原因,一个操作失败,整个事务将失败并回滚。在这种情况下,所有之前成功的操作都必须撤销。这确保了结束状态与事务开始之前的状态相匹配。

在您的日常生活中,您总是会遇到事务。考虑一个在线银行场景,您希望将 300 美元从您的储蓄账户转移到您的支票账户。这一操作包括借记储蓄账户 300 美元,贷记支票账户 300 美元。如果操作的借记部分成功了,而贷记部分失败了,你的合并账户将会减少 300 美元。(理想情况下,我们都希望借方操作失败,贷方操作成功,但银行可能第二天就来敲我们的门。)银行通过使用事务来确保账户永远不会处于这种不一致的状态。

事务通常与以下四个众所周知的特征相关联,这些特征通常被称为 ACID 属性:

  • 原子性: 该属性确保事务完全执行或者根本不执行。所以在上面的例子中,我们要么成功转账,要么转账失败。这种全有或全无的属性也被称为单一工作单元或逻辑工作单元。

  • 一致性: 该属性确保事务在完成后以一致的状态离开系统。例如,对于数据库系统,这意味着满足所有的完整性约束,如主键或参照完整性。

  • Isolation: This property ensures that a transaction executes independent of other parallel transactions. Changes or side effects of a transaction that has not yet completed will never be seen by other transactions. In the money transfer scenario, another owner of the account will only see the balances before or after the transfer. They will never be able to see the intermediate balances no matter how long the transaction takes to complete. Many database systems relax this property and provide several levels of isolation. Table 9-1 lists the primary transaction levels and descriptions. As the isolation level increases, transaction concurrency decreases and transaction consistency increases.

    表 9-1 。隔离级别

    隔离级别 描述
    未提交读取 这种隔离级别允许正在运行的事务看到其他未提交的事务所做的更改。此事务所做的更改甚至在它完成之前就对其他事务可见。这是最低级别的隔离,可以更恰当地认为是缺乏隔离。因为它完全违背了 ACID 的一个属性,所以大多数数据库供应商都不支持它。
    已提交读取 此隔离级别允许正在运行的事务中的查询仅查看查询开始前提交的数据。但是,在查询执行期间,所有未提交的更改或由并发事务提交的更改将不会被看到。这是大多数数据库(包括 Oracle、MySQL 和 PostgreSQL)的默认隔离级别。
    可重复读 此隔离级别允许正在运行的事务中的查询在每次执行时读取相同的数据。为了实现这一点,事务获取所有被检查的行上的锁(不仅仅是获取),直到它完成。
    可序列化 这是所有隔离级别中最严格和最昂贵的。交叉事务被堆叠起来,以便事务被一个接一个地执行,而不是并发地执行。使用这种隔离级别,查询将只能看到在事务开始之前提交的数据,而永远看不到未提交的更改或并发事务提交的数据。
  • 持久性: 这个属性确保提交的事务的结果不会因为失败而丢失。回到银行转帐的场景,当您收到转帐成功的确认时,耐久性属性确保此更改成为永久的。

本地与全球事务

根据参与事务的资源数量,事务通常分为本地事务或全局事务。这些资源的例子包括数据库系统或 JMS 队列。JDBC 驱动程序等资源管理器通常用于管理资源。

本地事务是涉及单个资源的事务。最常见的例子是与单个数据库相关联的事务。这些事务通常通过用于访问资源的对象来管理。在 JDBC 数据库事务的情况下,java.sql.Connection 接口的实现用于访问数据库。这些实现还提供了用于管理事务的提交和回滚方法。对于 JMS 队列,javax.jms.Session 实例提供了控制事务的方法。

另一方面,全局事务处理多个资源。例如,可以使用一个全局事务从 JMS 队列中读取一条消息,并在一个事务中将一条记录写入数据库。

使用资源外部的事务管理器来管理全局事务。它负责与资源管理器通信,并对分布式事务做出最终的提交或回滚决定。在 Java/JEE 中,使用 Java 事务 API (JTA)实现全局事务。JTA 为事务管理器和事务参与组件提供了标准接口。

事务管理器采用“两阶段提交”协议来协调全局事务。顾名思义,两阶段提交协议有以下两个阶段:

  • 准备阶段:在这个阶段,询问所有参与的资源管理器是否准备好提交他们的工作。收到请求后,资源管理器尝试记录它们的状态。如果成功,资源管理器会积极响应。如果无法提交,资源管理器会做出否定响应,并回滚本地更改。
  • 提交阶段:如果事务管理器收到所有肯定的响应,它就提交事务,并通知所有参与者提交。如果收到一个或多个否定响应,它将回滚整个事务并通知所有参与者。

两阶段提交协议如图 9-1 所示。

9781430263975_Fig09-01.jpg

图 9-1 。两阶段提交协议

编程式与声明式事务

在向应用添加事务功能时,开发人员有两种选择。

程序化

在这个场景中,用于启动、提交或回滚事务的事务管理代码围绕着业务代码。这可以提供极大的灵活性,但也会使维护变得困难。以下代码给出了一个使用 JTA 和 EJB 3.0 的编程事务的示例:

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class OrderManager {

   @Resource
   private UserTransaction transaction;

   public void create(Order order) {
   try {
      transaction.begin();
      // business logic for processing order
      verifyAddress(order);
          processOrder(order);
      sendConfirmation(order);
      transaction.commit();
   }
   catch(Exception e) {
      transaction.rollback();
   }
   }
}

声明性地

在这个场景中,容器负责启动、提交或回滚事务。开发人员通常通过注释或 XML 来指定事务行为。这个模型清楚地将事务管理代码与业务逻辑分开。以下代码给出了一个使用 JTA 和 EJB 3.0 的声明性事务的示例。订单处理过程中发生异常时,调用会话上下文上的 setRollbackOnly 方法;这标志着事务必须回滚。

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class OrderManager {

   @Resource
   private SessionContext context;

   @TransactionAttribute(TransactionAttributeType.REQUIRED)
   public void create(Order order) {
   try {
      // business logic for processing order
      verifyAddress(order);
          processOrder(order);
      sendConfirmation(order);
   }
   catch(Exception e) {
      context.setRollbackOnly();
   }
   }
}

Spring 事务抽象

Spring 框架为处理全局和本地事务提供了一致的编程模型。事务抽象隐藏了不同事务 API(如 JTA、JDBC、JMS 和 JPA)的内部工作方式,并允许开发人员以环境中立的方式编写支持事务的代码。在幕后,Spring 只是将事务管理委托给底层的事务提供者。不需要任何 EJB 就可以支持编程式和声明式事务管理模型。通常推荐使用声明式方法,这也是我们将在本书中使用的方法。

Spring 事务管理的核心是 PlatformTransactionManager 抽象。它以独立于技术的方式公开了事务管理的关键方面。它负责创建和管理事务,对于声明性和编程性事务都是必需的。这个接口的几个实现,比如 JtaTransactionManager、DataSourceTransactionManager 和 JmsTransactionManager,都是现成可用的。平台事务管理器 API 如清单 9-1 所示。

清单 9-1。

package org.springframework.transaction;

public interface PlatformTransactionManager {

   TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
   void commit(TransactionStatus status) throws TransactionException;
   void rollback(TransactionStatus status) throws TransactionException;
   String getName();
}

PlatformTransactionManager 中的 getTransaction 方法用于检索现有事务。如果未找到活动事务,此方法可能会基于 TransactionDefinition 实例中指定的事务属性创建一个新事务。下面是 TransactionDefinition 接口抽象的属性列表:

  • 只读:该属性表示该事务是否只读。
  • 超时:该属性规定了事务必须完成的时间。如果事务未能在指定时间内完成,它将自动回滚。
  • 隔离:该属性控制事务之间的隔离程度。可能的隔离级别在表 9-1 中讨论。
  • 传播:考虑这样一个场景,存在一个活动事务,Spring 遇到需要在事务中执行的代码。该场景中的一个选项是执行现有事务中的代码。另一种选择是挂起现有的事务,并启动一个新的事务来执行代码。传播属性可用于定义此类事务行为。可能的值包括 PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_SUPPORTS 等。

getTransaction 方法返回表示当前事务状态的 TransactionStatus 的实例。应用代码可以使用这个接口来检查这是一个新的事务还是事务已经完成。该接口还可以用于以编程方式请求事务回滚。PlatformTransactionManager 中的另外两个方法是 commit 和 rollback,顾名思义,它们可用于提交或回滚事务。

使用 Spring 的声明性事务

Spring 提供了两种以声明方式向应用添加事务行为的方法:纯 XML 和注释。注释方法非常流行,并且极大地简化了配置。为了演示声明性事务,考虑在数据库的 Person 表中插入新记录的简单场景。清单 9-2 给了 PersonRepositoryImpl 类一个实现这个场景的创建方法。

清单 9-2。

import org.springframework.jdbc.core.JdbcTemplate;

public class PersonRepositoryImpl implements PersonRepository {

   private JdbcTemplate jdbcTemplate;

   public void create(String firstName, String lastName) {
      String sql = "INSERT INTO PERSON (FIRST_NAME, " + "LAST_NAME) VALUES (?, ?)";
      jdbcTemplate.update(sql, new Object[]{firstName, lastName});
   }
}

清单 9-3 显示了上面的类实现的 PersonRepository 接口。

清单 9-3。

public interface PersonRepository {

   public void create(String firstName, String lastName);

}

下一步是使创建方法成为事务性的。这可以通过简单地用@Transactional 注释方法来完成,如清单 9-4 所示。(注意,我注释了实现中的方法,而不是接口中的方法。)

清单 9-4。

import org.springframework.transaction.annotation.Transactional;

public class PersonRepositoryImpl implements PersonRepository {
   ...........
   @Transactional
   public void create(String firstName, String lastName) {
   ...........
   }
}

@Transactional 注释有几个属性可用于指定附加信息,如传播和隔离。清单 9-5 显示了默认隔离的方法,并要求新的传播。

清单 9-5。

@Transactional(propagation=Propagation.REQUIRES_NEW, isolation=Isolation.DEFAULT)
public void create(String  firstName, String lastName) {
}

下一步是指定一个事务管理器供 Spring 使用。由于您要处理的是单个数据库,所以清单 9-6 中显示的 org . spring framework . JDBC . data source . data source eTransactionManager 非常适合您的情况。从清单 9-6 中,您可以看到 data sourcetransactionmanager 需要一个数据源来获取和管理到数据库的连接。

清单 9-6。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSource"/>
</bean>

声明式事务管理的完整应用上下文配置文件在清单 9-7 中给出。

清单 9-7。

<?xml version="1.0" encoding="UTF-8"?>
<beans http://www.springframework.org/schema/beans">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
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
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/tx/spring-aop.xsd">

   <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource" ref="dataSource"/>
   </bean>
   <tx:annotation-driven transaction-manager="transactionManager"/>
   <aop:aspectj-autoproxy />

</beans>

标记表明您正在使用基于注释的事务管理。这个标签和一起,指示 Spring 使用面向方面编程(AOP)并创建代理来代表带注释的类管理事务。因此,当调用事务性方法时,代理会截获调用,并使用事务管理器来获取事务(新的或现有的)。然后调用被调用的方法,如果该方法成功完成,使用事务管理器的代理将提交事务。如果方法失败,抛出异常,事务将被回滚。这种基于 AOP 的事务处理如图图 9-2 所示。

9781430263975_Fig09-02.jpg

图 9-2 。基于 AOP 的 Spring 事务

LDAP 事务支持

LDAP 协议要求所有 LDAP 操作(如修改或删除)都遵循 ACID 属性。这种事务行为确保了存储在 LDAP 服务器中的信息的一致性。但是,LDAP 不定义跨多个操作的事务。考虑这样一个场景,您希望将两个 LDAP 条目添加为一个原子操作。操作的成功完成意味着两个条目都被添加到 LDAP 服务器中。如果失败,其中一个条目无法添加,服务器将自动撤销另一个条目的添加。这种事务行为不是 LDAP 规范的一部分,也不存在于 LDAP 世界中。此外,缺少事务语义(如提交和回滚)使得跨多个 LDAP 服务器确保数据一致性成为不可能。

尽管事务不是 LDAP 规范的一部分,但 IBM Tivoli Directory Server 和 ApacheDS 等服务器提供了事务支持。IBM Tivoli Directory Server 支持的 Begin transaction(OID 1 . 3 . 18 . 0 . 2 . 12 . 5)和 End transaction(OID 1 . 3 . 18 . 0 . 2 . 12 . 6)扩展控件可用于区分事务内部的一组操作。RFC 5805(tools.ietf.org/html/rfc5805)试图标准化 LDAP 中的事务,目前正处于试验阶段。

Spring LDAP 事务支持

起初,LDAP 中缺少事务似乎令人惊讶。更重要的是,它会成为企业广泛采用目录服务器的障碍。为了解决这个问题,Spring LDAP 提供了非 LDAP/JNDI 特定的补偿事务支持。这种事务支持与您在前面章节中看到的 Spring 事务管理基础设施紧密集成。图 9-3 显示了负责 Spring LDAP 事务支持的组件。

9781430263975_Fig09-03.jpg

图 9-3 。Spring LDAP 事务支持

ContextSourceTransactionManager 类实现 PlatformTransactionManager,并负责管理基于 LDAP 的事务。这个类及其合作者跟踪事务内部执行的 LDAP 操作,并记录每个操作之前的状态。如果事务回滚,事务管理器将采取措施恢复原始状态。为了实现这种行为,事务管理器使用 transactionanawarecontextsourceproxy,而不是直接使用 LdapContextSource。这个代理类还确保在整个事务中使用单个 javax . naming . directory . dir context 实例,并且在事务完成之前不会被关闭。

补偿事务

补偿事务撤销先前提交的事务的影响,并将系统恢复到先前的一致状态。考虑一个涉及预订机票的事务。在这种情况下,补偿事务是取消预订的操作。在 LDAP 的情况下,如果一个操作添加了一个新的 LDAP 条目,相应的补偿事务只是删除那个条目。

补偿事务对于 LDAP 和 web 服务等不提供任何标准事务支持的资源非常有用。但是,重要的是要记住,补偿事务提供了一种假象,永远无法取代真实的事务。因此,如果在补偿事务完成之前服务器崩溃或与 LDAP 服务器的连接丢失,您将会得到不一致的数据。此外,由于事务已经提交,并发事务可能会看到无效数据。补偿事务会导致额外的开销,因为客户端必须处理额外的撤销操作。

为了更好地理解 Spring LDAP 事务,让我们创建一个具有事务行为的顾客服务。清单 9-8 显示了只有一个创建方法的父服务接口。

清单 9-8。

package com.inflinx.book.ldap.transactions;

import com.inflinx.book.ldap.domain.Patron;

public interface PatronService {
   public void create(Patron patron);
}

清单 9-9 展示了这个服务接口的实现。create 方法实现只是将调用委托给 DAO 层。

清单 9-9。

package com.inflinx.book.ldap.transactions;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.inflinx.book.ldap.domain.Patron;

@Service("patronService")
@Transactional
public class PatronServiceImpl implements PatronService {

   @Autowired
   @Qualifier("patronDao")
   private PatronDao patronDao;

   @Override
   public void create(Patron patron) {
      patronDao.create(patron);
   }
}

注意在类声明的顶部使用了@Transactional 注释。清单 9-10 和清单 9-11 分别显示了 PatronDao 接口及其实现 PatronDaoImpl。

清单 9-10。

package com.inflinx.book.ldap.transactions;

import com.inflinx.book.ldap.domain.Patron;

public interface PatronDao {
   public void create(Patron patron);
}

清单 9-11。

package com.inflinx.book.ldap.transactions;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.stereotype.Repository;
import com.inflinx.book.ldap.domain.Patron;

@Repository("patronDao")
public class PatronDaoImpl implements PatronDao {

   private static final String PATRON_BASE = "ou=patrons,dc=inflinx,dc=com";

   @Autowired
   @Qualifier("ldapTemplate")
   private LdapTemplate ldapTemplate;

   @Override
   public void create(Patron patron) {
      System.out.println("Inside the create method ...");
      DistinguishedName dn = new DistinguishedName(PATRON_BASE);
      dn.add("uid", patron.getUserId());
      DirContextAdapter context = new DirContextAdapter(dn);
      context.setAttributeValues("objectClass", new String[]
              {"top", "uidObject", "person", "organizationalPerson", "inetOrgPerson"});
      context.setAttributeValue("sn", patron.getLastName());
      context.setAttributeValue("cn", patron.getCn());
      ldapTemplate.bind(context);
   }
}

正如您在这两个清单中看到的,您创建了 Patron DAO 及其实现,遵循了在第五章中讨论的概念。下一步是创建一个 Spring 配置文件,它将自动连接组件,并将包含事务语义。清单 9-12 给出了配置文件的内容。这里您使用的是本地安装的 OpenDJ LDAP 服务器。

清单 9-12。

<?xml version="1.0" encoding="UTF-8"?>
<beans http://www.springframework.org/schema/beans">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
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 http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

   <context:component-scan base-package="com.inflinx.book.ldap" />
   <bean id="contextSourceTarget" 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" />
      <property name="base" value=""/>
   </bean>
   <bean id="contextSource" class="org.springframework.ldap.transaction.compensating.manager. TransactionAwareContextSourceProxy">
      <constructor-arg ref="contextSourceTarget" />
   </bean>
   <bean id="ldapTemplate" class="org.springframework.ldap. core.LdapTemplate">
      <constructor-arg ref="contextSource" />
   </bean>
   <bean id="transactionManager" class="org.springframework.ldap.transaction.compensating.manager.ContextSourceTransactionManager">
      <property name="contextSource" ref="contextSource" />
   </bean>
      <tx:annotation-driven transaction-manager="transactionManager" />
 </beans>

在这个配置中,首先定义一个新的 LdapContextSource,并向它提供您的 LDAP 信息。到目前为止,您使用 id contextSource 引用了这个 bean,并注入它供 LdapTemplate 使用。但是,在这个新配置中,您将它称为 contextSourceTarget。然后,配置 transactionawarenecontextsourceproxy 的一个实例,并将 contextSource bean 注入其中。这个新配置的 transactionanawarecontextsourceproxy bean 的 id 为 contextSource,由 LdapTemplate 使用。最后,使用 ContextSourceTransactionManager 类配置事务管理器。如前所述,这种配置允许在单个事务中使用单个 DirContext 实例,从而支持事务提交/回滚。

有了这些信息,让我们来验证您的创建方法和配置在事务回滚期间的行为是否正确。为了模拟事务回滚,让我们修改 PatronServiceImpl 类中的 create 方法,以抛出 RuntimeException ,如下所示:

@Override
public void create(Patron  patron)  {
    patronDao.create(patron);
    throw new  RuntimeException(); // Will roll  back the  transaction
}

验证预期行为的下一步是编写一个测试用例,调用 PatronServiceImpl 的 create 方法来创建一个新的 Patron。测试用例如清单 9-13 所示。repositoryContext-test.xml 文件包含清单 9-12 中定义的 xml 配置。

清单 9-13。

package com.inflinx.book.ldap.transactions;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:repositoryContext-test.xml")
public class PatronServiceImplTest {

   @Autowired
   private PatronService patronService;

   @Test(expected=RuntimeException.class)
   public void testCreate() {
      Patron patron = new Patron();
      patron.setUserId("patron10001");
      patron.setLastName("Patron10001");
      patron.setCn("Test Patron10001");
      patronService.create(patron);
   }
}

当您运行测试时,Spring LDAP 应该创建一个新的 patron 然后,在回滚事务时,它将删除新创建的顾客。通过查看 OpenDJ 日志文件,可以看到 Spring LDAP 的补偿事务的内部工作方式。该日志文件被命名为 access ,位于 OPENDJ_INSTALL\logs 文件夹中。

清单 9-14 显示了这个创建操作的日志文件的一部分。您会注意到,当 PatronDaoImpl 上的 create 方法被调用时,“ADD REQ”命令被发送到 OpenDJ 服务器,以添加新的 Patron 条目。当 Spring LDAP 回滚事务时,会发送一个新的“DELETE REQ”命令来删除条目。

清单 9-14。

[14/Sep/2013:15:03:09 -0600] CONNECT conn=52 from=127.0.0.1:54792 to=127.0.0.1:11389 protocol=LDAP
[14/Sep/2013:15:03:09 -0600] BIND REQ conn=52 op=0 msgID=1 type=SIMPLE dn="cn=Directory Manager"
[14/Sep/2013:15:03:09 -0600] BIND RES conn=52 op=0 msgID=1 result=0 authDN="cn=Directory Manager,cn=Root DNs,cn=config" etime=0
[14/Sep/2013:15:03:09 -0600] ADD REQconn=52 op=1 msgID=2 dn="uid=patron10001,ou=patrons,dc=inflinx,dc=com"
[14/Sep/2013:15:03:09 -0600] ADD RES conn=52 op=1 msgID=2 result=0 etime=2
[14/Sep/2013:15:03:09 -0600] DELETE REQconn=52 op=2 msgID=3 dn="uid=patron10001,ou=patrons,dc=inflinx,dc=com"
[14/Sep/2013:15:03:09 -0600] DELETE RES conn=52 op=2 msgID=3 result=0 etime=4
[14/Sep/2013:15:03:09 -0600] UNBIND REQ conn=52 op=3 msgID=4
[14/Sep/2013:15:03:09 -0600] DISCONNECT conn=52 reason="Client Unbind""

这个测试验证了 Spring LDAP 的补偿事务基础设施会自动删除新添加的条目,如果事务因为任何原因回滚的话。

现在让我们继续实现 PatronServiceImpl 方法并验证它们的事务行为。清单 9-15 和清单 9-16 分别展示了添加到 PatronService 接口和 PatronServiceImpl 类中的删除方法。同样,实际的 delete 方法实现很简单,只需要调用 PatronDaoImpl 的 delete 方法。

清单 9-15。

public interface PatronDao {
   public void create(Patron patron);
   public void delete(String id) ;
}

清单 9-16。

// Import and annotations remvoed for brevity
public class PatronServiceImpl implements PatronService {

   // Create method removed for brevity
   @Override
   public void delete(String id) {
      patronDao.delete(id);
   }
}

清单 9-17 显示了 PatronDaoImpl 的删除方法实现。

清单 9-17。

// Annotation and imports removed for brevity
public class PatronDaoImpl implements PatronDao {

   // Removed other methods for brevity
   @Override
   public void delete(String id) {
      DistinguishedName dn = new DistinguishedName(PATRON_BASE);
      dn.add("uid", id);
      ldapTemplate.unbind(dn);
   }
}

有了这段代码,让我们编写一个在事务中调用 delete 方法的测试用例。清单 9-18 显示了测试用例。“uid=patron98”是您的 OpenDJ 服务器中的一个现有条目,是在第三章的中的 LDIF 导入过程中创建的。

清单 9-18。

@Test
public void testDeletePatron() {
   patronService.delete("uid=patron98");
}

当您运行这个测试用例并在事务中调用 PatronServiceImpl 的 delete 方法时,Spring LDAP 的事务基础设施只是在新计算的临时 DN 下重命名条目。本质上,通过重命名,Spring LDAP 将您的条目移动到 LDAP 服务器上的不同位置。成功提交后,临时条目将被删除。回滚时,条目被重命名,因此将从临时位置移动到其原始位置。

现在,运行该方法并观察 OpenDJ 下的访问日志。清单 9-19 显示了删除操作的日志文件部分。请注意,删除操作会产生一个“MODIFYDN REQ”命令,该命令将被删除的条目重命名。成功提交后,通过“DELETE REQ”命令删除重命名的条目。

清单 9-19。

[[14/Sep/2013:16:21:56 -0600] CONNECT conn=54 from=127.0.0.1:54824 to=127.0.0.1:11389 protocol=LDAP
[14/Sep/2013:16:21:56 -0600] BIND REQ conn=54 op=0 msgID=1 type=SIMPLE dn="cn=Directory Manager"
[14/Sep/2013:16:21:56 -0600] BIND RES conn=54 op=0 msgID=1 result=0 authDN="cn=Directory Manager,cn=Root DNs,cn=config" etime=1
[14/Sep/2013:16:21:56 -0600] MODIFYDN REQconn=54 op=1 msgID=2 dn="uid=patron97,ou=patrons,dc=inflinx,dc=com" newRDN="uid=patron97_temp" deleteOldRDN=true newSuperior="ou=patrons,dc=inflinx,dc=com
[14/Sep/2013:16:21:56 -0600] MODIFYDN RES conn=54 op=1 msgID=2 result=0 etime=4
[14/Sep/2013:16:21:56 -0600] DELETE REQconn=54 op=2 msgID=3 dn="uid=patron97_temp,ou=patrons,dc=inflinx,dc=com"
[14/Sep/2013:16:21:56 -0600] DELETE RES conn=54 op=2 msgID=3 result=0 etime=2
[14/Sep/2013:16:21:56 -0600] UNBIND REQ conn=54 op=3 msgID=4
[14/Sep/2013:16:21:56 -0600] DISCONNECT conn=54 reason="Client Unbind"

现在,让我们为 PatronServiceImpl 类中的 delete 方法模拟一个回滚,如清单 9-20 所示。

清单 9-20。

public void delete(String id) {
   patronDao.delete(id);
   throw new RuntimeException(); // Need this to simulate a rollback
}

现在,让我们用一个新的顾客 Id 更新测试用例,您知道它仍然存在于 OpenDJ 服务器中,如清单 9-21 所示。

清单 9-21。

@Test(expected=RuntimeException.class)
public void testDeletePatron() {
   patronService.delete("uid=patron96");
}

运行这段代码时,预期的行为是 Spring LDAP 将通过更改 DN 来重命名 patron96 条目,然后在回滚时将它重新重命名为正确的 DN。清单 9-22 显示了上述操作的 OpenDJ 的访问日志。注意,删除操作首先通过发送第一个 MODIFYDN REQ 导致条目的重命名。回滚后,会发送第二个“MODIFYDN REQ”来将条目重命名回原始位置。

清单 9-22。

[14/Sep/2013:16:33:43 -0600] CONNECT conn=55 from=127.0.0.1:54829 to=127.0.0.1:11389 protocol=LDAP
[14/Sep/2013:16:33:43 -0600] BIND REQ conn=55 op=0 msgID=1 type=SIMPLE dn="cn=Directory Manager"
[14/Sep/2013:16:33:43 -0600] BIND RES conn=55 op=0 msgID=1 result=0 authDN="cn=Directory Manager,cn=Root DNs,cn=config" etime=0
[14/Sep/2013:16:33:43 -0600] MODIFYDN REQ conn=55 op=1 msgID=2 dn="uid=patron96,ou=patrons,dc=inflinx,dc=com" newRDN="uid=patron96_temp" deleteOldRDN=true newSuperior="ou=patrons,dc=inflinx,dc=com
[14/Sep/2013:16:33:43 -0600] MODIFYDN RES conn=55 op=1 msgID=2 result=0 etime=1
[14/Sep/2013:16:33:43 -0600] MODIFYDN REQ conn=55 op=2 msgID=3 dn="uid=patron96_temp,ou=patrons,dc=inflinx,dc=com" newRDN="uid=patron96" deleteOldRDN=true newSuperior="ou=patrons,dc=inflinx,dc=com
[14/Sep/2013:16:33:43 -0600] MODIFYDN RES conn=55 op=2 msgID=3 result=0 etime=0
[14/Sep/2013:16:33:43 -0600] UNBIND REQ conn=55 op=3 msgID=4
[14/Sep/2013:16:33:43 -0600] DISCONNECT conn=55 reason="Client Unbind"

对于更新操作,正如您现在已经猜到的那样,Spring LDAP 基础设施为条目上所做的修改计算补偿 ModificationItem 列表。在提交时,不需要做任何事情。但是在回滚时,计算出的补偿 ModificationItem 列表将被写回。

摘要

在这一章中,您探索了事务的基础知识,并查看了 Spring LDAP 的事务支持。在执行操作之前,Spring LDAP 会在 LDAP 树中记录状态。如果发生回滚,Spring LDAP 会执行补偿操作来恢复之前的状态。请记住,这种补偿性的事务支持给人一种原子性的错觉,但并不保证这一点。

在下一章中,您将探索其他 Spring LDAP 特性,如连接池和 LDIF 解析。

十、杂项

在本章中,您将学习

  • 如何使用 Spring LDAP 执行认证
  • 如何解析 LDIF 文件
  • LDAP 连接池

使用 Spring LDAP 进行身份验证

身份验证是针对 LDAP 服务器执行的常见操作。这通常包括根据目录服务器中存储的信息验证用户名和密码。

使用 Spring LDAP 实现身份验证的一种方法是通过 ContextSource 类的 getContext 方法。下面是 getContext 方法 API :

DirContext getContext(String  principal, String credentials) throws  NamingException

主体参数是用户的全限定 DN,凭证参数是用户的密码。该方法使用传入的信息针对 LDAP 进行身份验证。身份验证成功后,该方法返回表示用户条目的 DirContext 实例。身份验证失败通过异常传递给调用者。清单 10-1 给出了一个 DAO 实现,用于使用 getContext 技术在您的图书馆应用中认证顾客。

清单 10-1。

package com.inflinx.book.ldap.repository;

import javax.naming.directory.DirContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.stereotype.Repository;

@Repository("authenticationDao")
public class AuthenticationDaoImpl implements AuthenticationDao{

   public static final String BASE_DN = "ou=patrons,dc=inflinx,dc=com";

   @Autowired
   @Qualifier("contextSource")
   private ContextSource contextSource;

   @Override
   public boolean authenticate(String userid, String password) {
      DistinguishedName dn = new DistinguishedName(BASE_DN);
      dn.add("uid", userid);
      DirContext authenticatedContext = null;
      try {
         authenticatedContext = contextSource.getContext( dn.toString(), password);
         return true;
      }
      catch(NamingException e) {
         e.printStackTrace();
         return false;
      }
      finally {
         LdapUtils.closeContext(authenticatedContext);
      }
   }
}

getContext 方法需要用户条目的完全限定 DN。因此,身份验证方法首先创建一个 DistinguishedName 实例,该实例具有提供的“ou = customers,dc=inflinx,dc=com”基。然后将提供的 userid 附加到 DN 上,创建顾客的完全合格的 DN。身份验证方法然后调用 getContext 方法,传入顾客的 DN 和密码的字符串表示。成功的身份验证只需退出该方法,返回值为 true。注意,在 finally 块中,您关闭了获得的上下文。

清单 10-2 显示了一个 JUnit 测试来验证这个认证方法的正常工作。

清单 10-2。

package com.inflinx.book.ldap.parser;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.inflinx.book.ldap.repository.AuthenticationDao;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:repositoryContext-test.xml")

public class AuthenticationDaoTest {

@Autowired
@Qualifier("authenticationDao")
private AuthenticationDao authenticationDao;

   @Test
   public void testAuthenticate() {
      boolean authResult = authenticationDao.authenticate("patron0", "password");
      Assert.assertTrue(authResult);
      authResult = authenticationDao.authenticate("patron0", "invalidPassword");
      Assert.assertFalse(authResult);
   }
}

与清单 10-2 中的相关联的 repositoryContext-test.xml 显示在清单 10-3 中的中。在这个场景中,您正在使用您安装的 OpenDJ LDAP 服务器。

清单 10-3。

<?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="userDn" value="cn=Directory Manager" />
      <property name="password" value="opendj" />
      <property name="base" value=""/>
   </bean>
   <bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
      <constructor-arg ref="contextSource" />
   </bean>
</beans>

清单 10-3 中显示的实现的唯一缺点是 getContext 方法需要顾客条目的完全限定 DN 。可能会出现客户端代码不知道用户的完全限定 DN 的情况。在清单 10-1 中,您添加了一个硬编码值来创建完全限定的 DN。如果你想开始使用清单 10-1 中的代码来验证你的库的雇员,这种方法将会失败。为了解决这种情况,Spring LDAP 向 LdapTemplate 类添加了如下所示的 authenticate 方法的几种变体:

boolean authenticate(String base, String filter,  String password)

这个身份验证方法使用提供的基本 DN 和过滤器参数来搜索用户的 LDAP 条目。如果找到条目,则提取用户的全限定 DN。然后,这个 DN 和密码一起被传递给 ContextSource 的 getContext 方法来执行身份验证。本质上,这是一个两步过程,但它减少了预先完全合格的 DN 的需要。清单 10-4 包含了修改后的认证实现。注意,DAO 实现中的 authenticate 方法签名没有改变。它仍然接受用户名和密码作为参数。但是由于身份验证方法抽象,实现变得简单多了。该实现传递一个空的基本 DN,因为您希望相对于在 ContextSource 创建期间使用的基本 DN 执行搜索。

清单 10-4。

package com.inflinx.book.ldap.repository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.stereotype.Repository;

@Repository("authenticationDao2")
public class AuthenticationDaoImpl2 implements AuthenticationDao {

   @Autowired
   @Qualifier("ldapTemplate")
   private LdapTemplate ldapTemplate;

   @Override
   public boolean authenticate(String userid, String password){
      return ldapTemplate.authenticate("","(uid=" + userid + ")", password);
   }
}

清单 10-5 显示了 JUnit 测试用例来验证上述认证方法的实现。

清单 10-5。

package com.inflinx.book.ldap.parser;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.inflinx.book.ldap.repository.AuthenticationDao;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:repositoryContext-test.xml")
public class AuthenticationDao2Test {

@Autowired
@Qualifier("authenticationDao2")
private AuthenticationDao authenticationDao;

   @Test
   public void testAuthenticate() {
      boolean authResult = authenticationDao.authenticate("patron0", "password");
      Assert.assertTrue(authResult);
      authResult = authenticationDao.authenticate("patron0","invalidPassword");
      Assert.assertFalse(authResult);
   }
}

处理身份验证异常

LdapTemplate 中前面的 authenticate 方法只是告诉您身份验证是成功还是失败。有些情况下,您会对导致失败的实际异常感兴趣。对于这些场景,LdapTemplate 提供了 authenticate 方法的重载版本。重载认证方法之一的 API 如下:

boolean authenticate(String base, String filter,  String  password, AuthenticationErrorCallback errorCallback);

在执行上述 authenticate 方法期间发生的任何异常将被传递给作为方法参数提供的 AuthenticationErrorCallback 实例。这个收集的异常可以被记录或用于身份验证后的过程。清单 10-6 和清单 10-7 分别展示了 AuthenticationErrorCallback API 及其简单实现。回调中的 execute 方法可以决定如何处理引发的异常。在您的简单实现中,您只是存储它并让它对 LdapTemplate 的搜索调用者可用。

清单 10-6。

package org.springframework.ldap.core;

public interface AuthenticationErrorCallback {
   public void execute(Exception e);
}

清单 10-7。

package com.practicalspring.ldap.repository;

import org.springframework.ldap.core.AuthenticationErrorCallback;

public class EmployeeAuthenticationErrorCallback implements AuthenticationErrorCallback {

   private Exception authenticationException;

   @Override
   public void execute(Exception e) {
      this.authenticationException = e;
   }

   public Exception getAuthenticationException() {
      return authenticationException;
   }
}

清单 10-8 显示了修改后的 AuthenticationDao 实现以及错误回调;这里,您只是将失败的异常记录到控制台。清单 10-9 展示了 JUnit 测试。

清单 10-8。

package com.practicalspring.ldap.repository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.stereotype.Repository;

@Repository("authenticationDao3")
public class AuthenticationDaoImpl3 implements AuthenticationDao {

@Autowired
@Qualifier("ldapTemplate")
private LdapTemplate ldapTemplate;

   @Override
   public boolean authenticate(String userid, String password){
      EmployeeAuthenticationErrorCallback errorCallback = new EmployeeAuthenticationErrorCallback();
      boolean isAuthenticated = ldapTemplate.authenticate("","(uid=" + userid + ")", password, errorCallback);
      if(!isAuthenticated) {
         System.out.println(errorCallback.getAuthenticationException());
      }
      return isAuthenticated;
   }
}

清单 10-9

package com.inflinx.book.ldap.parser;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.inflinx.book.ldap.repository.AuthenticationDao;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:repositoryContext-test.xml")
public class AuthenticationDao3Test {

@Autowired
@Qualifier("authenticationDao3")
private AuthenticationDao authenticationDao;

   @Test
   public void testAuthenticate() {
      boolean authResult = authenticationDao.authenticate("patron0", "invalidPassword");
      Assert.assertFalse(authResult);
   }
}

在运行清单 10-9 中的 JUnit 测试时,您应该在控制台中看到以下错误消息:

org . spring framework . LDAP . authenticationexception:[LDAP:错误代码 49 -无效凭据];嵌套异常是 javax . naming . authenticationexception:[LDAP:错误代码 49 -无效凭据]

解析 LDIF 数据

LDAP 数据交换格式是一种基于标准的数据交换格式,用于以平面文件格式表示 LDAP 目录数据。LDIF 在第一章中有详细论述。作为 LDAP 开发人员或管理员,您有时可能需要解析 LDIF 文件并执行诸如批量目录加载之类的操作。对于这样的场景,Spring LDAP 在 org.springframework.ldap.ldif 包及其子包中引入了一组类,使得读取和解析 ldif 文件变得容易。

org . spring framework . LDAP . ldif . Parser 包的核心是解析器接口及其默认实现 LdifParser。LdifParser 负责从 LDIF 文件中读取单独的行,并将它们转换成 Java 对象。这种对象表示可以通过两个新添加的类来实现,即 LdapAttribute 和 LdapAttributes。

清单 10-10 中的代码使用 LdifParser 读取并打印 LDIF 文件中的记录总数。通过创建 LdifParser 的一个实例并传入您想要解析的文件来开始实现。在使用解析器之前,您需要打开它。然后,使用解析器的迭代器风格接口来读取和计数单个记录。

清单 10-10。

package com.inflinx.book.ldap.parser;

import java.io.File;
import java.io.IOException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ldap.core.LdapAttributes;
import org.springframework.ldap.ldif.parser.LdifParser;

public class SimpleLdifParser {

   public void parse(File file) throws IOException {
      LdifParser parser = new LdifParser(file);
      parser.open();
      int count = 0;
      while(parser.hasMoreRecords()) {
         LdapAttributes attributes = parser.getRecord();
         count ++;
      }
      parser.close();
      System.out.println(count);
   }

   public static void main(String[] args) throws IOException {
      SimpleLdifParser parser = new SimpleLdifParser();
      parser.parse(new ClassPathResource("patrons.ldif").getFile());
   }
}

在运行上面的类之前,请确保在类路径中有 customers . ldif 文件。在运行包含在第十章代码中的 customers . ldif 文件的类时,您应该看到 count 103 被打印到控制台上。

LdifParser 的解析实现依赖于三个支持策略定义:分隔符策略、属性验证策略和记录规范策略。

  • 分隔符策略为文件中的 LDIF 记录提供分隔规则,并在 RFC 2849 中定义。它是通过 org . spring framework . LDAP . ldif . support . separator policy 类实现的。
  • 顾名思义,属性验证策略用于确保在解析之前,所有属性在 LDIF 文件中的结构正确。它是通过 AttributeValidationPolicy 接口和 DefaultAttributeValidationPolicy 类实现的。这两个位于 org . spring framework . LDAP . ldif . support 包中。根据 RFC 2849,DefaultAttributeValidationPolicy 使用正则表达式来验证属性格式。
  • 记录规范策略用于验证每个 LDIF 记录必须遵守的规则。Spring LDAP 为这个策略提供了规范接口和两个实现:org . spring framework . LDAP . schema . DefaultSchemaSpecification 和 org . spring framework . LDAP . schema . basicschemaspecification . DefaultSchemaSpecification 有一个空的实现,并不真正验证记录。BasicSchemaSpecification 可用于执行基本检查,例如每个 LAP 条目必须存在一个对象类。对于大多数情况,basic schema 规范就足够了。

清单 10-11 中给出了修改后的解析方法实现,以及三个策略定义。

清单 10-11。

package com.inflinx.book.ldap.parser;

import java.io.File;
import java.io.IOException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ldap.core.LdapAttributes;
import org.springframework.ldap.ldif.parser.LdifParser;
import org.springframework.ldap.ldif.support.DefaultAttributeValidationPolicy;
import org.springframework.ldap.schema.BasicSchemaSpecification;

public class SimpleLdifParser2 {

   public void parse(File file) throws IOException {
      LdifParser parser = new LdifParser(file);
       parser.setAttributeValidationPolicy(new DefaultAttributeValidationPolicy());
      parser.setRecordSpecification(new BasicSchemaSpecification());
      parser.open();
          int count = 0;
      while(parser.hasMoreRecords()) {
         LdapAttributes attributes = parser.getRecord();
         count ++;
      }
      parser.close();
      System.out.println(count);
   }

   public static void main(String[] args) throws IOException {
      SimpleLdifParser2 parser = new SimpleLdifParser2();
      parser.parse(new ClassPathResource("patrons.ldif").getFile());
   }
}

运行上述方法后,您应该在控制台中看到计数 103。

LDAP 连接池

LDAP 连接池是一种技术,其中到 LDAP 目录的连接被重用,而不是在每次请求连接时都被创建。如果没有连接池,对 LDAP 目录的每个请求都会导致创建一个新连接,然后在不再需要该连接时释放该连接。创建新的连接是资源密集型的,这种开销会对性能产生负面影响。使用连接池,连接在创建后存储在池中,并为后续客户端请求回收。

池中的连接在任何时候都可以处于以下三种状态之一:

  • 使用中:连接已打开,当前正在使用中。
  • 空闲:连接打开,可以重用。
  • 关闭:连接不再可用。

图 10-1 说明了在任何给定的时间,连接上可能的动作。

9781430263975_Fig10-01.jpg

图 10-1 。连接池状态

内置连接池

JNDI 通过“com.sun.jndi.ldap.connect.pool”环境属性为连接池提供基本支持。创建目录上下文的应用可以将此属性设置为 true,并指示需要打开连接池。清单 10-12 显示了利用池支持的普通 JNDI 代码。

清单 10-12。

// Set up environment for creating initial context
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:11389");

// Enable connection pooling
env.put("com.sun.jndi.ldap.connect.pool", "true");

// Create one initial context
(Get connection from pool) DirContext ctx = new InitialDirContext(env);

// do something useful with ctx
// Close the context when we’re done
ctx.close(); // Return connection to pool

默认情况下,使用 Spring LDAP 创建的上下文将“com.sun.jndi.ldap.connect.pool”属性设置为 false。通过在配置文件中将 LdapContextSource 的 pooled 属性设置为 true,可以打开本机连接池。以下代码显示了配置更改:

<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
   <property name="url" value="ldap://localhost:11389" />
   <property name="base" value="dc=example,dc=com" />
   <property name="userDn" value="cn=Manager" />
   <property name="password" value="secret" />
   <property name="pooled" value="true"/>
</bean>

尽管本地 LDAP 连接池很简单,但它确实有一些缺点。连接池是根据 Java 运行时环境来维护的。不可能为每个 JVM 维护多个连接池。此外,也无法控制连接池的属性,例如任何时候要维护的连接数或空闲连接时间。也不可能提供任何自定义连接验证来确保池连接仍然有效。

Spring LDAP 连接池

为了解决本地 JNDI 池的缺点,Spring LDAP 为 LDAP 连接提供了一个定制的池库。Spring LDAP 连接池维护自己的一组特定于每个应用的 LDAP 连接。

image 注意 Spring LDAP 利用 Jakarta Commons 池库作为其底层池实现。

Spring LDAP 池的核心是 org . Spring framework . LDAP . pool . factory . pooling ContextSource,它是一个专门的 ContextSource 实现,负责池化 DirContext 实例。要利用连接池,首先要配置一个 Spring LDAP 上下文源,如下所示:

<bean id="contextSourceTarget" class="org.springframework.ldap.core.support.LdapContextSource">
   <property name="url" value="ldap://localhost:389" />
   <property name="base" value="dc=example,dc=com" />
   <property name="userDn" value="cn=Manager" />
   <property name="password" value="secret" />
   <property name="pooled" value="false"/>
</bean>

请注意,您将上下文源的 pooled 属性设置为 false。这将允许 LdapContextSource 在需要时创建全新的连接。此外,ContextSource 的 id 现在设置为 contextSourceTarget,而不是您通常使用的 contextSource。下一步是创建 PoolingContextSource,如下所示:

<bean id="contextSource" class="org.springframework.ldap.pool.factory.PoolingContextSource">
   <property name="contextSource" ref="contextSourceTarget" />
</bean>

PoolingContextSource 包装了您之前配置的 contextSourceTarget。这是必需的,因为 PoolingContextSource 将 DirContexts 的实际创建委托给 contextSourceTarget。另请注意,您已经为此 bean 实例使用了 id contextSource。这允许您在 LdapTemplate 中使用 PoolingContextSource 实例时,将配置更改保持在最低限度,如下所示:

<bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
   <constructor-arg ref="contextSource" />
</bean>

PoolingContextSource 提供了多种选项,可用于微调连接池。表 10-1 列出了一些重要的配置属性。

表 10-1 。PoolingContextSource 配置属性

财产 描述 默认
多国文字 当设置为 true 时,在从池中借用 DirContext 之前会对其进行验证。如果 DirContext 验证失败,它将从池中删除,并尝试借用另一个 DirContext。该测试可能会在处理借用请求时增加一点延迟。 错误的
连接被归还到连接池时 当设置为 true 时,此属性指示在返回池之前将验证 DirContext。 错误的
testWhileIdle 当设置为 true 时,此属性指示应以指定的频率验证池中的空闲 DirContext 实例。验证失败的对象将从池中删除。 错误的
定时炸弹 此属性指示运行空闲上下文测试之间的休眠时间(以毫秒为单位)。负数表示永远不会运行空闲测试。 -1
当用尽动作 指定当池耗尽时要采取的操作。可能的选项有 WHEN_EXHAUSTED_FAIL (0)、WHEN_EXHAUSTED_BLOCK (1)和 WHEN_EXHAUSTED_GROW (2)。 one
最大总数 该池可以包含的最大活动连接数。非正整数表示没有限制。 -1
maxIdle(最大空闲时间) 池中可以空闲的每种类型(读、读写)的最大空闲连接数。 eight
max wait-max 等待 在引发异常之前,池等待连接返回池的最大毫秒数。负数表示无限期等待。 -1

池验证

Spring LDAP 使得验证池连接变得很容易。该验证确保 DirContext 实例在从池中借用之前已正确配置并连接到 LDAP 服务器。在上下文返回到池中之前,或者在池中空闲的上下文上,进行相同的验证。

PoolingContextSource 将实际的验证委托给 org . spring framework . LDAP . pool . validation . dircontextvalidator 接口的具体实例。在清单 10-13 中,你可以看到 DirContextValidator 只有一个方法:validateDirContext。第一个参数 contextType 指示要验证的上下文是只读上下文还是读写上下文。第二个参数是需要验证的实际上下文。

清单 10-13。

package org.springframework.ldap.pool.validation;

import javax.naming.directory.DirContext;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.pool.DirContextType;

public interface DirContextValidator {
   boolean validateDirContext(DirContextType contextType, DirContext dirContext);
}

Spring LDAP 提供了一个名为 org . Spring framework . LDAP . pool . validation . defaultdircontextvalidator 的默认 DirContextValidator 实现,这个实现只是使用上下文执行搜索,并验证返回的 javax.naming.NamingEnumeration。需要更复杂验证的应用可以创建 DirContextValidator 接口的新实现。

配置池验证如清单 10-14 所示。首先创建一个 DefaultDirContextValidator 类型的 dirContextValidator bean。然后修改 contextSource bean 声明以包含 dirContextValidator bean。在清单 10-14 中,您还添加了 testOnBorrow 和 testWhileIdle 属性。

清单 10-14。

<bean id="dirContextValidator" class="org.springframework.ldap.pool.validation.DefaultDirContextValidator" />
<bean id="contextSource" class="org.springframework.ldap.pool.factory.PoolingContextSource">
   <property name="contextSource" ref="contextSourceTarget" />
   <property name="dirContextValidator" ref="dirContextValidator"/>
   <property name="testOnBorrow" value="true" />
   <property name="testWhileIdle" value="true" />
</bean>

摘要

这就把我们带到了旅程的终点。在整本书中,您已经学习了 Spring LDAP 的关键特性。有了这些知识,您应该可以开始开发基于 Spring LDAP 的应用了。

最后,写这本书并与你分享我的见解是一种绝对的快乐。祝你一切顺利。编码快乐!

posted @   绝不原创的飞龙  阅读(215)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
历史上的今天:
2020-10-02 《线性代数》(同济版)——教科书中的耻辱柱
点击右上角即可分享
微信分享提示