Spring Boot 从数据库加载Sping Boot 应用程序属性

j91ykkif  于 2023-02-22  发布在  Spring
关注(0)|答案(5)|浏览(176)

我需要你的建议我这个问题,在一个spring Boot 应用程序中,我从数据库加载一些属性,如(cron周期,电子邮件数据),我需要在应用程序上下文中导出这些属性,以便spring构建与加载的数据对应的bean。

bt1cpqcv

bt1cpqcv1#

对于那些需要在应用程序启动之前从数据库加载属性,并使这些属性在项目中的任何地方都可以通过@Value访问的人,只需添加此处理器。

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

application.properties中,必须存在数据源数据才能连接到数据库。
然后在文件夹META-INF中创建一个名为spring.factories的文件,并在其中放置以下行:

org.springframework.boot.env.EnvironmentPostProcessor=test.config.ReadDbPropertiesPostProcessor

就这样,检索到的属性将在任何地方访问。

83qze16e

83qze16e2#

我认为使用BeanPostProcessor和Binder是个好主意,这样就不需要列出所有想要读取的属性。下面的代码引用了ConfigurationPropertysBindingPostProcessor。

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;
    }
}

修改Environment中的PropertySources也是可以实现的,实现BeanPostProcessor接口,在创建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;
        }
    }
}
zsbz8rwp

zsbz8rwp3#

您可以根据自己的需要手动地用数据库值配置bean(这样您就可以利用SpringCDI和 Boot 数据库配置)。
以设置会话超时为例:

@SpringBootApplication
public class MySpringBootApplication extends SpringBootServletInitializer {           
    public static void main(String[] args) {
        SpringApplication.run(MySpringBootApplication.class, args);
    }

    @Bean
    public HttpSessionListener httpSessionListener(){
        return new MyHttpSessionListener();
    }
}

然后是用于配置bean的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
    }

}

注意:您可以将数据库调用移到@PostConstruct方法,以避免为每个会话都调用。

4ktjp1zp

4ktjp1zp4#

使用org.apache.commons:commons-configuration2:jar:2.8.0 dependency中的DatabaseConfiguration类可以让生活变得更轻松,结合springboot2.7.3(rsp.spring5.3.22),下面的设置对我来说很有效。
预处理:必须在别处配置DataSource类型的bean。

import javax.sql.DataSource;

import org.apache.commons.configuration2.ConfigurationConverter;
import org.apache.commons.configuration2.DatabaseConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;

@Configuration
public class ConfigFromDb implements ApplicationContextAware
{
    @Override
    public void setApplicationContext( ApplicationContext aApplicationContext )
    {
        var dbConfig = aApplicationContext.getBean(DatabaseConfiguration.class);
        var props = ConfigurationConverter.getProperties(dbConfig);
        var propSource = new PropertiesPropertySource("CONFIGURATION_FROM_DB", props);
        var env = (ConfigurableEnvironment) aApplicationContext.getEnvironment();
        env.getPropertySources().addLast(propSource);
    }

    @EventListener
    public void onAppStart(ContextRefreshedEvent aEvent)
    {
        var context = aEvent.getApplicationContext();
        var env = context.getEnvironment();
        System.out.println( "ConfigKeyFromDB: " + env.getProperty("ConfigKeyFromDB") );
    }

    @Bean
    public static DatabaseConfiguration databaseConfiguration( DataSource aDataSource )
    {
        var result = new DatabaseConfiguration();
        result.setDataSource( aDataSource);
        result.setTable( "CONFIGURATION" );
        result.setKeyColumn( "CONFIG_KEY" );
        result.setValueColumn( "CONFIG_VALUE" );
        return result;
    }
}

bean DatabaseConfiguration能够从DB加载配置值(DB的访问权限在参数aDataSource中配置)。
DB有一个名为"CONFIGURATION"的表。这个表有一个名为"CONFIG_KEY"的列和一个名为"CONFIG_VALUE"的列(当然,表和列可以有你选择的任何名称)。
setApplicationContext方法从上下文中获取上述bean,将配置的值提取为java Properties类,并将这些值转换为可添加到环境中的PropertysPropertySource。
事件监听器onAppStart只是为了证明这样的DB配置值可以从环境中获取。
请注意,将属性添加到spring的环境中并不足以解析属性占位符(例如,在@Value注解的成员中)。要实现这一点,您必须再提供一个bean:

import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
@Bean
public static PropertySourcesPlaceholderConfigurer dbConfigForVariableResolving(DatabaseConfiguration aDbConfig)
{
    var result = new PropertySourcesPlaceholderConfigurer();
    var props = ConfigurationConverter.getProperties(aDbConfig);
    result.setProperties(props);
    
    return result;
}
biswetbf

biswetbf5#

对@Maya的回答做一点补充,当你在你的spring Boot 应用程序中使用多个配置文件(例如dev,prd)时,你需要检查environment.getActiveProfile().length > 0。我不知道为什么spring调用我的后处理器两次,一次使用配置文件,所以我的最终代码如下:

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    if(environment.getActiveProfile().length == 0)
       return;
    

     // Remaining code ...

相关问题