Apart from properties files, the database table can also be a place to get key-value pairs. Great, so just extend the PropertySourcesPlaceholderConfigurer, and have it read a table containing the key-value pairs, populate them and we're done!
However, there's a slight problem. If the DataSource bean also relies on values obtained from a properties file (e.g. JDBC URL, username, password), and being good Springers, inject this bean to the bean class extending PropertySourcesPlaceholderConfigurer, the bean container will fail to startup properly, because the 'jdbc.driverClassName' variable cannot be resolved. Strange, but true.
The reason for this is that any bean injected into a BeanFactoryPostProcessor class will trigger bean initialization BEFORE the BeanFactoryPostProcessor classes are run. You know, dependency injection...all depending beans have to be ready before being injected into the consumer. So this creates a cyclic-dependency kind of thing. All dependencies in the XML configuration are resolved first before the BeanFactoryPostProcessor classes are run.
So, how to go about this? Well, there's a trick you can employ. A BeanFactoryPostProcessor class has access to the ConfigurableListableBeanFactory object via the 'postProcessBeanFactory' method. From this object, you can do a 'getBean' and get a reference of any bean with an id. And guess what, you can get the vaunted DataSource bean without triggering premature bean initialization.
Let's say there's a table 'sys_param' with the following data:
PARAM_CD PARAM_VALUE
-------------- --------------
service.charge 1.5
rebate.amount 15.99
smtp.ip 173.194.79.16
The DbPropertySourcesPlaceholderConfigurer is shown here:
package org.gizmo.labs.utils.spring;
import javax.sql.DataSource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
public class DbPropertySourcesPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer
{
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
{
DataSource dataSource = beanFactory.getBean(DataSource.class);
DbProperties dbProps = new DbProperties(dataSource);
setProperties(dbProps);
super.postProcessBeanFactory(beanFactory);
}
}
The DbProperties class will make use of the DataSource reference and queries the database to get the key-value pairs:
package org.gizmo.labs.utils.spring;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
public class DbProperties extends Properties
{
private final Logger logger = LoggerFactory.getLogger(DbProperties.class);
private static final long serialVersionUID = 1L;
public DbProperties(DataSource dataSource)
{
super();
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
List
To demonstrate that the values from the table are properly injected, here's the class which acts as the consumer:
package org.gizmo.labs.utils.spring;
import java.math.BigDecimal;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
public class DbPropConsumer implements InitializingBean
{
private final Logger logger = LoggerFactory.getLogger(DbPropConsumer.class);
private BigDecimal serviceCharge;
private double rebateAmount;
private String smtpIp;
@Override
public void afterPropertiesSet() throws Exception
{
logger.debug("I have consumed: {}", this);
}
public String toString()
{
return ReflectionToStringBuilder.toString(this, ToStringStyle.MULTI_LINE_STYLE);
}
public BigDecimal getServiceCharge() {
return serviceCharge;
}
public void setServiceCharge(BigDecimal serviceCharge) {
this.serviceCharge = serviceCharge;
}
public double getRebateAmount() {
return rebateAmount;
}
public void setRebateAmount(double rebateAmount) {
this.rebateAmount = rebateAmount;
}
public String getSmtpIp() {
return smtpIp;
}
public void setSmtpIp(String smtpIp) {
this.smtpIp = smtpIp;
}
}
Last but not least, the Spring configuration (DataSource bean not shown, simplified for clarity):
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemalocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="order" value="1">
<property name="locations">
<list>
<value>classpath:system.properties</value>
</list>
</property>
</property></bean>
<bean class="org.gizmo.labs.utils.spring.DbPropertySourcesPlaceholderConfigurer">
<property name="order" value="2">
<property name="placeholderPrefix" value="%{">
<property name="placeholderSuffix" value="}">
</property></property></property></bean>
<bean class="org.gizmo.labs.utils.spring.DbPropConsumer" lazy-init="false">
<property name="serviceCharge" value="%{service.charge}">
<property name="rebateAmount" value="%{rebate.amount}">
<property name="smtpIp" value="%{smtp.ip}">
</property></property></property></bean>
</beans>
The first 2 bean definitions are the BeanFactoryPostProcessor classes, and to ensure the first one is run first, the 'order' property is set (lower means higher precedence).
For the DbPropertySourcesPlaceholderConfigurer, a different placeholder prefix and suffix is used for clarity (notice the placeholders for DbPropConsumer).
So, upon Spring container startup, you should be able to view a similar output as below:
2012-09-18 00:03:14, DEBUG, org.gizmo.labs.utils.spring.DbProperties, Loading from DB: [service.charge:1.5]
2012-09-18 00:03:14, DEBUG, org.gizmo.labs.utils.spring.DbProperties, Loading from DB: [rebate.amount:15.99]
2012-09-18 00:03:14, DEBUG, org.gizmo.labs.utils.spring.DbProperties, Loading from DB: [smtp.ip:173.194.79.16]
2012-09-18 00:03:14, DEBUG, org.gizmo.labs.utils.spring.DbPropConsumer, I have consumed: org.gizmo.labs.utils.spring.DbPropConsumer@189b939[
logger=Logger[org.gizmo.labs.utils.spring.DbPropConsumer]
serviceCharge=1.5
rebateAmount=15.99
smtpIp=173.194.79.16
]
4 comments:
Thanks, This is great. I am using this idea, but ran into the following issue. If i want my db properties to fill in a nested value in the file one, it does not work. I found a workaround where i set it as environment variable but not sure there is a better way to do it
Solving my own issue. Instead of defining two objects, the PropertySourcesPlaceholderConfigure and the DbPropertySourcesPlaceholderConfigurer, just define teh DbPropertySourcesPlaceholderConfigurer and add the file locations to that one. Works great!
Hello Allen,
I tried suggested approach, but it did not work for me. I am using spring 4.2.4. I see that postProcessBeanFactory is invoked only after resolving other beans. So I always get this exception if any of the bean uses a property:
Exception in thread “main” org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean definition with name ‘customer’ defined in class path resource [test-context.xml]: Could not resolve placeholder ‘configParam’ in value “${configParam}”; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder ‘configParam’ in value “${configParam}”
at org.springframework.beans.factory.config.PlaceholderConfigurerSupport.doProcessProperties(PlaceholderConfigurerSupport.java:228)
Please advice.
I was able to solve my problem, I missed adding place holder prefix and suffixes, why is it necessary ?
What i understand now is, the first PropertySourcesPlaceholderConfigurer bean instantiated and resolved all the properties it could and then second one, so it requires a different prefix/ suffix ?
Can you please help me understand.
Post a Comment