Different environments within application runs usually require different configuration. For example, we might have a development, test and/or production configurations. Configuration is usually stored in (external) properties files and we have to manage it somehow for all different environments. We might use different set of property files for each target environment or to have a building process (ant) that prepares the properties during deployment or test etc.
My idea is to keep all configuration in just one set of files. That makes configuration more visible to a build manager and much easier to maintain. And in most cases, configuration changes are minimal: it is just a matter of root path location(s), JDBC connection credentials and so on; the most of other values are the same. Even when this is not true, it still make sense to have a configuration defined at one, single place.
Idea is very simple: each property may define different value for different profile. Property profile is, therefore, some kind of scope for property values. When resolving properties, it is first checked if there is a property named as requested property with active profile name appended to it. And that's all.
The best way to describe it is on an example:
########### # profile # ########### profile=@develop #profile=@production # jdbc properties jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/foo jdbc.url@production=jdbc:mysql://192.168.1.173:3306/foo jdbc.username=root jdbc.password=root! jdbc.password@production=some_strong_password # cache i18n messages messages.cache=false messages.cache@production=true ...
First we define the active profile (line #4). In above example, if we ask for property 'foo', it is first checked if there is a property 'foo@develop'; if there is no such property, the originally requested property 'foo' will be checked for. Active profile can be simply switched by using different profile name in line #4. For example, if we use '@develop' profile, value for 'messages.cache' will be false. If we switch to '@production' profile (line #5) we will got value 'true' for the same property. Similarly, above example also defines different JDBC url and password for production and development (i.e. all non-production profiles).
What is important here is that profiles values are resolved transparently. There must not be a reference to a property with profile name.
Before we continue with the implementation details, lets first decide to keep active profile as a global variable, so it can be set not only through property file (as in above example), but also programmatically. Now, lets continue.
It is quite simple actually, we just need to tweak Springs PropertyPlaceholderConfigurer.
public class RejoicePropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
@Override
protected void processProperties(ConfigurableListableBeanFactory configurableListableBeanFactory, Properties properties) throws BeansException {
if (AppCore.getProfile() == null) {
String profile = properties.getProperty("profile");
if (StringUtil.isBlank(profile) == false) {
AppCore.setProfile(profile);
}
}
super.processProperties(configurableListableBeanFactory, properties);
}
@Override
protected String resolvePlaceholder(String placeholder, Properties properties, int systemPropertiesMode) {
String value = null;
String profile = AppCore.getProfile();
if (profile != null) {
value = super.resolvePlaceholder(placeholder + profile, properties, systemPropertiesMode);
}
if (value == null) {
value = super.resolvePlaceholder(placeholder, properties, systemPropertiesMode);
}
return value;
}
}
Method processProperties()is called once when all properties have been read from all properties files. So we need to hook before and do some pre-processing, in order to resolve the profile name. If the profile is not already set programmatically, we will try to read it from loaded properties, by reading the value of property named 'profile' (of course, any other name can be used).
The next step is reading properties. During resolving, Spring calls resolvePlaceholder() for all ${} placeholders in spring xml file. This is the place where we gonna first try to resolve the value using placeholder and the profile name. If such combination can not be resolved, we will fail back to the original placeholder name. And that will do the trick:)
Usage is trivial:
<!-- Configurator that replaces ${...} placeholders with values from a properties file -->
<bean class="rejoice.util.RejoicePropertyPlaceholderConfigurer">
<property name="locations" value="classpath*:app*.properties"/>
</bean>
Why stop here? Lets go even further and step into security:)
The very next thing that bothers me is that passwords are all stored in the plaintext. Using above approach it is easy to change that. For example, my implementation of PropertyPlaceholderConfigurer is wired with some CryptoEngine and checks if value starts with specific string, such as 'crypted:'. If so, than the rest of the value is considered to be symmetrically encrypted string that will be decrypted on placeholders resolving. Then I do not have to worry about storing passwords in the plaintext on production.