Spring to understand properties in YAML
Did Spring abandon YAML to use as an alternative to .properties / .xml because of:
[Spring Developer]: ...YAML was considered, but we thought that counting whitespace significant was a support nightmare in the making... [reference from spring forum]
I am confident YAML makes a lot of sense for properties, and I am u开发者_如何学JAVAsing it currently on the project, but have difficulties to inject properties in a
<property name="productName" value="${client.product.name}" />
fashion.
Anything I am missing, or I should create a custom YamlPropertyPlaceholderConfigurer ?
I don't know if this is a bit too late but no - you don't have to implement whole YamlPropertyPlaceholderConfigurer instead you can simply create custom PropertiesPersister and add it as optional parameter.
Here's how your configuration will look
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>file:///C:/somewhere/site.yaml</value>
</property>
<property name="propertiesPersister" ref="persister"></property>
</bean>
<bean id="persister" class="com.foo.utils.YamlPropertiesPersister"></bean>
And here's bare-bone (read-only) implementation using SnakeYaml, feel free to add what you need including error handling
public class YamlPropertiesPersister implements PropertiesPersister {
@Override
public void load(Properties props, InputStream is) throws IOException {
load(props, new InputStreamReader(is));
}
/**
* We want to traverse map representing Yaml object and each time we find String=String pair we want to
* save it as Property. As we are going deeper into map we generate compound key as path-like String
*
* @param props
* @param reader
* @throws IOException
* @see org.springframework.util.PropertiesPersister#load(java.util.Properties, java.io.Reader)
*/
@Override
public void load(Properties props, Reader reader) throws IOException {
Yaml yaml = CollectorUtils.instanceOfYaml();
Map<String, Object> map = (Map<String, Object>) yaml.load(reader);
// now we can populate supplied props
assignProperties(props, map, null);
}
/**
* @param props
* @param map
*/
public void assignProperties(Properties props, Map<String, Object> map, String path) {
for (Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
if (StringUtils.isNotEmpty(path))
key = path + "." + key;
Object val = entry.getValue();
if (val instanceof String) {
// see if we need to create a compound key
props.put(key, val);
} else if (val instanceof Map) {
assignProperties(props, (Map<String, Object>) val, key);
}
}
}
@Override
public void store(Properties props, OutputStream os, String header) throws IOException {
throw new NotImplementedException("Current implementation is a read-only");
}
@Override
public void store(Properties props, Writer writer, String header) throws IOException {
throw new NotImplementedException("Current implementation is a read-only");
}
@Override
public void loadFromXml(Properties props, InputStream is) throws IOException {
throw new NotImplementedException("Use DefaultPropertiesPersister if you want to read/write XML");
}
@Override
public void storeToXml(Properties props, OutputStream os, String header) throws IOException {
throw new NotImplementedException("Use DefaultPropertiesPersister if you want to load/store to XML");
}
@Override
public void storeToXml(Properties props, OutputStream os, String header, String encoding) throws IOException {
throw new NotImplementedException("Use DefaultPropertiesPersister if you want to read/write XML");
}
}
As added benefit - here's how I create Yaml instance
public static Yaml instanceOfYaml() {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setDefaultScalarStyle(ScalarStyle.DOUBLE_QUOTED);
final Yaml yaml = new Yaml(new Loader(), new Dumper(options), new Resolver() {
/**
* @see org.yaml.snakeyaml.resolver.Resolver#addImplicitResolvers()
*/
@Override
protected void addImplicitResolvers() {
addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO");
// disable resolving of floats and integers
// addImplicitResolver(Tags.FLOAT, FLOAT, "-+0123456789.");
// addImplicitResolver(Tag.INT, INT, "-+0123456789");
addImplicitResolver(Tag.MERGE, MERGE, "<");
addImplicitResolver(Tag.NULL, NULL, "~nN\0");
addImplicitResolver(Tag.NULL, EMPTY, null);
addImplicitResolver(Tag.TIMESTAMP, TIMESTAMP, "0123456789");
addImplicitResolver(Tag.VALUE, VALUE, "=");
}
});
return yaml;
}
You can also read this in my blog
For those using Spring 3.1, you can register a Yaml PropetySource. The SnakeYaml code is from the Bostone code (thanks) adapted to new PropertySource system of Spring 3.1.
import com.google.common.base.Preconditions;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;
import org.yaml.snakeyaml.Dumper;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
import org.yaml.snakeyaml.Loader;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.resolver.Resolver;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
/**
* @author Sebastien Lorber <i>(lorber.sebastien@gmail.com)</i>
*/
public class YamlPropertiesSource extends PropertiesPropertySource {
public YamlPropertiesSource(String name, Resource yamlResource) {
super(name, getPropertySource(yamlResource) );
}
private static Properties getPropertySource(Resource yamlResource) {
Preconditions.checkArgument(yamlResource != null,"no yaml resource provided");
try {
InputStream is = yamlResource.getInputStream();
Properties properties = new Properties();
load(properties, is);
return properties;
} catch ( Exception e ) {
throw new IllegalStateException("Can't get PropertySource from YAML resource=" + yamlResource,e);
}
}
private static void load(Properties props, InputStream is) throws IOException {
load(props, new InputStreamReader(is));
}
private static void load(Properties props, Reader reader) throws IOException {
Yaml yaml = instanceOfYaml();
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) yaml.load(reader);
// now we can populate supplied props
assignProperties(props, map, null);
}
private static void assignProperties(Properties props, Map<String, Object> map, String path) {
for (Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
if ( StringUtils.hasLength(path) )
key = path + "." + key;
Object val = entry.getValue();
if (val instanceof String) {
// see if we need to create a compound key
props.put(key, val);
} else if (val instanceof Map) {
assignProperties(props, (Map<String, Object>) val, key);
}
}
}
public static Yaml instanceOfYaml() {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setDefaultScalarStyle(ScalarStyle.DOUBLE_QUOTED);
final Yaml yaml = new Yaml(new Loader(), new Dumper(options), new Resolver() {
/**
* @see org.yaml.snakeyaml.resolver.Resolver#addImplicitResolvers()
*/
@Override
protected void addImplicitResolvers() {
addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO");
// disable resolving of floats and integers
// addImplicitResolver(Tags.FLOAT, FLOAT, "-+0123456789.");
// addImplicitResolver(Tag.INT, INT, "-+0123456789");
addImplicitResolver(Tag.MERGE, MERGE, "<");
addImplicitResolver(Tag.NULL, NULL, "~nN\0");
addImplicitResolver(Tag.NULL, EMPTY, null);
addImplicitResolver(Tag.TIMESTAMP, TIMESTAMP, "0123456789");
addImplicitResolver(Tag.VALUE, VALUE, "=");
}
});
return yaml;
}
}
Please notice that this is also inspired by ResourcePropertySource, and it loads properties in ISO 8859-1 charset. I opened a bug for that: SPR-10096
You can add this property source to your application context. This can also be done in your unit tests:
public class PropertySourceContextLoader extends GenericXmlContextLoader {
@Override
protected void loadBeanDefinitions(GenericApplicationContext context,MergedContextConfiguration mergedConfig) {
PropertySource<String> ps = new MyPropertySource();
context.getEnvironment().getPropertySources().addLast(ps);
super.loadBeanDefinitions(context, mergedConfig);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = PropertySourceContextLoader.class, locations = { "classpath:/spring-application-context.xml" })
public class SpringBasedTest {
..........
}
For complete and utter nubs like me who have zero knowledge of what the author is actually doing, but need to do it anyway...here's how I made it work. Have no idea how to de-deprecate the instanceOfYaml() though. One other thing, my Spring Boot Eclipse project read from files marked .yml, not .yaml
import org.springframework.util.PropertiesPersister;
import org.springframework.util.StringUtils;
import org.yaml.snakeyaml.Dumper;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
import org.yaml.snakeyaml.Loader;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.resolver.Resolver;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
public class YamlPropertiesPersister implements PropertiesPersister {
@Override
public void load(Properties props, InputStream is) throws IOException {
load(props, new InputStreamReader(is));
}
/**
* We want to traverse map representing Yaml object and each time we will find String:String value pair we want to
* save it as Property. As we are going deeper into map we generate a compound key as path-like String
*
* @param props
* @param reader
* @throws IOException
* @see org.springframework.util.PropertiesPersister#load(java.util.Properties, java.io.Reader)
*/
@Override
public void load(Properties props, Reader reader) throws IOException {
Yaml yaml = instanceOfYaml();
Map<String, Object> map = (Map<String, Object>) yaml.load(reader);
// now we can populate supplied props
assignProperties(props, map, null);
}
/**
* @param props
* @param map
*/
public void assignProperties(Properties props, Map<String, Object> map, String path) {
for (Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
if (!StringUtils.isEmpty(path))
key = path + "." + key;
Object val = entry.getValue();
if (val instanceof String) {
// see if we need to create a compound key
props.put(key, val);
} else if (val instanceof Map) {
assignProperties(props, (Map<String, Object>) val, key);
}
}
}
@Override
public void store(Properties props, OutputStream os, String header) throws IOException {
throw new UnsupportedOperationException("Current implementation is a read-only");
}
@Override
public void store(Properties props, Writer writer, String header) throws IOException {
throw new UnsupportedOperationException("Current implementation is a read-only");
}
@Override
public void loadFromXml(Properties props, InputStream is) throws IOException {
throw new UnsupportedOperationException("Use DefaultPropertiesPersister if you want to read/write XML");
}
@Override
public void storeToXml(Properties props, OutputStream os, String header) throws IOException {
throw new UnsupportedOperationException("Use DefaultPropertiesPersister if you want to load/store to XML");
}
@Override
public void storeToXml(Properties props, OutputStream os, String header, String encoding) throws IOException {
throw new UnsupportedOperationException("Use DefaultPropertiesPersister if you want to read/write XML");
}
public static Yaml instanceOfYaml() {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setDefaultScalarStyle(ScalarStyle.DOUBLE_QUOTED);
final Yaml yaml = new Yaml(new Loader(), new Dumper(options), new Resolver() {
/**
* @see org.yaml.snakeyaml.resolver.Resolver#addImplicitResolvers()
*/
@Override
protected void addImplicitResolvers() {
addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO");
// disable resolving of floats and integers
// addImplicitResolver(Tags.FLOAT, FLOAT, "-+0123456789.");
// addImplicitResolver(Tag.INT, INT, "-+0123456789");
addImplicitResolver(Tag.MERGE, MERGE, "<");
addImplicitResolver(Tag.NULL, NULL, "~nN\0");
addImplicitResolver(Tag.NULL, EMPTY, null);
addImplicitResolver(Tag.TIMESTAMP, TIMESTAMP, "0123456789");
addImplicitResolver(Tag.VALUE, VALUE, "=");
}
});
return yaml;
}
}
The forum post you reference was from the dmServer forum, rather than the Spring Framework, and there's very little relation between the two, so I wouldn't read anything into it.
On top of that, YAML is pretty much unheard of in the Java world, so adding support for it would have been a token gesture (if you'll pardon the expression) at best. XML dominates in Java, especially server-side, so there's little use in swimming against the tide there, especially for a minority format like YAML.
Having said that, writing your own YamlPropertyPlaceholderConfigurer
should be easy enough, assuming you can find a YAML parser for Java.
精彩评论