Home > database >  Resolve environment variables of property file Java ResourceBundle
Resolve environment variables of property file Java ResourceBundle

Time:07-29

Our application is using java8 and spring. We are working to moving to kubernetes. For that reason, I want to use environment variables in the properties file like as follow and declare the -

conf.dir.path = ${APPLICATION_CONF_PATH}
database.name = ${APPLICATION_DB_SCHEMA}
save.file.path = ${COMMON_SAVE_PATH}${APPLICATION_SAVE_PATH}
# And many more keys

But right now the values are not resolved/expanded by environment variable.

Application initialization of property is as below -

public enum ApplicationResource {
    CONF_DIR_PATH("conf.dir.path"),
    DB_NAME("database.name")
    FILE_SAVE_PATH("save.file.path"),
    // And many more keys

    private final String value;

    ApplicationResource(String value) {
        this.value = value;
    }

    private static final String BUNDLE_NAME = "ApplicationResource";
    private static Properties props;

    static {
        try {
            Properties defaults = new Properties();
            initEnvironment(defaults, BUNDLE_NAME);
            props = new Properties(defaults);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    private static void initEnvironment(Properties props, String bundleName) throws Throwable {
        ResourceBundle rb = ResourceBundle.getBundle(bundleName);
        Enumeration<?> enu = rb.getKeys();
        String key = null;
        String value = null;
        while (enu.hasMoreElements()) {
            key = (String) enu.nextElement();
            value = rb.getString(key);
            props.setProperty(key, value);
        }
    }

    public String getString() {
        return props.getProperty(value);
    }

    public int getInt() throws NumberFormatException {
        String str = getString();
        if (str == null || str.length() == 0) {
            return 0;
        } else {
            return Integer.parseInt(str);
        }
    }
}

getString is used extensively. Right now when getString is called, it returns the literal string from the properties file. Is there any way to properly resolve environment variables without impacting the codebase?

Edit: By [without impacting the codebase], I meant only changing/editing code in the above enum/class file and the change being transparent in other areas.

CodePudding user response:

The simplest variant based on the Regex engine would be:

private static final Pattern VARIABLE = Pattern.compile("\\$\\{(.*?)\\}");
public String getString() {
    return VARIABLE.matcher(props.getProperty(value))
        .replaceAll(mr -> Matcher.quoteReplacement(System.getenv(mr.group(1))));
}

This replaces all occurrences of ${VAR} with the result of looking up System.getenv("VAR"). If the string contains no variable references, the original string is returned. It does, however, not handle absent variables. If you want to handle them (in a different way than failing with a runtime exception), you have to add the policy to the function.

E.g. the following code keeps variable references in their original form if the variable has not been found:

public String getString() {
    return VARIABLE.matcher(props.getProperty(value))
        .replaceAll(mr -> {
            String envVal = System.getenv(mr.group(1));
            return Matcher.quoteReplacement(envVal != null? envVal: mr.group());
        });
}

replaceAll(Function<MatchResult, String>) requires Java 9 or newer. For previous versions, you’d have to implement such a replacement loop yourself. E.g.

public String getString() {
    String string = props.getProperty(value);
    Matcher m = VARIABLE.matcher(string);
    if(!m.find()) return string;
    StringBuilder sb = new StringBuilder();
    int last = 0;
    do {
        String replacement = System.getenv(m.group(1));
        if(replacement != null) {
            sb.append(string, last, m.start()).append(replacement);
            last = m.end();
        }
    } while(m.find());
    return sb.append(string, last, string.length()).toString();
}

This variant does not use appendReplacement/appendTail which is normally used to build such loops, for two reasons.

  • First, it provides more control over how the replacement is inserted, i.e. by inserting it literally via append(replacement) we don’t need Matcher.quoteReplacement(…).
  • Second, we can use StringBuilder instead of StringBuffer which might also be more efficient. The Java 9 solution uses StringBuilder under the hood, as support for it has been added to appendReplacement/appendTail in this version too. But for previous versions, StringBuilder can only be used when implementing the logic manually.

Note that unlike the replaceAll variant, the case of absent variables can be handled simpler and more efficient with a manual replacement loop, as we can simply skip them.


You said you don’t want to change the initialization code, but I still recommend bringing it into a more idiomatic form, i.e.

private static void initEnvironment(Properties props, String bundleName) {
    ResourceBundle rb = ResourceBundle.getBundle(bundleName);
    for(Enumeration<String> enu = rb.getKeys(); enu.hasMoreElements(); ) {
        String key = enu.nextElement();
        String value = rb.getString(key);
        props.setProperty(key, value);
    }
}

In the end, it’s still doing the same. But iteration loops should be using for, to keep initialization expression, loop condition and fetching the next element as close as possible. Further, there is no reason to use Enumeration<?> with a type cast when you can use Enumeration<String> in the first place. And don’t declare variables outside the necessary scope. And there’s no reason to pre-initialize them with null.

CodePudding user response:

Spring support environment variable or system variable or application.property file if you able to use kubernates configmap its better choice. How to set environment variable dynamically in spring test

  • Related