Multi-tenant Configurations
In a multi-tenant environment a single instance of the application
will run on behalf of many clients. Typically, this will require
that each client has its own unique configuration. The easiest
approach to enable an application to be multi-tenant is for it
to not really be aware of it at all. This requires that the
configuration framework takes on some of the responsibility for
making the application work correctly.
One approach to enable this support in a web application might be
to use a Servlet Filter and then use the Log4j or SLF4J MDC to
save the attributes needed to identify the client. These attributes
can then be used to identify the specific client configuration to
be used. The classes described below use this technique to allow
configurations to transparently provide the configuration appropriate
for the clients.
MultiFileConfigurationBuilder
MultiFileConfigurationBuilder
is a specialized configuration
builder implementation which internally manages a set of builders for
file-based configurations. In the
initialization parameters of this builder a file name pattern has to
be passed. The pattern can contain keys that will be resolved using the
ConfigurationInterpolator
every time the builder's
getConfiguration()
method is called. The resolved pattern
is then interpreted as the name of a configuration file to be loaded
by a newly created
FileBasedConfigurationBuilder
.
The file-based configuration builder is stored in an internal map
together with the naming pattern. The next time the same pattern is
accessed, it can be retrieved from the map and used directly. If the
evaluation of the pattern changes, a new file-based configuration
builder is created, and a new configuration file is loaded.
When used in a combined
configuration it is often acceptable for a file
matching a particular pattern to be missing. The behavior of
MultiFileConfigurationBuilder
regarding exceptions
thrown for missing configuration files can be controlled using the
boolean allowFailOnInit argument accepted by the most generic
constructor. If here the value true is passed,
exceptions while loading a configuration file are ignored. Instead,
an empty configuration of the configured type is created for this
pattern.
DynamicCombinedConfiguration
The
CombinedConfigurationBuilder
class allows multiple configuration files to be
merged together. However, it will not collaborate with a MultiFileConfiguration
properly since the underlying managed configuration will be different depending
on the resolution of the location pattern.
DynamicCombinedConfiguration
solves this by creating a new
CombinedConfiguration
for each pattern.
Sample Configuration
This sample configuration illustrates how to use
CombinedConfigurationBuilder
in combination with
MultiFileConfigurationBuilder
to create a multi-tenant
configuration.
<configuration>
<header>
<result loggerName="TestLogger"
config-class="org.apache.commons.configuration2.DynamicCombinedConfiguration"
keyPattern="$${sys:Id}">
<nodeCombiner config-class="org.apache.commons.configuration2.tree.MergeCombiner"/>
<expressionEngine
config-class="org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine"/>
</result>
</header>
<override>
<multiFile filePattern="testMultiConfiguration_$${sys:Id}.xml"
config-name="clientConfig" config-optional="true"
config-forceCreate="true"
schemaValidation="true">
<expressionEngine
config-class="org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine"/>
</multiFile>
<xml fileName="testMultiConfiguration_default.xml"
config-name="defaultConfig" schemaValidation="true">
<expressionEngine
config-class="org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine"/>
</xml>
</override>
</configuration>
In this example configuration definition file for a
CombinedConfigurationBuilder
two configuration sources
are declared:
- A multi-file source with a file pattern containing a system
property. This means that depending on the current value of this
system property a different configuration file is loaded. Because
this source is declared as optional it is legal that for certain
values of the property no configuration file exists.
- An XML configuration source with common setting independent on
the value of a system property. This source may also contain
default values in case no configuration file could be loaded by the
multi-file source.
Of special importance is the declaration of the result configuration
in form of the <result>
element in the header
section. Here the config-class
attribute specifies that
an instance of DynamicCombinedConfiguration
is created
rather than a default CombinedConfiguration
object. Also,
the value of the keyPattern property is set which has to
conform to the pattern used by the multi-file source.
Note how the variables have multiple '$'. This is how variables are escaped and
is necessary because the variables will be interpolated multiple times. Each
attempt will remove the leading '$'. When there is only a single '$' in front
of the '{' the interpolator will then resolve the variable. The first extra '$'
is necessary because CombinedConfigurationBuilder
will interpolate any variables
in the configuration. In the case of the multi-file configuration item two
leading '$' characters are necessary before the variable because it will be
interpolated by both CombinedConfigurationBuilder
and DynamicCombinedConfiguration
before MultiFileConfigurationBuilder
gets the chance to evaluate it.
Although in this example one would not expect system properties to change
at runtime, other types of lookups such as the MDCStrLookup provided with
SLF4J require that the variables be evaluated as the configuration is being
accessed instead of when the configuration file is processed to behave as desired.
Builder Configuration Related to Multi-file Configurations
When setting up a
MultiFileConfigurationBuilder
a special object with
initialization parameters can be used as argument to the
configure()
method. It is of type
MultiFileBuilderParameters
and can be obtained via the
multiFile()
method of a
Parameters
object. The properties specific to this
configuration type are defined by the
MultiFileBuilderProperties
interface. They include
- The pattern string for determining the name of the
configuration file to be loaded. This is of course the most
important setting as it tells the builder how to perform interpolation
in order to resolve the correct configuration file.
- A parameters object for the file-based configuration builder
used behind the scenes to load the configuration file. Here some
additional settings can be provided. For instance, if the
configuration files to be loaded are XML documents, validation
could be enabled via these parameters.
Below is a code fragment demonstrating the set up of a
MultiFileConfigurationBuilder
which loads configuration
files of type XML:
Parameters params = new Parameters();
MultiFileConfigurationBuilder<XMLConfiguration> builder =
new MultiFileConfigurationBuilder(XMLConfiguration.class)
.configure(params.multiFile()
.setFilePattern("configuration_${sys:Id}.xml")
.setManagedBuilderParameters(params.xml()
.setValidating(true)
)
);
XMLConfiguration config = builder.getConfiguration();
PatternSubtreeConfigurationWrapper
Applications are often composed of many components each of which need their
own configuration. This can be accommodated by having a configuration file
per component, but this can make things hard to manage when there are many
clients and many components. A second approach is to combine them into
a single configuration file. However, this either means the subcomponent
has to be aware of the surrounding configuration and navigate past it or the
application must be provided just the portion of the configuration it
can process.
PatternSubtreeConfigurationWrapper
can be used for this purpose.
Normal practice when using dependency injection frameworks is to have the
attributes needed to make components work correctly injected into them.
When working with Commons Configuration this works very well. Components
simply need to have a
HierarchicalConfiguration
attribute along with
a corresponding setter and getter. The injection framework can then be
used to provide the component with the correct configuration using
PatternSubtreeConfigurationWrapper
as shown in the next example.
<bean id="configurationBuilder"
class="org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder">
<constructor-arg index="0"
value="org.apache.commons.configuration2.XMLConfiguration"/>
<constructor-arg index="1">
<map>
<entry key="config-fileBased">
<map>
<entry key="fileName" value="configuration.xml"/>
</map>
</entry>
</map>
</constructor-arg>
</bean>
<bean id="applicationConfig" factory-bean="configurationBuilder"
factory-method="getConfiguration">
</bean>
<bean id="subcomponentConfig"
class="org.apache.commons.configuration2.PatternSubtreeConfigurationWrapper"
autowire='autodetect'>
<constructor-arg index="0">
<ref bean="applicationConfig"/>
</constructor-arg>
<constructor-arg index="1" value="/Components/MyComponent"/>
</bean>
<bean id='MyComponent' class='org.test.MyComponent' autowire='autodetect'>
<property name="configuration">
<ref bean="subcomponentConfig"/>
</property>
</bean>
This example shows a Spring
configuration defining beans to be used in an application. The bean
named "MyComponent" is a component with its own
configuration which is injected into the configuration
property. Here an instance of PatternSubtreeConfigurationWrapper
is passed (defined by the "subcomponentConfig" bean) that
selects a subtree in the configuration data specific to this component;
the corresponding prefix is defined as second constructor argument.
It is also of interest how the global configuration of the
application is defined. It is loaded by a configuration builder
declared by the "configurationBuilder" bean. The fluent
API offered by configuration builders does not work very well here.
Therefore, the builder is initialized via a map with settings passed
to its constructor. Simple properties to be propagated to the
managed configuration instance can be declared directly as keys of
this map. The configuration of the file to be loaded is an
exceptional case: This information is stored internally as a
FileHandler
object, and the properties of this object are
contained in a sub map under the key config-fileBased.
With the declaration of the configuration builder in place, the actual
configuration object can now be defined as bean using the builder's
getConfiguration()
method as factory method. From
there it can be injected into arbitrary other beans.