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 所示。
图 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 所示。
图 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 事务支持的组件。
图 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 说明了在任何给定的时间,连接上可能的动作。
图 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 连接。
注意 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 的应用了。
最后,写这本书并与你分享我的见解是一种绝对的快乐。祝你一切顺利。编码快乐!