As the Spring team points out, the standard JNDI-LDAP programming model is cumbersome, at best - and there's no question that the Spring-LDAP module simplifies that problem considerably, though not as completely as some would prefer. In a 2008 article, Colin Lu describes a homegrown facade using an iBatis-style framework to address his concerns with Spring-LDAP. Our own experience with Spring-LDAP has been a mixed bag, and in this 2-part series, I'll discuss another approach that leverages iBatis-over-LDAP directly. This first article starts with the JNDI-LDAP approach, looks at the improvements available with Spring-LDAP, and describes our attempt to leverage Spring-LDAP in a Spring 3.0 environment. Since that attempt was not successful, my followup article will present an alternate solution that layers Spring-iBatis over LDAP.
JNDI-LDAP Example
Ill start with the plain-vanilla JNDI-LDAP model, and evolve it in two steps - first to Spring-LDAP, and secondly using Spring-iBatis. As presented by Sunil D. Patil in his 2007 article, an example class might look something like this (key pieces are bold-faced so you can keep track of them as we evolve the program):
public class PlainLDAPDemo { private static final String LDAP_BASE = "dc=mybiz,dc=com"; private static final String LDAP_USERNAME = "uid"; private static final String LDAP_PROVIDER_URL = "ldap://127.0.0.1:389"; private static final String LDAP_SECURITY_AUTHENTICATION = "simple"; private static final String LDAP_SECURITY_PRINCIPAL = "cn=Manager,dc=mybiz,dc=com"; private static final String LDAP_SECURITY_CREDENTIALS = "verycleverpassword"; public static void main(String[] args) { Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, LDAP_PROVIDER_URL); env.put(Context.SECURITY_AUTHENTICATION, LDAP_SECURITY_AUTHENTICATION); env.put(Context.SECURITY_PRINCIPAL, LDAP_SECURITY_PRINCIPAL); env.put(Context.SECURITY_CREDENTIALS, LDAP_SECURITY_CREDENTIALS); DirContext ctx = null; NamingEnumeration results = null; try { ctx = new InitialDirContext(env); SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); results = ctx.search(LDAP_BASE, "(objectclass=person)", new String[]{LDAP_USERNAME}, searchControls); List<string> users = new ArrayList<string>(); while (results.hasMore()) { SearchResult searchResult = (SearchResult) results.next(); Attributes attributes = searchResult.getAttributes(); Attribute attr = attributes.get(LDAP_USERNAME); String cn = (String) attr.get(); users.add(cn); } Collections.sort(users); for (String user : users) { System.out.println(" UID = " + user); } } catch (NamingException e) { throw new RuntimeException(e); } finally { if (results != null) { try { results.close(); } catch (Exception ignored) {} } if (ctx != null) { try { ctx.close(); } catch (Exception ignored) {} } } } }
As you can see, the bold-faced portions comprise the application-specific configuration, and are a small part of the overall program - most of the rest is boilerplate that can and should be factored out. That's what the Spring team has done for us.
Spring-LDAP Example
Mr. Patil does an excellent job of moving you from this verbose beginning to a lean Spring-LDAP treatment of the same program, so I won't repeat those details here. The transformation I came up with includes a test driver program, a DAO and a Spring configuration file. Here is my test driver:
public class SpringLDAPDemo { public static void main(String[] args) { Resource resource = new ClassPathResource("beans.xml"); BeanFactory factory = new XmlBeanFactory(resource); UserDao ldapContact = (UserDao) factory.getBean("ldapContact"); List<String> users = ldapContact.getAllUsers(); Collections.sort(users); for (String user : users) { System.out.println(" UID = " + user); } } }
This is noticeably simpler client access. Here is the DAO:
public class UserDao { private LdapTemplate ldapTemplate; private String ldapBase; private String ldapUsername; private String searchQualifier; public void setLdapTemplate(LdapTemplate ldapTemplate) { this.ldapTemplate = ldapTemplate; } public String getLdapBase() { return ldapBase; } public void setLdapBase(String ldapBase) { this.ldapBase = ldapBase; } public String getLdapUsername() { return ldapUsername; } public void setLdapUsername(String ldapUsername) { this.ldapUsername = ldapUsername; } public String getSearchQualifier() { return searchQualifier; } public void setSearchQualifier(String searchQualifier) { this.searchQualifier = searchQualifier; } public List getAllUsers() { return this.ldapTemplate.search(getLdapBase(), getSearchQualifier(), new AttributesMapper() { public Object mapFromAttributes(Attributes attrs) throws NamingException { return attrs.get(getLdapUsername()).get(); } }); } }
Again, much cleaner than the original - perhaps even reusable. All of the application-specific configuration and most of the cruft has been factored out, thanks to Spring. Here's the Spring configuration that encapsulates application-specific details:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource"> <property name="url" value="ldap://127.0.0.1:389"/> <property name="userDn" value="cn=Manager,dc=mybiz,dc=com"/> <property name="password" value="verycleverpassword"/> <property name="pooled" value="true"/> </bean> <bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate"> <constructor-arg ref="contextSource"/> </bean> <bean id="ldapContact" class="UserDao"> <property name="ldapTemplate" ref="ldapTemplate"/> <property name="ldapBase" value="dc=mybiz,dc=com"/> <property name="ldapUsername" value="uid"/> <property name="searchQualifier" value="(objectclass=person)"/> </bean> </beans>
Note that I'm working with the latest Spring-LDAP release, 1.3.0, and as such my packages and property names are slightly different from the 2.0.1-based example in Mr. Patil's article - in particular, please note the bold-italic sections above.
The Problem
Now, I'm also working with Spring 3.0 to gain the latest-and-greatest dependency injection, AOP and iBatis integration. Since the Spring LDAP Reference document claims that "...Spring LDAP 1.3 is supported on Spring 2.0 and later", I initially assumed I'd be OK. However, when my program is deployed as a standalone, it actually fails with this:
java.lang.ClassNotFoundException: org.springframework.dao.EmptyResultDataAccessException
I used an Ivy configuration to fetch dependencies for org.springframework.ldap, and in checking back with the artifacts from that retrieval, I found the missing class is available only in the 2.5.6 distribution (in particular, I needed org.springframework.transaction-2.5.6-SEC01.jar), so I would need to do some mixing-and-matching of 3.0 and 2.5.6 jars in my deployment to get this to work.
Yes, I had misgivings about this; the last thing I want is to experiment with combinations of libraries that might cause classloading or other runtime mismatches later on - a costly misadventure in a production environment. So my next step was to test drive the Spring-LDAP standalone with the 3.0 core and the 2.5.6 transaction libraries; this worked out just fine - the missing class is of course available and my simple test program succeeded without a problem. Next, I combined this program with another test driver that applies Spring AOP - but now this message appears:
Cannot convert value of type [$Proxy15 implementing org.springframework.ldap.core.LdapOperations,org.springframework.beans.factory.InitializingBean,org.springframework.aop.SpringProxy,org.springframework.aop.framework.Advised] to required type [org.springframework.ldap.core.LdapTemplate] for property 'ldapTemplate': no matching editors or conversion strategy
Removing Spring-LDAP from the mix of course remedies my AOP program - this is no surprise since the LdapTemplate is implicated in the error message above. Checking a little deeper, I find that the 2.5.6 transaction jar has many (if not all) of the same classes as the 3.0 transaction jar that I need to support my AOP configurations. So there's a growing doubt that I will succeed in a full-blown deployment, given the obvious concern around classloading problems, let alone how quickly I encountered the error as seen above.
Analysis of the Problem
Now, I could certainly take steps at this point to Google the error message (which, in fairness, I did - but I found no solution), experiment further, query the forums, and etcetera - but the cost/benefit of further effort here is a question mark. My mission is not to leverage Spring-LDAP; my mission is to establish a clean alternative to some existing JNDI-LDAP code. If Spring-LDAP worked without a hitch in my 3.0 deployment, I would choose that direction and move on. In fairness, and impressively enough, "it just works" has been the case for everything else I've tried so far with Spring 3.0 - injection, iBatis integration, AOP and in particular AOP-based transaction management. But here are some points about our application requirements, rationalizing why I'm reluctant to put much more effort into Spring-LDAP:
- Our LDAP-based use cases are dirt simple; this part of the application is not enterprise-scale. We need simple CRUD operations, nothing more. The path of least resistance here is to just wrap that CRUD in our own DAO facade and be done with it.
- The application itself, however, is enterprise-scale, and we're prototyping modern technologies like Spring to facilitate a rewrite of the web tier. The legacy web tier is a mix-and-match of more than a few technologies - Struts 1.x, Dojo, (massive amounts of) JavaScript, iFrames, JSTL, JSP, etc. - and is a poster child for a "how many different ways can we do the same thing?" approach. We are motivated this time to keep the technology selection down to a bare minimum, establish a minimal set of reusable patterns, choose frameworks that "just work" and think twice before going with things that have problems out of the gate. The resistance we're meeting here with Spring-LDAP in a 3.0-AOP context gives us cause for pause.
- While we could get around the deployment problems with Spring 3.0 AOP and 2.5.6 LDAP modules by isolating the LDAP component as a web service (e.g.), this begins to feel like overkill. This would add an extra network hop to retrieve information, and gives us one more point of failure (the new service) - this is probably not worth it, given #1 above, and especially since we'd be doing this only to enable use of Spring-LDAP. That's the tail wagging the dog.
As such, I am now motivated to explore a second alternative to JNDI-LDAP, that of wrapping Spring-iBatis around a JDBC-LDAP bridge driver. But first, here are some points further validating my conclusion on #3 above, i.e. reluctance to use Spring LDAP in a separate service for our application:
- Clearly Spring-LDAP is a vast improvement over the JNDI-LDAP programming model. But, while the code is cleaner and simpler, it'sjust not that much code to worry about for our application (see #1 above).
- Spring-LDAP provides the ability to execute some business logic either before or after a search (but, we can also do this using regular Spring AOP, if I understand this feature correctly).
- We can optionally implement a method in the Spring-LDAP inner class that converts LDAP object into custom Java objects (but, we can also do this using an iBatis approach).
- Spring-LDAP provides the ability to create dynamic filters (but, this obscures the query being constructed since it's done in Java code. An iBatis approach simply externalizes the SQL in one place, supporting dynamic queries using parameterized templating).
- Likewise, basic CRUD idioms in Spring-LDAP are available; but these are also constructed procedurally in java code. Again, an iBatis approach is our preference.
- Spring-LDAP provides simplified "dynamic authentication" (i.e. an arbitrary user logging in to the LDAP system, as opposed to an implicitly declared principal as done in the example). However, this is not one of our requirements - the use of a single declaratively specified principal (via Spring configuration) is all we need.
- Spring-LDAP provides "compensating transaction support" - which is a solid improvement over the transaction-impaired LDAP environment - but we do not need this. From the Spring-LDAP docs: "The client side transaction support will add some overhead in addition to the work required by the original operations. While this overhead should not be something to worry about in most cases, if your application will not perform several LDAP operations within the same transaction (e.g. a
modifyAttributes
followed by arebind
), or if transaction synchronization with a JDBC data source is not required (see below) there will be nothing to gain by using the LDAP transaction support."
Resources
Spring Framework 1.3.x Reference Documentation, by , , 2009 Spring Framework
Simplify directory access with Spring LDAP, by Sunil D. Patil, 2007 JavaWorld
Extending Spring LDAP with an iBATIS-style XML Data Mapper, by Colin (Chun) Lu, 2008 JavaWorld
SpringSource Enterprise Bundle Repository
MyVD Virtual Directory: JDBC-LDAP Bridge
Simple Authentication Using Spring LDAP, by , 2009 Spring Framework
No comments:
Post a Comment