Spring Конфигурация источника окружающей среды

Я работаю над библиотекой приложений с классом утилиты под названием "Config", который поддерживается объектом Spring Environment и предоставляет строго типизированные геттеры для всех значений конфигурации приложений.

Источники ресурсов для конфигурации могут варьироваться в зависимости от среды (DEV/PROD) и использования (автономный/test/webapp) и могут варьироваться от стандартных (системных и env-реквизитов) до настраиваемых баз данных и источников JNDI.

То, с чем я борюсь, - это позволить приложениям, использующим эту библиотеку, легко конфигурировать источники (-и) свойств, используемые Environment, чтобы свойства были доступны для использования в нашем классе Config и через PropertySourcesPlaceholderConfigurer.

Мы по-прежнему используем XML-конфигурацию, поэтому в идеале это можно настроить в XML-виде.

<bean id="propertySources">
 <property name="sources">
 <list>
 <ref local="jndiPropertySource">
 <ref local="databasePropertySource">
 </ref></ref></list>
 </property>
</bean>

... и затем каким-то образом ввел в коллекцию источников свойств среды.

Я читал, что что-то вроде этого может быть невозможно из-за времени жизненного цикла контекста приложения, и что это, возможно, потребуется сделать с помощью класса инициализатора приложения.

Любые идеи?

5 ответов

Я придумал следующее, которое, похоже, работает, но я довольно новичок в Spring, поэтому я не уверен, как он будет поддерживаться в разных вариантах использования.

В принципе, подход заключается в расширении PropertySourcesPlaceholderConfigurer и добавлении установщика, позволяющего пользователю легко настраивать объекты List из PropertySource в XML. После создания источники свойств копируются в текущий Environment.

Это в основном позволяет настраивать источники ресурсов в одном месте, но используется как с конфигурацией placholder, так и с сценариями Environment.getProperty.

Расширенный PropertySourcesPlaceholderConfigurer

public class ConfigSourcesConfigurer 
 extends PropertySourcesPlaceholderConfigurer
 implements EnvironmentAware, InitializingBean {
 private Environment environment;
 private List<propertysource> sourceList;
 // Allow setting property sources as a List for easier XML configuration
 public void setPropertySources(List<propertysource> propertySources) {
 this.sourceList = propertySources;
 MutablePropertySources sources = new MutablePropertySources();
 copyListToPropertySources(this.sourceList, sources); 
 super.setPropertySources(sources);
 }
 @Override
 public void setEnvironment(Environment environment) {
 // save off Environment for later use
 this.environment = environment;
 super.setEnvironment(environment);
 }
 @Override
 public void afterPropertiesSet() throws Exception {
 // Copy property sources to Environment
 MutablePropertySources envPropSources = ((ConfigurableEnvironment)environment).getPropertySources();
 copyListToPropertySources(this.sourceList, envPropSources);
 }
 private void copyListToPropertySources(List<propertysource> list, MutablePropertySources sources) {
 // iterate in reverse order to insure ordering in property sources object
 for(int i = list.size() - 1; i >= 0; i--) {
 sources.addFirst(list.get(i));
 }
 }
}
</propertysource></propertysource></propertysource>

beans.xml файл с базовой конфигурацией

<beans>
 <context:annotation-config>
 <context:component-scan base-package="com.mycompany">
 <bean>
 <property name="propertySources">
 <list>
 <bean>
 <bean>
 <constructor-arg value="classpath:default-config.properties">
 </constructor-arg></bean>
 </bean></list>
 </property>
 </bean>
 <bean>
 <property name="stringValue" value="${placeholder}">
 </property></bean>
</context:component-scan></context:annotation-config></beans>


Это зависит от того, как вы хотите использовать свойства, если нужно вводить свойства с помощью синтаксиса ${propertyname}, тогда да, будет работать PropertySourcesPlaceHolderConfigurer, который внутренне имеет доступ к ресурсам, зарегистрированным в среде.

Если вы планируете напрямую использовать среду, используя say env.getProperty(), тогда вы правы - свойства, использующие PropertySourcesPlaceHolderConfigurer, здесь не видны. Единственный путь - это ввести его с помощью Java-кода, есть два способа, которыми я знаю:

а. Использование Java Config:

@Configuration
@PropertySource("classpath:/app.properties")
public class SpringConfig{
}

б. Использование пользовательского ApplicationContextInitializer, как описано здесь


Следующее работало для меня с Spring 3.2.4.

PropertySourcesPlaceholderConfigurer должен быть зарегистрирован статически для обработки заполнителей.

Источник настраиваемого свойства зарегистрирован в методе init, а поскольку источники свойств по умолчанию уже зарегистрированы, он сам может быть параметризован с использованием заполнителей.

Класс JavaConfig:

@Configuration
@PropertySource("classpath:propertiesTest2.properties")
public class TestConfig {
 @Autowired
 private ConfigurableEnvironment env;
 @Value("${param:NOVALUE}")
 private String param;
 @PostConstruct
 public void init() {
 env.getPropertySources().addFirst(new CustomPropertySource(param));
 }
 @Bean
 public static PropertySourcesPlaceholderConfigurer placeHolderConfigurer() {
 return new PropertySourcesPlaceholderConfigurer();
 }
 @Bean
 public TestBean1 testBean1() {
 return new TestBean1();
 }
 }

Свойство настраиваемого свойства:

public class CustomPropertySource extends PropertySource<object> {
 public CustomPropertySource(String param) {
 super("custom");
 System.out.println("Custom property source initialized with param " + param + ".");
 }
 @Override
 public Object getProperty(String name) {
 return "IT WORKS";
 }
}
<p>Тест bean (<code>getValue()</code> выведет <code>"IT WORKS"</code>):</p>
<pre class="prettyprint linenums"><code>public class TestBean1 {
 @Value("${value:NOVALUE}")
 private String value;
 public String getValue() {
 return value;
 }
}
</code>


У меня была аналогичная проблема, в моем случае я использую Spring в автономном приложении, после загрузки конфигураций по умолчанию мне может потребоваться применить еще один файл свойств (lazy load configs), присутствующий в каталоге конфигурации. Мое решение было вдохновлено этой Spring Boot документацией, но без зависимости от Spring Boot. См. Ниже исходный код:

@PropertySources(@PropertySource(value = "classpath:myapp-default.properties"))
public class PersistenceConfiguration {
 private final Logger log = LoggerFactory.getLogger(getClass());
 private ConfigurableEnvironment env;
 @Bean
 public static PropertySourcesPlaceholderConfigurer placeholderConfigurerDev(ConfigurableEnvironment env) {
 return new PropertySourcesPlaceholderConfigurer();
 }
 @Autowired
 public void setConfigurableEnvironment(ConfigurableEnvironment env) {
 for(String profile: env.getActiveProfiles()) {
 final String fileName = "myapp-" + profile + ".properties";
 final Resource resource = new ClassPathResource(fileName);
 if (resource.exists()) {
 try {
 MutablePropertySources sources = env.getPropertySources();
 sources.addFirst(new PropertiesPropertySource(fileName,PropertiesLoaderUtils.loadProperties(resource)));
 } catch (Exception ex) {
 log.error(ex.getMessage(), ex);
 throw new RuntimeException(ex.getMessage(), ex);
 }
 }
 }
 this.env = env;
 }
 ...
}


Недавно я столкнулся с проблемой регистрации пользовательских источников свойств в среде. Моя проблема заключается в том, что у меня есть библиотека с конфигурацией Spring, которую я хочу импортировать в контекст приложения Spring, и для этого требуются собственные источники свойств. Однако я не обязательно контролирую все места, где создается контекст приложения. Из-за этого я не хочу использовать рекомендуемые механизмы ApplicationContextInitializer или register-before-refresh для регистрации настраиваемых источников свойств.

То, что я действительно расстроило, заключается в том, что, используя старый PropertyPlaceholderConfigurer, было легко подклассифицировать и настроить конфигураторы полностью в конфигурации Spring. Напротив, для настройки источников свойств нам говорят, что мы должны сделать это не в самой конфигурации Spring, а перед инициализацией контекста приложения.

После некоторых исследований и испытаний и ошибок я обнаружил, что можно зарегистрировать настраиваемые источники свойств изнутри конфигурации Spring, но вы должны быть осторожны, как вы это делаете. Источники должны быть зарегистрированы до того, как любой объект PropertySourcesPlaceholderConfigurers выполнит в контексте. Вы можете сделать это, сделав исходную регистрацию BeanFactoryPostProcessor с PriorityOrdered и порядком, который имеет более высокий приоритет, чем PropertySourcesPlaceholderConfigurer, который использует источники.

Я написал этот класс, который выполняет задание:

package example;
import java.io.IOException;
import java.util.Properties;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.PropertiesLoaderSupport;
/**
 * This is an abstract base class that can be extended by any class that wishes
 * to become a custom property source in the Spring context.
 * <p>
 * This extends from the standard Spring class PropertiesLoaderSupport, which
 * contains properties that specify property resource locations, plus methods
 * for loading properties from specified resources. These are all available to
 * be used from the Spring configuration, and by subclasses of this class.
 * </p><p>
 * This also implements a number of Spring flag interfaces, all of which are
 * required to maneuver instances of this class into a position where they can
 * register their property sources BEFORE PropertySourcesPlaceholderConfigurer
 * executes to substitute variables in the Spring configuration:
 * </p><ul>
 * <li>BeanFactoryPostProcessor - Guarantees that this bean will be instantiated
 * before other beans in the context. It also puts it in the same phase as
 * PropertySourcesPlaceholderConfigurer, which is also a BFPP. The
 * postProcessBeanFactory method is used to register the property source.</li>
 * <li>PriorityOrdered - Allows the bean priority to be specified relative to
 * PropertySourcesPlaceholderConfigurer so that this bean can be executed first.
 * </li>
 * <li>ApplicationContextAware - Provides access to the application context and
 * its environment so that the created property source can be registered.</li>
 * </ul>
 * <p>
 * The Spring configuration for subclasses should contain the following
 * properties:
 * </p><ul>
 * <li>propertySourceName - The name of the property source this will register.</li>
 * <li>location(s) - The location from which properties will be loaded.</li>
 * <li>addBeforeSourceName (optional) - If specified, the resulting property
 * source will be added before the given property source name, and will
 * therefore take precedence.</li>
 * <li>order (optional) - The order in which this source should be executed
 * relative to other BeanFactoryPostProcessors. This should be used in
 * conjunction with addBeforeName so that if property source factory "psfa"
 * needs to register its property source before the one from "psfb", "psfa"
 * executes AFTER "psfb".
 * </li></ul>
 * 
 * @author rjsmith2
 *
 */
public abstract class AbstractPropertySourceFactory extends
 PropertiesLoaderSupport implements ApplicationContextAware,
 PriorityOrdered, BeanFactoryPostProcessor {
 // Default order will be barely higher than the default for
 // PropertySourcesPlaceholderConfigurer.
 private int order = Ordered.LOWEST_PRECEDENCE - 1;
 private String propertySourceName;
 private String addBeforeSourceName;
 private ApplicationContext applicationContext;
 private MutablePropertySources getPropertySources() {
 final Environment env = applicationContext.getEnvironment();
 if (!(env instanceof ConfigurableEnvironment)) {
 throw new IllegalStateException(
 "Cannot get environment for Spring application context");
 }
 return ((ConfigurableEnvironment) env).getPropertySources();
 }
 public int getOrder() {
 return order;
 }
 public void setOrder(int order) {
 this.order = order;
 }
 public String getPropertySourceName() {
 return propertySourceName;
 }
 public void setPropertySourceName(String propertySourceName) {
 this.propertySourceName = propertySourceName;
 }
 public String getAddBeforeSourceName() {
 return addBeforeSourceName;
 }
 public void setAddBeforeSourceName(String addBeforeSourceName) {
 this.addBeforeSourceName = addBeforeSourceName;
 }
 public void setApplicationContext(ApplicationContext applicationContext) {
 this.applicationContext = applicationContext;
 }
 /**
 * Subclasses can override this method to perform adjustments on the
 * properties after they are read.
 * <p><code>
 * This should be done by getting, adding, removing, and updating properties
 * as needed.
 * 
 * @param props
 * properties to adjust
 */
 protected void convertProperties(Properties props) {
 // Override in subclass to perform conversions.
 }
 /**
 * Creates a property source from the specified locations.
 * 
 * @return PropertiesPropertySource instance containing the read properties
 * @throws IOException
 * if properties cannot be read
 */
 protected PropertySource<!--?--> createPropertySource() throws IOException {
 if (propertySourceName == null) {
 throw new IllegalStateException("No property source name specified");
 }
 // Load the properties file (or files) from specified locations.
 final Properties props = new Properties();
 loadProperties(props);
 // Convert properties as required.
 convertProperties(props);
 // Convert to property source.
 final PropertiesPropertySource source = new PropertiesPropertySource(
 propertySourceName, props);
 return source;
 }
 @Override
 public void postProcessBeanFactory(
 ConfigurableListableBeanFactory beanFactory) throws BeansException {
 try {
 // Create the property source, and get its desired position in
 // the list of sources.
 if (logger.isDebugEnabled()) {
 logger.debug("Creating property source [" + propertySourceName
 + "]");
 }
 final PropertySource<!--?--> source = createPropertySource();
 // Register the property source.
 final MutablePropertySources sources = getPropertySources();
 if (addBeforeSourceName != null) {
 if (sources.contains(addBeforeSourceName)) {
 if (logger.isDebugEnabled()) {
 logger.debug("Adding property source ["
 + propertySourceName + "] before ["
 + addBeforeSourceName + "]");
 }
 sources.addBefore(addBeforeSourceName, source);
 } else {
 logger.warn("Property source [" + propertySourceName
 + "] cannot be added before non-existent source ["
 + addBeforeSourceName + "] - adding at the end");
 sources.addLast(source);
 }
 } else {
 if (logger.isDebugEnabled()) {
 logger.debug("Adding property source ["
 + propertySourceName + "] at the end");
 }
 sources.addLast(source);
 }
 } catch (Exception e) {
 throw new BeanInitializationException(
 "Failed to register property source", e);
 }
 }
}
</code>
</p><p>Следует отметить, что порядок по умолчанию этого класса свойств factory имеет более высокий приоритет, чем порядок по умолчанию для PropertySourcesPlaceholderConfigurer по умолчанию.</p>
<p>Кроме того, регистрация источника собственности происходит в postProcessBeanFactory, что означает, что он будет выполняться в правильном порядке относительно PropertySourcesPlaceholderConfigurer. Я обнаружил трудный путь, что InitializingBean и afterPropertiesSet не учитывают параметр порядка, и я отказался от этого подхода как ошибочный и избыточный.</p>
<p>Наконец, поскольку это BeanFactoryPostProcessor, это плохая идея, чтобы попытаться связать многое в зависимости от зависимостей. Поэтому класс обращается к среде непосредственно через контекст приложения, который он получает с помощью ApplicationContextAware.</p>
<p>В моем случае мне понадобился источник свойств для дешифрования свойств пароля, который я реализовал с помощью следующего подкласса:</p>
<pre class="prettyprint linenums">package example;
import java.util.Properties;
/**
 * This is a property source factory that creates a property source that can
 * process properties for substituting into a Spring configuration.
 * <p><code>
 * The only thing that distinguishes this from a normal Spring property source
 * is that it decrypts encrypted passwords.
 * 
 * @author rjsmith2
 *
 */
public class PasswordPropertySourceFactory extends
 AbstractPropertySourceFactory {
 private static final PasswordHelper passwordHelper = new PasswordHelper();
 private String[] passwordProperties;
 public String[] getPasswordProperties() {
 return passwordProperties;
 }
 public void setPasswordProperties(String[] passwordProperties) {
 this.passwordProperties = passwordProperties;
 }
 public void setPasswordProperty(String passwordProperty) {
 this.passwordProperties = new String[] { passwordProperty };
 }
 @Override
 protected void convertProperties(Properties props) {
 // Adjust password fields by decrypting them.
 if (passwordProperties != null) {
 for (String propName : passwordProperties) {
 final String propValue = props.getProperty(propName);
 if (propValue != null) {
 final String plaintext = passwordHelper
 .decryptString(propValue);
 props.setProperty(propName, plaintext);
 }
 }
 }
 }
}
</code>
</p><p>Наконец, я указал источник свойств factory в моей конфигурации Spring:</p>
<pre class="prettyprint linenums"><!-- Enable property resolution via PropertySourcesPlaceholderConfigurer. 
 The order has to be larger than the ones used by custom property sources 
 so that those property sources are registered before any placeholders
 are substituted. -->
<context:property-placeholder order="1000" ignore-unresolvable="true">
<!-- Register a custom property source that reads DB properties, and
 decrypts the database password. -->
<bean>
 <property name="propertySourceName" value="DBPropertySource">
 <property name="location" value="classpath:db.properties">
 <property name="passwordProperty" value="db.password">
 <property name="ignoreResourceNotFound" value="true">
 <!-- Order must be lower than on property-placeholder element. -->
 </property></property></property></property></bean>
</context:property-placeholder>

Если честно, с настройками по умолчанию в PropertySourcesPlaceholderConfigurer и AbstractPropertySourceFactory, возможно, даже не нужно указывать порядок в конфигурации Spring.

Тем не менее, это работает, и это не требует каких-либо попыток инициализации контекста приложения.

licensed under cc by-sa 3.0 with attribution.