Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Load spring boot app properties from database

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?

like image 530
Maya Avatar asked Sep 25 '17 14:09

Maya


People also ask

What are the application properties in Spring Boot?

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

Does Spring Boot support JDBC?

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.

How to get the configuration from the database in Spring Boot?

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.

How to load external JSON data in Spring Boot?

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.


3 Answers

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.

like image 110
Maya Avatar answered Sep 28 '22 08:09

Maya


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;
        }
    }
}
like image 20
pxzxj Avatar answered Sep 28 '22 08:09

pxzxj


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.

like image 41
java-addict301 Avatar answered Sep 28 '22 09:09

java-addict301