In the previous part I explained one of the messages flows in detail from the point of view of implementing in- and outbound messaging with Spring Integration’s AMQP support. I briefly mentioned that data handler adapters are loaded dynamically and they’re plugged into the message flow. In this third part, we’ll explore one of those technical challenges in detail that the application’s modular design raise and how it can be tackled by using Spring Boot 2’s new property Binder API.
For most of the use cases having a predefined, static message flow is sufficient. However, that’s not the case for the forget-me app, as multiple data handlers can be configured which will carry data erasure out. One major challenge to address is to decide whether or not a particular data handler needs to be initialized and plugged into the main message flow. I can tell you beforehand that Spring’s conditional configuration will be used to do that.
You can find the entire source code of the app on GitHub. Be aware however, that the app hasn’t been released yet and I do code reorganizations from time to time.
I had been searching for a proper solution with regards to configuring and initializing the child application contexts of
data handler dynamically. Eventually, I stumbled upon
OAuth2ClientRegistrationRepositoryConfiguration
and it gave me ideas.
The app is going to come with a fixed number of built-in modules (data handlers or adapters as I sometimes refer to them) and they’re are pre-configured with both meta-data and runtime configuration data. Such a configuration looks like this.
forgetme:
data-handler:
registration:
mailerlite:
name: mailerlite
display-name: MailerLite
description: Email Marketing
url: https://www.mailerlite.com/
data-scopes:
- notification
- profile
provider:
mailerlite:
api-key: ${MAILERLITE_API_KEY:#{null}}
If you used the new OAuth2 support in Spring Security, probably you noticed that this piece of configuration looks very much alike.
The first part (registration) of this configuration just holds metadata about the data handler, but the second part (provider) may contain arbitrary key value pairs for configuring it. In this case an API key, MailerLite needs only that.
How this is going to work is the following. When MAILERLITE_API_KEY
is defined, the corresponding child application context
gets loaded, otherwise it remains inactive. As the configuration key/value pairs for individual data handlers cannot be
known in advance,
Spring Boot 2’s property Binder API is a
good fit for loading them.
@Getter
@Setter
public class DataHandlerRegistration {
static final Bindable<Map<String, String>> DATA_HANDLER_PROVIDER_BINDABLE =
Bindable.mapOf(String.class, String.class);
static final String DATA_HANDLER_PROVIDER_PREFIX = "forgetme.data-handler.provider";
static final Bindable<Map<String, DataHandlerRegistration>> DATA_HANDLER_REGISTRATION_BINDABLE =
Bindable.mapOf(String.class, DataHandlerRegistration.class);
static final String DATA_HANDLER_REGISTRATION_PREFIX = "forgetme.data-handler.registration";
private String name;
private String displayName;
private String description;
private URI url;
private Set<DataScope> dataScopes;
public Optional<URI> getUrl() {
return Optional.ofNullable(url);
}
public void validate() {
Assert.hasText(getName(), "Data handler name must not be empty.");
Assert.hasText(getDisplayName(), "Data handler display-name must not be empty.");
Assert.hasText(getDescription(), "Data handler description must not be empty.");
Assert.notEmpty(getDataScopes(), "Data handler data-scopes must not be empty.");
}
public enum DataScope {
ACCOUNT,
CORRESPONDENCE,
ENQUIRY,
NOTIFICATION,
PROFILE,
PUBLICATION,
USAGE;
}
}
What’s relevant here is DATA_HANDLER_PROVIDER_BINDABLE
, which is basically a mapping between a set of properties and a
binding definition. The framework returns a
BindResult
object. Although it resembles to its well-known counterpart
BindingResult
from Spring MVC, it also embraces lambdas and you can use it in a similar way you would do with
java.util.Optional
.
public abstract class AbstractDataHandlerConfiguredCondition extends SpringBootCondition {
private final String dataHandlerName;
public AbstractDataHandlerConfiguredCondition(String dataHandlerName) {
this.dataHandlerName = dataHandlerName;
}
@Override
public ConditionOutcome getMatchOutcome(
ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition("Data handler configured:", dataHandlerName);
Map<String, String> dataHandlerProperties = getDataHandlerProperties(context.getEnvironment());
if (isDataHandlerConfigured(dataHandlerProperties)) {
return ConditionOutcome.match(message.available(dataHandlerName));
}
return ConditionOutcome.noMatch(message.notAvailable(dataHandlerName));
}
protected abstract boolean isDataHandlerConfigured(Map<String, String> dataHandlerProperties);
private Map<String, String> getDataHandlerProperties(Environment environment) {
String propertyName = DATA_HANDLER_PROVIDER_PREFIX + "." + dataHandlerName;
return Binder.get(environment)
.bind(propertyName, DATA_HANDLER_PROVIDER_BINDABLE)
.orElse(Collections.emptyMap());
}
}
Actual data handler adapters implement AbstractDataHandlerConfiguredCondition
where they can define upon which
constellation of provider properties should that particular data handler be enabled. In this case, as
MailerLite has got only a single property (API key), it’s suffice
to determine, if that one contains a non-empty piece of text.
@Configuration
@Conditional(MailerLiteConfiguredCondition.class)
public class MailerLiteFlowConfig extends AbstractDataHandlerFlowConfig {
static final String DATA_HANDLER_NAME = "mailerlite";
@Override
protected String getDataHandlerName() {
return DATA_HANDLER_NAME;
}
static class MailerLiteConfiguredCondition extends AbstractDataHandlerConfiguredCondition {
public MailerLiteConfiguredCondition() {
super(DATA_HANDLER_NAME);
}
@Override
protected boolean isDataHandlerConfigured(Map<String, String> dataHandlerProperties) {
return Optional.ofNullable(dataHandlerProperties.get("api-key"))
.filter(StringUtils::hasText)
.isPresent();
}
}
Here you can see MailerLite’s own configuration which is enabled only when there’s an API key set. Most of the heavy
lifting is done by AbstractDataHandlerFlowConfig
in terms of creating an configuring the child application context.
I’ll get to that in the next part.
Using conditional configuration with Spring Boot 2’s new property Binder API is a powerful combination.
Environment
,
it’s much more convenient to use and it also provides an API similar to
java.util.Optional
.@ConfigurationProperties
annotated configuration, because configuration data through the Binder API is available even before that happens.In the next part we’ll look into how message sub-flows are created within their own child application context and how these child contexts interact with the main message flow.
If you like Java and Spring as much as I do, sign up for my newsletter.