我正在开发一个应用程序,它应该是高度可配置的。
其目标是有一个XML文件来存储其配置。在配置中可以定义 "变量 "元素,这些元素可以在整个配置中重复使用。
下面是一个例子。
<var name = "QUEUE_PREFIX" value = "TEST/QUEUE/PREFIX" />
<var name = "IN_QUEUE-NAME" value = "${QUEUE_PREFIX}/IN" />
<var name = "OUT_QUEUE-NAME" value = "${QUEUE_PREFIX}/OUT" />
<mq-client IN-QUEUE = "${IN_QUEUE_NAME}"
OUT-QUEUE = "${OUT_QUEUE_NAME}"/>
它的结果应该是
<var name = "QUEUE_PREFIX" value = "TEST/QUEUE/PREFIX" />
<var name = "IN_QUEUE" value = "TEST/QUEUE/PREFIX/IN" />
<var name = "OUT_QUEUE" value = "TEST/QUEUE/PREFIX/OUT" />
<mq-client IN-QUEUE = "TEST/QUEUE/PREFIX/IN"
OUT-QUEUE = "TEST/QUEUE/PREFIX/OUT"/>
这种替换是很容易的,在我的原型中已经可以正常工作了。一旦有一整个数组和多个 "层 "的变量被引用,就会变得很困难。比如一个变量引用一个变量,而这个变量也已经引用了一个变量。
比如说,一个变量引用一个变量,而这个变量也已经引用了一个变量。
<var name = "USER_NAME" value = "TESTUSER" />
<var name = "USER_HOME" value = "C:\USERS\${USER_NAME}" />
<var name = "TEST_DIR" value = "${USER_HOME}/IN" />
<var name = "TEST" value = "${TEST_DIR}/${PID}" />
在这种情况下,应用程序必须决定先替换这些变量中的哪一个 为了不影响其他变量的运行,就必须先解决这个问题。
当然还有其他的问题,比如,如果两个变量相互引用,我们该怎么办?
我的问题
有人做过类似的事情吗,你是怎么解决的?有没有一个框架、库或者其他的东西能够解决这种变量的配置?
如果你是在Spring环境中工作,你可以将你的问题委托给现有的PropertyResolvers。如何在Spring中解决属性占位符
很久以前我就遇到过和你描述的同样的问题,不得不在没有Spring的情况下解决。正如你已经认识到的,主要的困难是递归和循环依赖。所以代码有点长,但15年后仍然可以使用。尽管如此,我还是把它提高到了Java 8的水平。
这是主类。
package config;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ConfigResolver {
private static final String CYCLE_MARKER = "#CYCLE?";
private static final String PARAM_START = "${";
private static final String PARAM_END = "}";
/**
* Creates a now config map with all references resolved.
*
* @throws IllegalStateException
* In case of undefined or circular references.
*/
public Map<String, String> resolve(Map<String, String> config) throws IllegalStateException {
final Map<String, String> result = new LinkedHashMap<>();
config.keySet().stream().forEach(key -> resolve(key, config, result));
return result;
}
/**
* Copies the given key and its value from the source map into the target map.
* If there are references to other keys, those will be resolved recursively and
* copied as well. References to System properties are also valid. If the value
* is already present, nothing will happen.
*
* @throws IllegalStateException
* In case of undefined or circular references.
*/
private String resolve(String key, Map<String, String> source, Map<String, String> target)
throws IllegalStateException {
String value = target.get(key);
if (value == CYCLE_MARKER) {
throw new IllegalStateException("Circular reference for key:" + key);
}
if (value != null) {
return value;
}
value = source.get(key);
if (value == null) {
return System.getProperty(key);
}
target.put(key, CYCLE_MARKER);
final List<Parameter> params = parseParams(value);
if (!params.isEmpty()) {
final StringBuilder resolvedValue = new StringBuilder(value);
int deviation = 0;
for (Parameter p : params) {
final String v = resolve(p.getName(), source, target);
if (v == null) {
throw new IllegalStateException("Undefined parameter: " + p.getName());
}
resolvedValue.replace(p.getStart() + deviation, p.getEnd() + deviation, v);
deviation += v.length() - p.getEnd() + p.getStart();
}
value = resolvedValue.toString();
}
target.put(key, value);
return value;
}
/**
* Extracts all parameters from the given String
*/
private List<Parameter> parseParams(String value) {
final List<Parameter> result = new ArrayList<Parameter>();
int start = 0;
int end = 0;
while (start >= 0) {
start = value.indexOf(PARAM_START, end);
end = value.indexOf(PARAM_END, start) + PARAM_END.length();
if (start >= 0 && end > start + 1) {
final String name = value.substring(start + PARAM_START.length(), end - PARAM_END.length());
result.add(new Parameter(name, start, end));
}
}
return result;
}
/**
* Parameter with position in String
*/
private static class Parameter {
private final String name;
private final int start;
private final int end;
Parameter(String name, int start, int end) {
this.start = start;
this.end = end;
this.name = name;
}
public String getName() {
return name;
}
public int getStart() {
return start;
}
public int getEnd() {
return end;
}
}
}
这是一个JUnit测试,涵盖了你的例子。
package config;
import static org.junit.Assert.assertEquals;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
public class ConfigResolverTest {
private ConfigResolver sut = new ConfigResolver();
@Test
public void testResolve() {
System.setProperty("PID", "1234");
final Map<String, String> raw = new LinkedHashMap<>();
raw.put("USER_NAME", "TESTUSER");
raw.put("USER_HOME", "C:/USERS/${USER_NAME}");
raw.put("TEST_DIR", "${USER_HOME}/IN");
raw.put("TEST", "${TEST_DIR}/${PID}");
final Map<String, String> resolved = sut.resolve(raw);
assertEquals("C:/USERS/TESTUSER/IN/1234", resolved.get("TEST"));
assertEquals(4, resolved.size());
}
@Test(expected = IllegalStateException.class)
public void testResolve_Circular() {
final Map<String, String> raw = new LinkedHashMap<>();
raw.put("first", "${second}");
raw.put("second", "${third}");
raw.put("third", "${first}");
sut.resolve(raw);
}
@Test(expected = IllegalStateException.class)
public void testResolve_Undefined() {
final Map<String, String> raw = new LinkedHashMap<>();
raw.put("first", "${second}");
sut.resolve(raw);
}
}
你可以试试Apache commons-configuration
来自 其例
application.name = Killer App
application.version = 1.6.2
application.title = ${application.name} ${application.version}
内插的字符串是 application.title = Killer App 1.6.2
也许像这样简单。
Map<String,String> vars = new HashMap<>();
vars.put("USER_NAME", "TESTUSER");
vars.put("USER_HOME", "C:/USERS/${USER_NAME}");
vars.put("TEST_DIR", "${USER_HOME}/IN");
vars.put("TEST", "${TEST_DIR}/${PID}");
如果没有带有引用的项,那么你就完成了。
否则,检查所有有引用的项,然后用所有可能的字元替换变量。将带引用的值用替换的方法替换,并更新Map。
进入步骤1。
从某种程度上来说,这就是广度先搜索,通过做替换来逐级传播,直到到达终点。