programing

데이터베이스에서 스프링 부팅 앱 속성 로드

testmans 2023. 3. 31. 21:55
반응형

데이터베이스에서 스프링 부팅 앱 속성 로드

이 문제에 대한 조언이 필요합니다.스프링 부트 어플리케이션에서는 데이터베이스로부터 속성을 로드합니다.예를 들어 (cron 기간, 이메일 데이터), 로드된 데이터로 대응하는 콩을 스프링 빌드하기 위해 애플리케이션 컨텍스트에서 이러한 속성을 내보내야 합니다.내가 어떻게 이럴 수 있지?

애플리케이션을 기동하기 전에 데이터베이스로부터 속성을 로드해, 프로젝트내의 어느 장소에서나 @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

그게 다야, 회수된 자산은 어디서든 접근할 수 있을 거야

읽고 싶은 속성을 모두 나열할 필요가 없도록 Bean Post Processor와 Binder를 사용하는 것이 좋다고 생각합니다.다음 코드는 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;
    }
}

환경의 PropertySources를 수정할 수도 있습니다.Bean Post Processor 인터페이스는 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;
        }
    }
}

필요에 따라 데이터베이스 값을 사용하여 콩을 수동으로 설정할 수 있습니다(이렇게 하면 Spring CDI 및 부트 데이터베이스 구성을 활용할 수 있습니다).

세션 타임아웃 설정을 예로 들어 보겠습니다.

@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각 세션에 대해 작성하지 않도록 하는 방법.

org.apache.commons:commons-configuration 2:jar:2.8.0 의존관계에서 DatabaseConfiguration 클래스를 사용하면 생활이 훨씬 쉬워집니다.스프링 부트 2.7.3(스프링 5.3.22)과 연계하여 다음 설정이 가능했습니다.

전제 조건: DataSource 유형의 빈이 다른 곳에 구성되어 있어야 합니다.

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의 param aDataSource에서 액세스가 설정됩니다).

DB에는 "CONFIGION"이라는 이름의 테이블이 있습니다.이 테이블에는 "CONFIG_KEY"라는 컬럼과 "CONFIG_VALUE"라는 컬럼이 있습니다(물론 테이블과 컬럼에는 원하는 이름을 사용할 수 있습니다).

set Application Context 메서드는 컨텍스트에서 bean 위의 값을 가져와 Java Properties 클래스로 추출하고 이를 Properties Property Source로 변환하여 환경에 추가할 수 있습니다.

이벤트 리스너 onAppStart는 이러한 DB 구성 값을 환경에서 가져올 수 있음을 증명하기 위한 것입니다.

스프링 환경에 속성을 추가하는 것만으로는 속성 자리 표시자를 해결할 수 없습니다(예: @Value 주석이 달린 멤버).이를 위해서는 콩을 1개 더 제공해야 합니다.

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

언급URL : https://stackoverflow.com/questions/46407230/load-spring-boot-app-properties-from-database

반응형