Issue
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.
Solution
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 needMatcher.quoteReplacement(…)
. - Second, we can use
StringBuilder
instead ofStringBuffer
which might also be more efficient. The Java 9 solution usesStringBuilder
under the hood, as support for it has been added toappendReplacement
/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
.
Answered By - Holger
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)