我累了,感情累了,中午不睡覺,也很累。 -----------Sayi
spring-ldap-core(the Spring LDAP library)java
JNDI是用來作LDAP的編程,正如JDBC是用來SQL編程同樣。儘管他們有着徹底不一樣各有優缺點的API,可是它們仍是有一些共性:spring
They require extensive plumbing code, even to perform the simplest of tasks.數據庫
All resources need to be correctly closed, no matter what happens.編程
Exception handling is difficultapi
Spring JDBC提供了jdbcTemplate等簡便的方式來操做數據庫,Spring LDAP也提供了相似的方式操做LDAP----ldapTemplate。數組
<bean id="contextSourceTarget" class="org.springframework.ldap.core.support.LdapContextSource"> <property name="url" value="ldap://localhost:10389" /> <property name="base" value="dc=example,dc=com" /> <property name="userDn" value="uid=admin,ou=system" /> <property name="password" value="secret" /> </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>
如上配置後,就能夠在程序中注入ldapTemplate進行操做了。服務器
DistinguishedName類實現了Name接口,提供了更優秀的方法來操做目錄,這個類已通過時了,推薦使用javax.naming.ldap.LdapName這個類。在咱們構建任何操做的時候,均可以使用此類構造基目錄。cookie
private DistinguishedName buildDn() { DistinguishedName dn = new DistinguishedName(); dn.append("ou", "People"); return dn; }
一個更復雜的構建以下,查找某個惟一userid的dn,其中使用了過濾器AndFilter:app
protected Name getPeopleDn(String userId) { AndFilter andFilter = new AndFilter(); andFilter.and(new EqualsFilter("objectclass", "person")); andFilter.and(new EqualsFilter("objectclass", "xUserObjectClass")); andFilter.and(new EqualsFilter("uid", userId)); List<Name> result = ldapTemplate.search(buildDn(), andFilter.encode(), SearchControls.SUBTREE_SCOPE, new AbstractContextMapper() { @Override protected Name doMapFromContext(DirContextOperations adapter) { return adapter.getDn(); } }); if (null == result || result.size() != 1) { throw new UserNotFoundException(); } else { return result.get(0); } }
除了dn能限制目錄條件外,過濾器提供了關於屬性的查詢限制條件,AndFilter是與的過濾器,EqualsFilter則是相等過濾器,還有不少內置過濾器,如WhitespaceWildcardsFilter、LikeFilter、GreaterThanOrEqualsFilter、LessThanOrEqualsFilter等。ide
查詢操做有兩個方法,分別是search和lookup,前者在不知道dn的狀況下進行搜索,然後者更像是直接取出對應的Entry。如上的search代碼就是在某個dn的全部子樹(SearchControls.SUBTREE_SCOPE)下搜索符合過濾器條件的DN列表:
List<DistinguishedName> result = ldapTemplate.search(buildDn(), filter.encode(), SearchControls.SUBTREE_SCOPE, new AbstractContextMapper() { @Override protected DistinguishedName doMapFromContext( DirContextOperations adapter) { return (DistinguishedName) adapter.getDn(); } });
下面的代碼將是直接查出所須要的Entry,其中第二個參數表示要取出的屬性,可選:
public Account queryUser(String userId) { return (Account) ldapTemplate.lookup(getPeopleDn(userId), new String[] { "uid", "cn", "objectClass" }, new AccountAttributesMapper()); } private class AccountAttributesMapper implements AttributesMapper { public Object mapFromAttributes(Attributes attrs) throws NamingException { Account person = new Account(); person.setUserID((String)attrs.get("uid").get()); person.setUserName((String)attrs.get("cn").get()); person.setDescription((String)attrs.get("description").get()); return person; } }
AttributesMapper相似與JDBC中的RowMapper,實現這個接口能夠實現ldap屬性到對象的映射,spring也提供了更爲簡單的上下文映射AbstractContextMapper來實現映射,這個類在取ldap屬性的時候代碼更爲簡單和優雅。
如上節所示,咱們已經知道ldap屬性到對象的映射,在咱們查找對象時,咱們可使映射更爲簡單,以下:
public Account queryUser(String userId) { return (Account) ldapTemplate.lookup(getPeopleDn(userId), new String[] { "uid", "cn", "objectClass" }, new AccountContextMapper()); } private class AccountContextMapper extends AbstractContextMapper { private String[] ldapAttrId; @SuppressWarnings("unchecked") public AccountContextMapper() { ldapAttrId = buildAttr(userAttrService.getLdapAttrIds(), new String[] { "uid", "cn", "objectClass" }); } @Override public Object doMapFromContext(DirContextOperations context) { Account account = new Account(); account.setUserId(context.getStringAttribute("uid")); account.setUserName(context.getStringAttribute("cn")); Map<String, Object> userAttributes = new HashMap<String, Object>(); // 取賬號元數據的屬性值 for (String ldapAttr : ldapAttrId) { Object value; Object[] values = context.getObjectAttributes(ldapAttr); if (values == null || values.length == 0) continue; value = (values.length == 1) ? values[0] : values; if (value instanceof String && StringUtils.isEmpty(value.toString())) continue; userAttributes.put(ldapAttr, value); } account.setUserAttributes(userAttributes); return account; } }
在上面的代碼中,咱們徹底能夠只關注doMapFromContext這個方法,經過參數context讓獲取屬性更爲方便,其中的變量ldapAttrId只是一些額外的用途,標識取哪些屬性映射到對象中,徹底能夠忽略這段代碼。
插入數據無非咱們要構造這個數據的存儲目錄,和數據屬性,經過上面的知識咱們能夠很輕鬆的構造DN,構造數據咱們採用DirContextAdapter這個類,代碼以下:
DirContextAdapter context = new DirContextAdapter(dn); context.setAttributeValues("objectclass", userLdapObjectClasses.split(",")); context.setAttributeValue("uid", account.getUserId()); mapToContext(account, context); ldapTemplate.bind(context);
ldapTemplate.bind(context)是綁定的核心api。
ldap的屬性值也是有類型的,好比能夠是字符串,則經過setAttributeValue來設置屬性值,能夠是數組,則經過setAttributeValues來設置屬性值。其中mapToContext屬於自定義的方法,用來映射更多的對象屬性到LDAP屬性,以下自定義的代碼:
protected void mapToContext(Account account, DirContextOperations ctx) { ctx.setAttributeValue("cn", account.getUserName()); ctx.setAttributeValue("sn", account.getUserName()); ctx.setAttributeValue("user-account-time", getDateStr(account.getLifeTime(), "yyyy/MM/dd")); if (StringUtils.isNotEmpty(account.getPassword())) { ctx.setAttributeValue("userPassword", account.getPassword()); } Map<String, Object> userAttributes = account.getUserAttributes(); for (Map.Entry<String, Object> o : userAttributes.entrySet()) { String ldapAtt = userAttrService.getLdapAttrId(o.getKey()); if (ldapAtt == null) throw new RuntimeException("Invalid attribute " + o.getKey()); if (o.getValue() == null) continue; if (o.getValue() instanceof String && StringUtils.isWhitespace(o.getValue().toString())) { continue; } if (ObjectUtils.isArray(o.getValue()) && !(o.getValue() instanceof byte[])) { Object[] array = ObjectUtils.toObjectArray(o.getValue()); if (array != null && array.length != 0) ctx.setAttributeValues(ldapAtt, array); } else if (o.getValue() instanceof List) { Object[] array = ((List) o.getValue()).toArray(); if (array != null && array.length != 0) ctx.setAttributeValues(ldapAtt, array); } else ctx.setAttributeValue(ldapAtt, o.getValue()); } }
解綁很是的簡單,直接解綁目錄便可,以下:
ldapTemplate.unbind(dn);
修改便是取得對應Entry,而後修改屬性,經過modifyAttributes方法來修改
DirContextOperations context = ldapTemplate .lookupContext(dn); context.setAttributeValue("user-status", status); ldapTemplate.modifyAttributes(context);
Spring 支持移動Entry,經過rename方法,與其同時帶來了一個概念:移動策略或者重命名策略。Spring內置了兩個策略,分別是DefaultTempEntryRenamingStrategy和DifferentSubtreeTempEntryRenamingStrategy,前者策略是在條目的最後增長後綴用來生成副本,對於DNcn=john doe, ou=users, 這個策略將生成臨時DN cn=john doe_temp, ou=users.後綴能夠經過屬性tempSuffix來配置,默認是"_temp",
後者策略則是在一個單獨的dn下存放臨時dn,單獨的dn能夠經過屬性subtreeNode來配置。 策略配置在ContextSourceTransactionManager的事務管理Bean中,屬性名爲renamingStrategy。 如:
<bean id="ldapRenameStrategy" class="org.springframework.ldap.transaction.compensating.support.DifferentSubtreeTempEntryRenamingStrategy" > <constructor-arg name="subtreeNode" value="ou=People,dc=temp,dc=com,dc=cn"></constructor-arg> </bean>
上面這麼多,須要在ds服務器支持移動的條件下,不然只能經過刪除--插入來代替移動,以下是個移動的示例:
DirContextAdapter newContext = new DirContextAdapter(newDn); DirContextOperations oldContext = ldapTemplate.lookupContext(oldDn); NamingEnumeration<? extends Attribute> attrs = oldContext .getAttributes().getAll(); try { while (attrs.hasMore()) { Attribute attr = attrs.next(); newContext.getAttributes().put(attr); } } catch (NamingException e) { throw new RuntimeException("remove entry error:" + e.getMessage()); } ldapTemplate.unbind(oldDn); ldapTemplate.bind(newContext);
很惋惜,有些ldap服務器不支持分頁,而有些已經支持PagedResultsControl,能夠經過cookie來實現與ldap的分頁交互。官方文檔的示例以下:
public PagedResult getAllPersons(PagedResultsCookie cookie) { PagedResultsRequestControl control = new PagedResultsRequestControl(PAGE_SIZE, cookie); SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); List persons = ldapTemplate.search("", "objectclass=person", searchControls, control); return new PagedResult(persons, control.getCookie()); }
事務在項目中是必須考慮的一部分,這節討論兩種事務的分別處理和結合,經過註解來完成。 典型的JDBC事務配置以下:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <tx:annotation-driven transaction-manager="txManager" />
咱們配置了jdbc的默認事務管理爲txManager,在服務層咱們可使用註解@Transcational來標註事務。
在單獨須要ldap事務管理時,咱們能夠配置ldap的事務,起了個別名ldapTx:
<bean id="ldapTxManager" class="org.springframework.ldap.transaction.compensating.manager.ContextSourceTransactionManager"> <property name="contextSource" ref="contextSource" /> <qualifier value="ldapTx"/> </bean>
咱們可使用註解@Transactional("ldapTx")來標註ldap的事務,若是不想每次引用別名,使用@LdapTransactional,則能夠建立註解:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.transaction.annotation.Transactional; @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Transactional("ldapTx") public @interface LdapTransactional { }
在ldap和jdbc同時都有操做的服務中,咱們能夠配置ContextSourceAndDataSourceTransactionManager來實現事務管理:
<bean id="ldapJdbcTxManager" class="org.springframework.ldap.transaction.compensating.manager.ContextSourceAndDataSourceTransactionManager"> <property name="contextSource" ref="contextSource"/> <property name="dataSource" ref="dataSource" /> <qualifier value="ldapJdbcTx"/> </bean>
正如數據庫操做中的ORM對象關係映射(表到java對象)同樣,ldap操做也有ODM對象目錄映射。