Uploaded image for project: 'Spring Framework'
  1. Spring Framework
  2. SPR-11759

@Value annotation should be able to inject List<String> from YAML properties

    Details

    • Type: Improvement
    • Status: Open
    • Priority: Minor
    • Resolution: Unresolved
    • Affects Version/s: None
    • Fix Version/s: Waiting for Triage
    • Component/s: Core
    • Labels:
      None

      Description

      Yaml file

       
      foobar:  
        ignoredUserIds:
          - 57016311
          - 22588218

      Class

       
          
      public class Foobar {
          @Value("${foobar.ignoredUserIds}")
          List<String> ignoredUserIds;
      }
       

      Error

       
      Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: java.util.List foobar.Foobar.ignoredUserIds; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'foobar.ignoredUserIds' in string value "${foobar.ignoredUserIds}"
       

        Activity

        Hide
        cooniur Tongliang Liu added a comment - - edited

        I think at least "one-level" array should be supported.

        By "one-level", I mean each item in the array is of a simple type (integer, boolean, or string) and all items are of the same type in the array, for example:

            oneLevelIntegerArray:
              - 1
              - 2
              - 3
            oneLevelStringArray:
              - foo
              - bar

        Therefore, in the code, one can use the following code to bind the above YAML arrays to a List:

            @Value("${oneLevelIntegerArray}")
            List<Integer> intArray;
         
            @Value("${oneLevelStringArray}")
            List<String> strArray;

        But in order to bind more complex array structures, i.e. items in the array are of compound types (arrays, maps, etc..), one should use @ConfigurationProperties.

        If this is acceptable, I'll create a pull request to make this happen. It is fairly easy to make this happen.

        Show
        cooniur Tongliang Liu added a comment - - edited I think at least "one-level" array should be supported. By "one-level", I mean each item in the array is of a simple type (integer, boolean, or string) and all items are of the same type in the array, for example: oneLevelIntegerArray: - 1 - 2 - 3 oneLevelStringArray: - foo - bar Therefore, in the code, one can use the following code to bind the above YAML arrays to a List : @Value("${oneLevelIntegerArray}") List<Integer> intArray;   @Value("${oneLevelStringArray}") List<String> strArray; But in order to bind more complex array structures, i.e. items in the array are of compound types (arrays, maps, etc..), one should use @ConfigurationProperties . If this is acceptable, I'll create a pull request to make this happen. It is fairly easy to make this happen.
        Hide
        dgomesbr Diego Magalhaes added a comment -

        Hey guys,

        Just encountered that and got help in the gitter spring-boot channel that led me here.

        Sample code:

        @Data
        @RefreshScope
        @ConfigurationProperties(prefix = "api")
        @Configuration
        public class APIProperties {
            private Config config;
         
            @Data
            public static class Config {
                private Blocked blocked;
         
                @Data
                public static class Blocked {
                    List<String> blockedExtensions = new ArrayList<>();
                }
            }
        }
        

        Looking at :8080/env shows that ConfigurationProperties worked as expected:

        applicationConfig: [classpath:/application.yml]: {
            api.config.blocked.extensions[0]: ".exe",
            api.config.blocked.extensions[1]: ".dll",
            api.config.blocked.extensions[2]: ".bin",
            api.config.blocked.extensions[3]: ".dat",
            api.config.blocked.extensions[4]: ".osx"
        }
        

        now the controler:

        @RestController
        public class SampleServices{
         
            private APIProperties props;
         
            @Autowired
            public ShorteningService(@Qualifier("APIProperties") APIProperties apiProperties) {
                this.props = apiProperties;
            }
         
            @RequestMapping("test")
            Callable<String> test() {
                return () -> props.getConfig().getBlocked().getBlockedExtensions().toString();
            }
        }
        

        Calling /test returns "[ ]", an empty list.

        So to verify that @Value won't work as well, let's change it for:

        @RestController
        public class ShorteningService {
            @Value("${api.config.blocked.extensions}")
            List<String> blockedExtensions;
         
            @RequestMapping("test")
            Callable<String> test() {
                return () -> blockedExtensions.toString();
            }
        }
        

        Error:

        Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'api.config.blocked.extensions' in string value "${api.config.blocked.extensions}"
        	at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:174)
        	at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
        	at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:204)
        	at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:178)
        	at org.springframework.context.support.PropertySourcesPlaceholderConfigurer$2.resolveStringValue(PropertySourcesPlaceholderConfigurer.java:175)
        	at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:807)
        	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:980)
        	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:967)
        	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:543)
        

        Changing the controller code to:

        @RestController
        public class ShorteningService {
            @Value("${api.config.blocked.extensions[0]}")
            String blockedExtensions;
         
            @RequestMapping("test")
            Callable<String> test() {
                return () -> blockedExtensions;
            }
        }
        

        Works like charm, for the .yaml bellow, the result is ".exe"

        api:
          config:
            blocked:
              extensions:
              - .exe
              - .dll
              - .bin
              - .dat
              - .osx
        

        Show
        dgomesbr Diego Magalhaes added a comment - Hey guys, Just encountered that and got help in the gitter spring-boot channel that led me here. Sample code: @Data @RefreshScope @ConfigurationProperties (prefix = "api" ) @Configuration public class APIProperties { private Config config;   @Data public static class Config { private Blocked blocked;   @Data public static class Blocked { List<String> blockedExtensions = new ArrayList<>(); } } } Looking at :8080/env shows that ConfigurationProperties worked as expected: applicationConfig: [classpath:/application.yml]: { api.config.blocked.extensions[0]: ".exe", api.config.blocked.extensions[1]: ".dll", api.config.blocked.extensions[2]: ".bin", api.config.blocked.extensions[3]: ".dat", api.config.blocked.extensions[4]: ".osx" } now the controler: @RestController public class SampleServices{   private APIProperties props;   @Autowired public ShorteningService( @Qualifier ( "APIProperties" ) APIProperties apiProperties) { this .props = apiProperties; }   @RequestMapping ( "test" ) Callable<String> test() { return () -> props.getConfig().getBlocked().getBlockedExtensions().toString(); } } Calling /test returns "[ ]", an empty list. So to verify that @Value won't work as well, let's change it for: @RestController public class ShorteningService { @Value ( "${api.config.blocked.extensions}" ) List<String> blockedExtensions;   @RequestMapping ( "test" ) Callable<String> test() { return () -> blockedExtensions.toString(); } } Error: Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'api.config.blocked.extensions' in string value "${api.config.blocked.extensions}" at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:174) at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126) at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:204) at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:178) at org.springframework.context.support.PropertySourcesPlaceholderConfigurer$2.resolveStringValue(PropertySourcesPlaceholderConfigurer.java:175) at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:807) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:980) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:967) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:543) Changing the controller code to: @RestController public class ShorteningService { @Value ( "${api.config.blocked.extensions[0]}" ) String blockedExtensions;   @RequestMapping ( "test" ) Callable<String> test() { return () -> blockedExtensions; } } Works like charm, for the .yaml bellow, the result is ".exe" api: config: blocked: extensions: - .exe - .dll - .bin - .dat - .osx
        Hide
        snicoll St├ęphane Nicoll added a comment -

        Yeah well, your code there reproduces the limitation described in this issue. And this issue is not fixed so what you're experiencing is pretty much the current state yeah.

        You are using Spring Boot so please do you a favour and stop using @Value. @ConfigurationProperties does a lot more and does support that use case.

        Show
        snicoll St├ęphane Nicoll added a comment - Yeah well, your code there reproduces the limitation described in this issue. And this issue is not fixed so what you're experiencing is pretty much the current state yeah. You are using Spring Boot so please do you a favour and stop using @Value . @ConfigurationProperties does a lot more and does support that use case.
        Hide
        wenjiezhang2013@gmail.com Wenjie Zhang added a comment - - edited

        I encountered this problem when I was using spring framework to load yaml configuration:

        @Bean
        public static PropertySourcesPlaceholderConfigurer properties() {
            final PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
            final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
            yaml.setResources(new ClassPathResource("application.yml"));
            propertySourcesPlaceholderConfigurer.setProperties(yaml.getObject());
            return propertySourcesPlaceholderConfigurer;
        }
         
        //The property is 
        @Value("${platforms}")
        private List<String> platforms;
        

        I can see the properties get loaded into the PropertySources object like platforms[0], platforms[1].

        However, I still get following exception:

        Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'platforms' in string value "${platforms}"
        	at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:174)
        	at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
        

        Show
        wenjiezhang2013@gmail.com Wenjie Zhang added a comment - - edited I encountered this problem when I was using spring framework to load yaml configuration: @Bean public static PropertySourcesPlaceholderConfigurer properties() { final PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer(); final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); yaml.setResources( new ClassPathResource( "application.yml" )); propertySourcesPlaceholderConfigurer.setProperties(yaml.getObject()); return propertySourcesPlaceholderConfigurer; }   //The property is @Value ( "${platforms}" ) private List<String> platforms; I can see the properties get loaded into the PropertySources object like platforms [0] , platforms [1] . However, I still get following exception: Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'platforms' in string value "${platforms}" at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java: 174 ) at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java: 126 )
        Hide
        wenjiezhang2013@gmail.com Wenjie Zhang added a comment -

        Do you guys think making the @Value to support regular expression is a good idea to resolve this bug?

        Show
        wenjiezhang2013@gmail.com Wenjie Zhang added a comment - Do you guys think making the @Value to support regular expression is a good idea to resolve this bug?

          People

          • Assignee:
            juergen.hoeller Juergen Hoeller
            Reporter:
            aberlin@pivotallabs.com Adam Berlin
            Last updater:
            Wenjie Zhang
          • Votes:
            17 Vote for this issue
            Watchers:
            17 Start watching this issue

            Dates

            • Created:
              Updated:
              Days since last comment:
              1 year, 15 weeks, 2 days ago