30

For my Spring Boot application, I am trying to use an environment variable that holds the list of properties.topics in application.yml (see configuration below).

properties:
      topics:
        - topic-01
        - topic-02
        - topic-03

I use the configuration file to populate properties bean (see this spring documentation), as shown below

import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("properties")
public class ApplicationProperties {
  private List<String> topics = new ArrayList<>();
  public void setTopics(List<String> topics) {
     this.topics = topics;
  }
  public List<String> getTopics() {
     return this.topics;
  }
}

With the use of environment variable, I can have the list's content change without changing the application.yml. However, all examples that I could find so far only for cases where an environment variable holding only single value, not a collection of values in my case.

Edit:

To make it clear after @vancleff's comment, I do not need the values of the environment variable to be saved to application.yml.

Another edit:

I think by oversimplifying my question, I shoot myself in the foot. @LppEdd answer works well with the example given in my question. However, what happens if instead of a collection of simple string topic names, I need a bit more complex structure. For example, something like

properties:
  topics:
    - 
      name: topic-01
      id: id-1
    - 
      name: topic-02
      id: id-2
    - 
      name: topic-03
      id: id-3
Phuong Hoang
  • 461
  • 1
  • 6
  • 12
  • I found an example for you, here is the link : https://stackoverflow.com/questions/26699385/spring-boot-yaml-configuration-for-a-list-of-strings – Vihar Manchala Mar 11 '19 at 16:50
  • @ViharManchala The question that you gave me deals with importing the configuration to the java class. I got that part working. What I want to do is to have an environment variable that specify the values of a collection instead of hard-coding the list values in the `application.yml` file. This gives me the freedom to change the list values without changing the content of `application.yml` file. – Phuong Hoang Mar 11 '19 at 16:59
  • Do you wish to re-write the updated value to the `application.yml` file back or just keep it in memory? – Van_Cleff Mar 11 '19 at 17:08
  • @vancleff I just need to keep the update value in memory, no need to update `application.yml`. – Phuong Hoang Mar 11 '19 at 19:37

5 Answers5

50

a bit late for the show but, I was facing the same problem and this solves it

https://github.com/spring-projects/spring-boot/wiki/Relaxed-Binding-2.0#lists-1

MY_FOO_1_ = my.foo[1]

MY_FOO_1_BAR = my.foo[1].bar

MY_FOO_1_2_ = my.foo[1][2]`

So, for the example in the question:

properties:
  topics:
    - 
      name: topic-01
      id: id-1
    - 
      name: topic-02
      id: id-2
    - 
      name: topic-03
      id: id-3

The environment variables should look like this:

PROPERTIES_TOPICS_0_NAME=topic-01
PROPERTIES_TOPICS_0_ID=id-01
PROPERTIES_TOPICS_1_NAME=topic-02
PROPERTIES_TOPICS_1_ID=id-02
PROPERTIES_TOPICS_2_NAME=topic-03
PROPERTIES_TOPICS_2_ID=id-03
Corgan
  • 669
  • 8
  • 11
  • Can you provide a bit more about what your answer does to solve the problem (aside from posting an external link?) – J. Murray Oct 01 '19 at 14:07
  • 5
    Thanks! This actually answers the question unlike the accepted answer! I want to add one **caution**: as soon as you override some property of object-item in the list this way, you'll actually override all the list, not just a one property of one list item. So, the all list will be created from scratch, ignoring any bindings in original `yaml` configuration file. So, you can't just override one small property. You are forced to set up **all** list items in environment variables. – Ruslan Stelmachenko Dec 02 '19 at 17:43
  • Another important addition :) Don't forget that index starts from 0. So if you wanna override this yaml list `my.foo: [one, two, three, four]` define this env-s: `MY_FOO_0=eins; MY_FOO_1=zwei; MY_FOO_2=drei` – fightlight Mar 19 '20 at 09:50
6

Suggestion, don't overcomplicate.

Say you want that list as an Environment variable. You'd set it using

-Dtopics=topic-01,topic-02,topic-03

You then can recover it using the injected Environment Bean, and create a new List<String> Bean

@Bean
@Qualifier("topics")
List<String> topics(final Environment environment) {
    final var topics = environment.getProperty("topics", "");
    return Arrays.asList(topics.split(","));
}

From now on, that List can be @Autowired.
You can also consider creating your custom qualifier annotation, maybe @Topics.

Then

@Service
class TopicService {
   @Topics
   @Autowired
   private List<String> topics;

   ...
}

Or even

@Service
class TopicService {
   private final List<String> topics;

   TopicService(@Topics final List<String> topics) {
      this.topics = topics;
   }

   ...
}

What you could do is use an externalized file.
Pass to the environment parameters the path to that file.

-DtopicsPath=C:/whatever/path/file.json

Than use the Environment Bean to recover that path. Read the file content and ask Jackson to deserialize it

You'd also need to create a simple Topic class

public class Topic {
    public String name;
    public String id;
}

Which represents an element of this JSON array

[
    {
        "name": "topic-1",
        "id": "id-1"
    },
    {
        "name": "topic-2",
        "id": "id-2"
    }
]

@Bean
List<Topic> topics(
        final Environment environment,
        final ObjectMapper objectMapper) throws IOException {
    // Get the file path
    final var topicsPath = environment.getProperty("topicsPath");

    if (topicsPath == null) {
        return Collections.emptyList();
    }

    // Read the file content
    final var json = Files.readString(Paths.get(topicsPath));

    // Convert the JSON to Java objects
    final var topics = objectMapper.readValue(json, Topic[].class);
    return Arrays.asList(topics);
}

enter image description here

LppEdd
  • 20,274
  • 11
  • 84
  • 139
  • I think by oversimplifying my question, I shoot myself in the foot. Your answer works well with the example given in my question. However, what happens if instead of a collection of simple string topic names, I need a bit more complex structure. For example, something like properties: topics: - name: topic-01 id: id-1 - name: topic-02 id: id-2 - name: topic-03 id: id-3 – Phuong Hoang Mar 11 '19 at 20:39
  • @PhuongHoang why would you want to set such a complex structure inside the environment variables? I mean, if you can set the environment variables you can also just point to a specific YAML file. You can recover that path, and deserialize the content (using Jackson) – LppEdd Mar 11 '19 at 20:41
  • @PhuongHoang let me know if I need to post an example using a file path. – LppEdd Mar 11 '19 at 20:50
  • I am not aware of using the environment containing file path. I am using this complex structure is because I am trying to do similar things like this (https://objectpartners.com/2018/07/31/configuring-kafka-topics-with-spring-kafka/). I also want to deploy the app to multiple environments, each with different set of topics and configuration options. I can resort to using multiple spring profiles, but if the values of the topics change, I have to change the `yml` files. This may not be something I have to do, but it's nice to know if it can be done so that changing the values is simple. – Phuong Hoang Mar 11 '19 at 20:58
  • it may be helpful if you can post an example with file path, if you don't mind. – Phuong Hoang Mar 11 '19 at 20:59
  • @PhuongHoang see updated answer. I tested it on a JSON document, but it's the same with YAML. – LppEdd Mar 11 '19 at 21:05
  • @PhuongHoang and by tested I mean that I just tried it live. – LppEdd Mar 11 '19 at 21:15
  • Thanks, I will modify your examples to fit my case. – Phuong Hoang Mar 11 '19 at 21:30
  • @PhuongHoang remember that Jackson has a separate module for YAML – LppEdd Mar 11 '19 at 21:32
0
Also facing the same issue , fixed with having a array in deployment.yaml from values.yml replacing the default values of application.yml

example as : 

deployment.yml -

----------------

env: 
            - name : SUBSCRIBTION_SITES_0_DATAPROVIDER
              value: {{ (index .Values.subscription.sites 0).dataprovider | quote }}
            - name: SUBSCRIBTION_SITES_0_NAME
              value: {{ (index .Values.subscription.sites 0).name | quote }}

values.yml -

---------------

  subscription:
    sites:
      - dataprovider: abc
        name: static

application.yml -

------------------

  subscription:
    sites:
      - dataprovider: ${SUBSCRIBTION_SITES_0_DATAPROVIDER:abcd}
        name: ${SUBSCRIBTION_SITES_0_NAME:static}
    
Java Code :

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "subscription")
public class DetailsProperties {

    private List<DetailsDto> sites;

    DetailsProperties() {
        this.sites = new ArrayList<>();
    }

}

Pojo mapped :

@Getter
@Setter
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DetailsDto {
    private String dataprovider;
    private String name;
}
Sandeep Jain
  • 1,019
  • 9
  • 13
0

Yet another way to solve this, it is uglier in the parsing but very easy and works. Replace:

properties:
  topics:
    - 
      name: topic-01
      id: id-1
    - 
      name: topic-02
      id: id-2
    - 
      name: topic-03
      id: id-3

With:

properties:
  topics: topic-01,topic-02,topic-03
  id: id-1,id-2,id-3

And read it like this:

private List<CombinedTopic> topicsCombined = new ArrayList<>();
public CombinedTopic(@Value("${properties.topics}") List<String> topics,
                     @Value("${properties.id}") List<String> ids) {
    List<CombinedTopic> collect = IntStream.range(0, ids.size())
        .mapToObj(i -> new CombinedTopic(ids.get(i), topics.get(i)))
            .toList();
}

Note that I assume that ID and Topic must go together and in the same order.

Andrey Dobrikov
  • 457
  • 4
  • 20
-1

I built a quick little utility to do this.

import java.util.LinkedList;
import java.util.List;

import org.springframework.core.env.Environment;

/**
 * Convenience methods for dealing with properties.
 */
public final class PropertyUtils {

  private PropertyUtils() {
  }

  public static List<String> getPropertyArray(Environment environment, String propertyName) {
    final List<String> arrayPropertyAsList = new LinkedList<>();
    int i = 0;
    String value;
    do {
      value = environment.getProperty(propertyName + "[" + i++ + "]");
      if (value != null) {
        arrayPropertyAsList.add(value);
      }
    } while (value != null);

    return arrayPropertyAsList;
  }

}

You could modify this without too many changes to support multiple fields as well. I've seen similar things done to load an array of database configurations from properties.

gaoagong
  • 1,177
  • 14
  • 15