javaspringspring-bootspring-ldap

Spring Boot 2 - Wire Two LDAP Templates


I need to configure multiple LDAP data sources / LdapTemplates in my Spring Boot 2 application. The first LdapTemplate will be used for most of the work, while the second will be used for a once-in-a-while subset of data (housed elsewhere).

I have read these StackOverflow questions regarding doing that, but they seem to be for Spring Boot 1.
Can a spring ldap repository project access two different ldap directories?
Multiple LDAP repositories with Spring LDAP Repository

From what I can gather, much of that configuration/setup had to be done anyway, even for just one LDAP data source, back in Spring Boot 1. With Spring Boot 2, I just put the properties in my config file like so

ldap.url=ldap://server.domain.com:389
ldap.base:DC=domain,DC=com
ldap.username:domain\ldap.svc.acct
ldap.password:secret

and autowire the template in my repository like so

@Autowired
private final LdapTemplate ldapTemplate;

and I'm good to go. (See: https://stackoverflow.com/a/53474188/3669288)

For a second LDAP data source, can I just add the properties and configuration elements for "ldap2" and be done (see linked questions)? Or does adding this configuration cause Spring Boot 2's auto configuration to think I'm overriding it and so now I lose my first LdapTemplate, meaning I now need to go explicitly configure that as well?
If so, do I need to configure everything, or will only a partial configuration work? For example, if I add the context source configuration and mark it as @Primary (does that work for LDAP data sources?), can I skip explicitly assigning it to the first LdapTemplate? On a related note, do I still need to add the @EnableLdapRepositories annotation, which is otherwise autoconfigured by Spring Boot 2?

TLDR: What's the minimum configuration I need to add in Spring Boot 2 to wire in a second LdapTemplate?


Solution

  • This takes what I've learned over the weekend and applies it as an answer to my own question. I'm still not an expert in this so I welcome more experienced answers or comments.

    The Explanation

    First, I still don't know for certain if I need the @EnableLdapRepositories annotation. I don't yet make use of those features, so I can't say if not having it matters, or if Spring Boot 2 is still taking care of that automatically. I suspect Spring Boot 2 is, but I'm not certain.

    Second, Spring Boot's autoconfigurations all happen after any user configurations, such as my code configuring a second LDAP data source. The autoconfiguration is using a couple of conditional annotations for whether or not it runs, based on the existence of a context source or an LdapTemplate.
    This means that it sees my "second" LDAP context source (the condition is just that a context source bean exists, regardless of what its name is or what properties it is using) and skips creating one itself, meaning that I no longer have that piece of my primary data source configured.
    It will also see my "second" LdapTemplate (again, the condition is just that an LdapTemplate bean exists, regardless of what its name is or what context source or properties it is using) and skip creating one itself, so I again no longer have that piece of my primary data source configured.
    Unfortunately, those conditions mean that in this case there is no in-between either (where I can manually configure the context source, for example, and then allow the autoconfiguration of the LdapTemplate to still happen). So the solution is to either make my configuration run after the autoconfiguration, or to not leverage the autoconfiguration at all and set them both up myself.

    As for making my configuration run after the autoconfiguration: the only way to do that is to make my configuration an autoconfiguration itself and specify its order to be after Spring's built-in autoconfiguration (see: https://stackoverflow.com/a/53474188/3669288). That's not appropriate for my use case, so for my situation (because Spring Boot's setup does make sense for a standard single-source situation) I'm stuck forgoing the autoconfiguration and setting them both up myself.

    The Code

    Setting up two data sources is pretty well covered in the following two answers (though partly for other reasons), as linked in my question, but I'll also detail my setup here.
    Can a spring ldap repository project access two different ldap directories?
    Multiple LDAP repositories with Spring LDAP Repository

    First up, the configuration class needs to be created, as one was not previously needed at all with Spring Boot 2. Again, I left out the @EnableLdapRepositories annotation partly because I don't use it yet, and partly because I think Spring Boot 2 will still cover that for me. (Note: All of this code was typed up in the Stack Overflow answer box as I don't have a development environment where I'm writing this, so imports are skipped and the code may not be perfectly compilable and function correctly, though I hope it's good.)

    @Configuration
    public class LdapConfiguration {
    }
    

    Second is manually configuring the primary data source; the one that used to be autoconfigured but no longer will be. There is one piece of Spring Boot's autoconfiguration that can be leveraged here, and that is its reading in of the standard spring.ldap.* properties (into a properties object), but since it wasn't given a name, you have to reference it by its fully qualified class name. This means you can skip straight to setting up the context source for the primary data source. This code is not quite as full featured as the actual autoconfiguration code (See: Spring Code) I marked this LdapTemplate as @Primary because for my use, this is the primary data source and so it's what all other autowired calls should default to. This also means you don't need a @Qualifier where you autowire this source up (as seen later).

    @Configuration
    public class LdapConfiguration {
        @Bean(name="contextSource")
        public LdapContextSource ldapContextSource(@Qualifier("spring.ldap-org.springframework.boot.autoconfigure.ldap.LdapProperties") LdapProperties properties) {
            LdapContextSource source = new LdapContextSource();
            source.setUrls(properties.getUrls());
            source.setUserDn(properties.getUsername());
            source.setPassword(properties.getPassword());
            source.setBaseEnvironmentProperties(Collections.unmodifiableMap(properties.getBaseEnvironment()));
            
            return source;
        }
        
        @Bean(name="ldapTemplate")
        @Primary
        public LdapTemplate ldapTemplate(@Qualifier("contextSource") LdapContextSource source) {
            return new LdapTemplate(source);
        }
    }
    

    Third is to manually configure the secondary data source, the one that caused all of this to begin with. For this one, you do need to configure the reading of your properties into an LdapProperties object. This code builds on the previous code, so you can see the complete class for context.

    @Configuration
    public class LdapConfiguration {
        @Bean(name="contextSource")
        public LdapContextSource ldapContextSource(@Qualifier("spring.ldap-org.springframework.boot.autoconfigure.ldap.LdapProperties") LdapProperties properties) {
            LdapContextSource source = new LdapContextSource();
            source.setUrls(properties.getUrls());
            source.setUserDn(properties.getUsername());
            source.setPassword(properties.getPassword());
            source.setBaseEnvironmentProperties(Collections.unmodifiableMap(properties.getBaseEnvironment()));
            
            return source;
        }
        
        @Bean(name="ldapTemplate")
        @Primary
        public LdapTemplate ldapTemplate(@Qualifier("contextSource") LdapContextSource source) {
            return new LdapTemplate(source);
        }
        
        
        
        @Bean(name="ldapProperties2")
        @ConfigurationProperties("app.ldap2")
        public LdapProperties ldapProperties2() {
            return new LdapProperties();
        }
        
        @Bean(name="contextSource2")
        public LdapContextSource ldapContextSource2(@Qualifier("ldapProperties2") LdapProperties properties) {
            LdapContextSource source = new LdapContextSource();
            source.setUrls(properties.getUrls());
            source.setUserDn(properties.getUsername());
            source.setPassword(properties.getPassword());
            source.setBaseEnvironmentProperties(Collections.unmodifiableMap(properties.getBaseEnvironment()));
            
            return source;
        }
        
        @Bean(name="ldapTemplate2")
        public LdapTemplate ldapTemplate2(@Qualifier("contextSource2") LdapContextSource source) {
            return new LdapTemplate(source);
        }
    }
    

    Finally, in your class that uses these LdapTemplates, you can autowire them as normal. This uses constructor autowiring instead of the field autowiring the other two answers used. Either is technically valid though constructor autowiring is recommended.

    @Component
    public class LdapProcessing {
        protected LdapTemplate ldapTemplate;
        protected LdapTemplate ldapTemplate2;
        
        @Autowired
        public LdapProcessing(LdapTemplate ldapTemplate, @Qualifier("ldapTemplate2") LdapTemplate ldapTemplate2) {
            this.ldapTemplate = ldapTemplate;
            this.ldapTemplate2 = ldapTemplate2;
        }
    }
    

    TLDR: Defining a "second" LDAP data source stops the autoconfiguration of the first LDAP data source, so both must be (nearly fully) manually configured if using more than one; Spring's autoconfiguration can not be leveraged even for the first LDAP data source.