sjngm
sjngm

Reputation: 12861

@NestedConfigurationProperty and Converter Doesn't Work

I guess I have a rather complex configuration structure that I can't get to work. Here are the important pieces of the configuration classes:

@ConfigurationProperties
public abstract class AbstractConfigHolder<T extends AbstractComponentConfig> {

}

@Component
public class ExportConfigHolder extends AbstractConfigHolder<GenericExportConfig> {

  @NestedConfigurationProperty
  private Map<String, GenericExportConfig> exports;

  // getters and setters for all fields

}

public class GenericExportConfig extends AbstractComponentConfig {

  @NestedConfigurationProperty
  private AbstractLocatedConfig target;

  // getters and setters for all fields

}

public abstract class AbstractLocatedConfig extends RemoteConfig {

  @NestedConfigurationProperty
  private ProxyConfig proxy;

  // getters and setters for all fields

}

public class ProxyConfig extends RemoteConfig {

  private Type type;

  // getters and setters for all fields

}

public class RemoteConfig {

  private String host;
  private int port;
  private String user;
  private String password;

  // getters and setters for all fields

}

Here's the properties file:

exports.mmkb.name=MMKB
exports.mmkb.target=ftp
exports.mmkb.target.path=${user.home}/path/blah
# throws an exception:
exports.mmkb.target.proxy.host=super-host

The conversion stuff is what IMHO should cover everything and provide the proper beans to Spring:

@Configuration
public class ConversionSupport {

  @ConfigurationPropertiesBinding
  @Bean
  public Converter<String, AbstractLocatedConfig> locatedConfigConverter(ApplicationContext applicationContext) {
    return new Converter<String, AbstractLocatedConfig>() {

      private ProxyConfigs proxyConfigs;
      private ConnectionConfigs connectionConfigs;

      @Override
      public AbstractLocatedConfig convert(String targetType) {
        System.out.println("Converting " + targetType);
        initFields(applicationContext);
        switch (targetType.toLowerCase()) {
          case "ftp":
            return new FtpTargetConfig(proxyConfigs, connectionConfigs);
          // others...
        }
      }

      // This is necessary to avoid conflicts in bean dependencies
      private void initFields(ApplicationContext applicationContext) {
        if (proxyConfigs == null) {
          AbstractConfigHolder<?> configHolder = applicationContext.getBean(AbstractConfigHolder.class);
          proxyConfigs = configHolder.getProxy();
          connectionConfigs = configHolder.getConnection();
        }
      }

    };
  }

}

However, I get this instead:

Converting ftp
2016-04-29 09:33:23,900 WARN  [org.springframework.context.annotation.AnnotationConfigApplicationContext] [main] Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'exportConfigHolder': Could not bind properties to ExportConfigHolder (prefix=, ignoreInvalidFields=false, ignoreUnknownFields=true, ignoreNestedProperties=false); nested exception is org.springframework.beans.InvalidPropertyException: Invalid property 'exports[mmkb].target.proxy[host]' of bean class [at.a1.iap.epggw.exporter.config.GenericExportConfig]: Property referenced in indexed property path 'proxy[host]' is neither an array nor a List nor a Map; returned value was [at.a1.iap.epggw.commons.config.properties.ProxyConfig@52066604]
2016-04-29 09:33:23,902 ERROR [org.springframework.boot.SpringApplication] [main] Application startup failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'exportConfigHolder': Could not bind properties to ExportConfigHolder (prefix=, ignoreInvalidFields=false, ignoreUnknownFields=true, ignoreNestedProperties=false); nested exception is org.springframework.beans.InvalidPropertyException: Invalid property 'exports[mmkb].target.proxy[host]' of bean class [at.a1.iap.epggw.exporter.config.GenericExportConfig]: Property referenced in indexed property path 'proxy[host]' is neither an array nor a List nor a Map; returned value was [at.a1.iap.epggw.commons.config.properties.ProxyConfig@52066604]
    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:339)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:289)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:408)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:766)
    at org.springframework.boot.SpringApplication.createAndRefreshContext(SpringApplication.java:361)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1191)
    at at.a1.iap.epggw.exporter.Application.main(Application.java:23)
Caused by: org.springframework.beans.InvalidPropertyException: Invalid property 'exports[mmkb].target.proxy[host]' of bean class [at.a1.iap.epggw.exporter.config.GenericExportConfig]: Property referenced in indexed property path 'proxy[host]' is neither an array nor a List nor a Map; returned value was [at.a1.iap.epggw.commons.config.properties.ProxyConfig@52066604]
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:406)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:280)
    at org.springframework.boot.bind.RelaxedDataBinder$RelaxedBeanWrapper.setPropertyValue(RelaxedDataBinder.java:700)
    at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:95)
    at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:834)
    at org.springframework.validation.DataBinder.doBind(DataBinder.java:730)
    at org.springframework.boot.bind.RelaxedDataBinder.doBind(RelaxedDataBinder.java:128)
    at org.springframework.validation.DataBinder.bind(DataBinder.java:715)
    at org.springframework.boot.bind.PropertiesConfigurationFactory.doBindPropertiesToTarget(PropertiesConfigurationFactory.java:269)
    at org.springframework.boot.bind.PropertiesConfigurationFactory.bindPropertiesToTarget(PropertiesConfigurationFactory.java:241)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:334)
    ... 17 common frames omitted

I mean the error clearly expresses that so far it all worked, there is a proper object in place, but somehow it fails to further apply the properties. I know that it's neither an array nor a List nor a Map, because I want it to be POJO.

What can I do here to make this work?

This is Spring-boot 1.3.3 BTW.

Upvotes: 1

Views: 2429

Answers (1)

sjngm
sjngm

Reputation: 12861

Well, it seems as if I somehow hit a corner-case where Spring doesn't do much about it. The main problem is that Spring seems to collect the available bean structure including their nested field structure before it knows of (or at least makes use of) the Converters lying around in the system.

I let the class with @ConfigurationProperties implement ApplicationContextAware and the new method

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    AnnotationConfigApplicationContext context = (AnnotationConfigApplicationContext) applicationContext;

    @SuppressWarnings("unchecked")
    Converter<String, AbstractLocatedConfig> locatedConfigSupport = context.getBean("locatedConfigConverter", Converter.class);

    :
  }

then also looked for all properties in the context's environment that would trigger the conversion process, manually called the conversion and created the bean structure that way.

For some reason the following lifecycle-stuff of Spring caused not all properties to end up in the bean, which made me do this:

@Configuration
public class SampleConfiguration {

  @Autowired
  private Environment environment;

  @Autowired
  private ClassWithTheConfigurationPropertiesAbove theBeanWithTheConfigurationPropertiesAbove;

  @PostConstruct
  void postConstruct() throws Exception {
    if (environment instanceof AbstractEnvironment) {
      MutablePropertySources sources = ((AbstractEnvironment) environment).getPropertySources();
      // This is a MUST since Spring calls the nested properties handler BEFORE
      // calling the conversion service on that field. Therefore, our converter
      // for AbstractLocatedConfigs is called too late the first time. A second
      // call will fill in the fields in the new objects and set the other ones
      // again, too.
      // See org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(String, Class<T>, boolean)
      // Note: in case Spring reorders this, the logic here won't be needed.
      setProperties(theBeanWithTheConfigurationPropertiesAbove, sources);
    } else {
      throw new IllegalArgumentException("The environment must be an " + AbstractEnvironment.class.getSimpleName());
    }
  }

  void setProperties(Object target, MutablePropertySources propertySources) {
    // org.springframework.boot.bind.PropertiesConfigurationFactory.doBindPropertiesToTarget()
    // was the base for this. Go there for further logic if needed.
    RelaxedDataBinder dataBinder = new RelaxedDataBinder(target);
    dataBinder.bind(new MutablePropertyValues(getProperties(propertySources)));
  }

  public String getProperty(String propertyName) {
    return environment.getProperty(propertyName);
  }

  private Map<String, String> getProperties(MutablePropertySources propertySources) {
    Iterable<PropertySource<?>> iterable = () -> propertySources.iterator();
    return StreamSupport.stream(iterable.spliterator(), false)
        .map(propertySource -> {
          Object source = propertySource.getSource();
          if (source instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, String> sourceMap = (Map<String, String>) source;
            return sourceMap.keySet();
          } else if (propertySource instanceof SimpleCommandLinePropertySource) {
            return Arrays.asList(((SimpleCommandLinePropertySource) propertySource).getPropertyNames());
          } else if (propertySource instanceof RandomValuePropertySource) {
            return null;
          } else {
            throw new NotImplementedException("unknown property source " + propertySource.getClass().getName() + " or its source " + source.getClass().getName());
          }
        })
        .filter(Objects::nonNull)
        .flatMap(Collection::stream)
        .collect(Collectors.toMap(Function.identity(), this::getProperty));
  }

}

It would be nice if Spring could do something about this to make it easier...

Upvotes: 1

Related Questions