I need you to advice me with this issue, in a spring boot application I load some properties from database like (cron periods, email data), I need to export these properties in the application context in order to spring build the corresponding beans with the loaded data. How could I do this?
Inside the application properties file, we define every type of property like changing the port, database connectivity, connection to the eureka server, and many more. Now let’s see some examples for better understanding. Sometimes when you run your spring application you may encounter the following type of error
Spring Cloud Config Server supports JDBC (relational database) as a backend for configuration properties. Spring boot Config Server will pull properties from a SQL Database on startup of your application. The database needs to have a table called PROPERTIES.
But Spring Boot does not get the configuration from the database. Show activity on this post. One possible solution that you could workout, is to use ConfigurableEnvironment and reload and add properties. Show activity on this post.
Conclusion The Spring Boot framework provides a simple approach to load external JSON data through the command line. In case of need, we can load JSON data through properly configured PropertySourceFactory. Although, loading nested properties is solvable but requires extra care. As always, the code is available over on GitHub.
For those who need load properties from database before application starts, and make those props accesible by @Value anywhere in your project, just add this processor.
public class ReadDbPropertiesPostProcessor implements EnvironmentPostProcessor {
/**
* Name of the custom property source added by this post processor class
*/
private static final String PROPERTY_SOURCE_NAME = "databaseProperties";
private String[] KEYS = {
"excel.threads",
"cronDelay",
"cronDelayEmail",
"spring.mail.username",
"spring.mail.password",
"spring.mail.host",
"spring.mail.port",
"spring.mail.properties.mail.transport.protocol",
"spring.mail.properties.mail.smtp.auth",
"spring.mail.properties.mail.smtp.starttls.enabled",
"spring.mail.properties.mail.debug",
"spring.mail.properties.mail.smtp.starttls.required",
"spring.mail.properties.mail.socketFactory.port",
"spring.mail.properties.mail.socketFactory.class",
"spring.mail.properties.mail.socketFactory.fallback",
"white.executor.threads",
"white.search.threads",
"lot.sync.threads",
"lot.async.threads",
"lot.soap.threads",
"excel.async.threads",
"kpi.threads",
"upload.threads"
};
/**
* Adds Spring Environment custom logic. This custom logic fetch properties from database and setting highest precedence
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Map<String, Object> propertySource = new HashMap<>();
try {
// Build manually datasource to ServiceConfig
DataSource ds = DataSourceBuilder
.create()
.username(environment.getProperty("spring.datasource.username"))
.password(environment.getProperty("spring.mail.password"))
.url(environment.getProperty("spring.datasource.url"))
.driverClassName("com.mysql.jdbc.Driver")
.build();
// Fetch all properties
Connection connection = ds.getConnection();
JTrace.genLog(LogSeverity.informational, "cargando configuracion de la base de datos");
PreparedStatement preparedStatement = connection.prepareStatement("SELECT value FROM config WHERE id = ?");
for (int i = 0; i < KEYS.length; i++) {
String key = KEYS[i];
preparedStatement.setString(1, key);
ResultSet rs = preparedStatement.executeQuery();
// Populate all properties into the property source
while (rs.next()) {
propertySource.put(key, rs.getString("value"));
}
rs.close();
preparedStatement.clearParameters();
}
preparedStatement.close();
connection.close();
// Create a custom property source with the highest precedence and add it to Spring Environment
environment.getPropertySources().addFirst(new MapPropertySource(PROPERTY_SOURCE_NAME, propertySource));
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
} // class ReadDbPropertiesPostProcessor end
In application.properties must exist datasource data in order to be able to connect to database.
Then in folder META-INF create a file named spring.factories an there put the following line:
org.springframework.boot.env.EnvironmentPostProcessor=test.config.ReadDbPropertiesPostProcessor
And that's it, retreived properties will be accessible anywhere.
I think it’s a good idea to use BeanPostProcessor and Binder so that you don’t need to list all the attributes you want to read. The following code refers to ConfigurationPropertiesBindingPostProcessor.
public class PropertiesInsideDatabaseInitializer implements BeanPostProcessor, InitializingBean, ApplicationContextAware {
private JdbcTemplate jdbcTemplate;
private ApplicationContext applicationContext;
private BeanDefinitionRegistry registry;
private Map<String, Object> systemConfigMap = new HashMap<>();
private final String propertySourceName = "propertiesInsideDatabase";
public PropertiesInsideDatabaseInitializer(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
return bean;
}
private void bind(ConfigurationPropertiesBean propertiesBean) {
if (propertiesBean == null || hasBoundValueObject(propertiesBean.getName())) {
return;
}
Assert.state(propertiesBean.getBindMethod() == ConfigurationPropertiesBean.BindMethod.JAVA_BEAN, "Cannot bind @ConfigurationProperties for bean '"
+ propertiesBean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean");
try {
Bindable<?> target = propertiesBean.asBindTarget();
ConfigurationProperties annotation = propertiesBean.getAnnotation();
BindHandler bindHandler = new IgnoreTopLevelConverterNotFoundBindHandler();
MutablePropertySources mutablePropertySources = new MutablePropertySources();
mutablePropertySources.addLast(new MapPropertySource(propertySourceName, systemConfigMap));
Binder binder = new Binder(ConfigurationPropertySources.from(mutablePropertySources), new PropertySourcesPlaceholdersResolver(mutablePropertySources),
ApplicationConversionService.getSharedInstance(), getPropertyEditorInitializer(), null);
binder.bind(annotation.prefix(), target, bindHandler);
}
catch (Exception ex) {
throw new BeanCreationException("", ex);
}
}
private Consumer<PropertyEditorRegistry> getPropertyEditorInitializer() {
if (this.applicationContext instanceof ConfigurableApplicationContext) {
return ((ConfigurableApplicationContext) this.applicationContext).getBeanFactory()::copyRegisteredEditorsTo;
}
return null;
}
private boolean hasBoundValueObject(String beanName) {
return this.registry.containsBeanDefinition(beanName) && this.registry
.getBeanDefinition(beanName).getClass().getName().contains("ConfigurationPropertiesValueObjectBeanDefinition");
}
@Override
public void afterPropertiesSet() {
String sql = "SELECT key, value from system_config";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
for (Map<String, Object> map : maps) {
String key = String.valueOf(map.get("key"));
Object value = map.get("value");
systemConfigMap.put(key, value);
}
this.registry = (BeanDefinitionRegistry) this.applicationContext.getAutowireCapableBeanFactory();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
Modifying the PropertySources in Environment can also be achieved. The BeanPostProcessor interface is implemented to initialize it before creating the Bean
public class PropertiesInsideDatabaseInitializer implements BeanPostProcessor, InitializingBean, EnvironmentAware {
private JdbcTemplate jdbcTemplate;
private ConfigurableEnvironment environment;
private final String propertySourceName = "propertiesInsideDatabase";
public PropertiesInsideDatabaseInitializer(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void afterPropertiesSet() {
if(environment != null){
Map<String, Object> systemConfigMap = new HashMap<>();
String sql = "SELECT key, value from system_config";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
for (Map<String, Object> map : maps) {
String key = String.valueOf(map.get("key"));
Object value = map.get("value");
systemConfigMap.put(key, value);
}
environment.getPropertySources().addFirst(new MapPropertySource(propertySourceName, systemConfigMap));
}
}
@Override
public void setEnvironment(Environment environment) {
if(environment instanceof ConfigurableEnvironment){
this.environment = (ConfigurableEnvironment) environment;
}
}
}
You could configure the beans with the database values manually depending on what your need is (this way you can take advantage of Spring CDI and boot database configs).
Take setting the session timeout for example:
@SpringBootApplication
public class MySpringBootApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
@Bean
public HttpSessionListener httpSessionListener(){
return new MyHttpSessionListener();
}
}
Then a bean definition for configuring the bean:
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class MyHttpSessionListener implements HttpSessionListener {
@Autowired
private MyRepository myRepository;
@Override
public void sessionCreated(HttpSessionEvent se) {
se.getSession().setMaxInactiveInterval(this.myRepository.getSessionTimeoutSeconds());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// Noop
}
}
Note: you could move the database call to a @PostConstruct
method to avoid making it for each session.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With