YAML简介
YAML(YAML Ain't Markup Language,即YAML不是一种标记语言),简称yml,是一种直观的数据序列化格式,广泛用于配置文件,同时也被用于数据交换和存储。YAML的设计目标是易于人类阅读和编写,同时也易于机器解析和生成。它基于Unicode,并且支持多种编程语言。
YAML的主要特点
1)易于阅读:YAML使用缩进表示数据层次结构,使得它看起来很像自然语言的文档结构,易于人类理解和编写;
2)简洁性:YAML的数据结构通过少量的符号来表示,比如冒号“:”用于键值对,破折号“-”用于列表项,缩进表示层级。使得YAML文件通常比XML或JSON文件更简洁;
3)扩展性:YAML支持自定义数据类型,它可以在不修改核心语法的情况下扩展其用途;
4)跨语言支持:YAML被多种编程语言支持,包括Python、Ruby、Perl、Java等,在不同系统和应用之间交换数据变得容易;
5)注释:YAML支持注释,有助于说明文档的结构和内容,使得维护变得更加容易;
使用#符号开始,#后面的所有内容都将被视为注释,直到该行介绍。注释可以出现在行的开始、中间或末尾
SnakeYaml简介
SnakeYAML是用于Java的YAML解析器和发射器,是一个用于解析YAML、序列化以及反序列化的第三方框架。
1)解析YAML:SnakeYaml能够解析YAML格式的数据,将其转换为Java对象或Java中可操作的数据结构(如Map、List等);
2)序列化/反序列化Java对象:SnakeYAML支持将Java对象序列化为YAML格式的字符串,便于存储或传输,同时支持将YAML格式的字符串系列化为Java对象;
使用SnakeYaml解析Yaml的实现
以下以Dog、Cat的yaml文件解析为例,分享一下SnakeYaml解析Yaml的使用。
4.1 snakeyaml依赖添加
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
4.2 实体类
public interface Animal {
}
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
public class Cat implements Animal {
private String color;
private int age;
}
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
public class Dog implements Animal {
private String color;
private int age;
}
定义一个接口Animal,Dog和Cat实现了Animal接口。
4.3 yaml配置类
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class YamlTest1Configuration {
private Dog dog;
private Cat cat;
}
配置类中包含Dog和Cat对象。
4.4 test1.yml配置文件
dog:
color: black
age: 2
cat:
color: white
age: 1
4.5 yaml系列化为Java配置类
import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
public class Test1 {
public static void main(String[] args) {
Yaml yaml = new Yaml();
InputStream resource = Test1.class.getClassLoader().getResourceAsStream("test1.yml");
YamlTest1Configuration yamlTest1Configuration = yaml.loadAs(resource, YamlTest1Configuration.class);
System.out.println("cat : " + yamlTest1Configuration.getCat().toString());
System.out.println("dog : " + yamlTest1Configuration.getDog().toString());
}
}
4.6 输出结果
以上为SnakeYaml解析为Java对象的简单使用,其中YamlTest1Configuration的格式同yaml文件的配置格式一一对应,使用较为简单。除了以上使用以外,SnakeYaml还支持更加复杂的yaml文件解析,以下介绍一下SnakeYaml自定义标签的使用。
SnakeYaml自定义标签的实现及解析
5.1 简介
Yaml中直接定义标签(以 ! 符号自定义标签)并不是Yaml规范的一部分,而是由处理SnakeYaml来解释。在SnakeYaml框架中,可以在Java代码中定义标签,并通过Representer和Constructor类来告诉SnakeYaml如何序列化和反序列化带有这些标签的YAML节点。
1)Representer:用于定义如何将Java对象转换为Yaml表示。通过Representer注册一个Representer实例,并为其指定一个自定义标签;
2)Constructor:用于定义如何从Yaml节点创建Java对象。通过注册一个Constructor实例,再通过TypeDescription指定自定义标签及对应Java对象;
5.2 实现
使用的依赖和实体类同上面的示例。
5.2.1 yaml配置类
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Setter
@Getter
public class YamlTest2Configuration {
private List<Animal> animals;
}
此处的Java对象只解析获取Animal集合。
5.2.2 test2.yml配置文件
animals:
- !Dog # Dog标签
color: black
age: 2
- !Cat # Cat标签
color: white
age: 1
在配置文件中,通过 ! 符号,定义了Dog和Cat标签,用于指定标签下面的信息为对应的标签内容。
5.2.3 yaml系列化为Java配置类
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import java.io.InputStream;
import java.util.List;
public class Test2 {
public static void main(String[] args) {
// 定义一个Constructor构造器
Constructor constructor = new Constructor(YamlTest2Configuration.class);
// 定义一个TypeDescription,指定Cat类对应!Cat标签
TypeDescription typeDescription = new TypeDescription(Cat.class, "!Cat");
// 添加到构造器中
constructor.addTypeDescription(typeDescription);
constructor.addTypeDescription(new TypeDescription(Dog.class, "!Dog"));
Yaml yaml = new Yaml(constructor);
InputStream resource = Test2.class.getClassLoader().getResourceAsStream("test2.yml");
YamlTest2Configuration yamlTest2Configuration = yaml.loadAs(resource, YamlTest2Configuration.class);
List<Animal> animals = yamlTest2Configuration.getAnimals();
for (Animal a : animals) {
System.out.println(a.toString());
}
}
}
5.2.4 输出结果
SnakeYaml解析的原理
6.1 Yaml字符串信息解析成Node,每个Node包含Tag(标签)、value、起始位置、type等。每个键值对应一个NodeTuple,该对象包含keyNode和valueNode,分别表示键和值;
Node节点包含:
1)ScalarNode:标量节点。为叶子节点,即为确定的常量值。如keyNode,只能是字符串,一定为ScalarNode,其value为String类型;
2)CollectionNode:集合节点,抽象类,表示一组Node;
3)SequenceNode:有序集合节点,继承CollectionNode。对应的配置值为List集合的,其value为List<Node>;
4)MappingNode:键值对集合节点,继承CollectionNode。对应一组key、value的配置,其value为List<NodeTuple>;
6.2 解析的核心方法为Constructor.construct(Node node),执行如下:
1)在BaseConstructor.newInstance(Class<?> ancestor, Node node, boolean tryDefault)方法中,根据Node的type,从typeDescriptions集合中找到对应类型的TypeDescription对象;
type的设置主要以下两种:
a)在Constructor.constructJavaBean2ndStep()中通过通过Introspector(内省,类似反射技术)获得对象属性的Property,根据Property获取属性type,调用MappingNode.setTypes()或Node.setType()进行设置(解析时,Constructor.constructJavaBean2ndStep()方法层层递归调用);
b)BaseConstructor.constructObjectNoCheck(Node node)【获取节点的值】 -> Construct.construct() -> getConstructor() -> getClassForNode(),在该方法中,通过node.getTag()标签,从typeTags集合【通过constructor.addTypeDescription()添加TypeDescription时,会将type及标签添加到typeTags集合中】中获取标签对应的类,然后执行node.setType()设置节点的真实类型;
2)执行Constructor.ConstructMapping.constructJavaBean2ndStep(MappingNode node, Object object);
snakeymal-1.33版本的源码如下:
package org.yaml.snakeyaml.constructor;
public class Constructor extends SafeConstructor {
/**
* @param node:节点
* @param object:节点对应的对象。如YamlTest2Configuration对象
*/
protected Object constructJavaBean2ndStep(MappingNode node, Object object) {
// node.values的合并
flattenMapping(node, true);
// 如YamlTest2Configuration.class
Class<? extends Object> beanType = node.getType();
List<NodeTuple> nodeValue = node.getValue();
// 遍历value
for (NodeTuple tuple : nodeValue) {
// value为SequenceNode,集合中的Node为MappingNode
Node valueNode = tuple.getValueNode();
// flattenMapping enforces keys to be Strings
// 获取key,如animals
String key = (String) constructObject(tuple.getKeyNode());
try {
// 获取node对应的TypeDescription
TypeDescription memberDescription = typeDefinitions.get(beanType);
// 获取key对应的Property
Property property = memberDescription == null ? getProperty(beanType, key)
: memberDescription.getProperty(key);
if (!property.isWritable()) {
throw new YAMLException(
"No writable property '" + key + "' on class: " + beanType.getName());
}
// 设置value的type。如animals的type为List
valueNode.setType(property.getType());
final boolean typeDetected =
memberDescription != null && memberDescription.setupPropertyType(key, valueNode);
if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) {
// only if there is no explicit TypeDescription
// 获取实际参数类型,如Animal
Class<?>[] arguments = property.getActualTypeArguments();
if (arguments != null && arguments.length > 0) {
// type safe (generic) collection may contain the
// proper class
// 如:animals的valueNode为sequence,List集合参数的值
if (valueNode.getNodeId() == NodeId.sequence) {
// 获取第一个参数类型,如:Animal
Class<?> t = arguments[0];
// 强转
SequenceNode snode = (SequenceNode) valueNode;
// 设置集合的泛型
snode.setListType(t);
} else if (Map.class.isAssignableFrom(valueNode.getType())) {
// 如果是Map参数的值,则对应的valueNode为MappingNode,分别获取key和value的实际类型
Class<?> keyType = arguments[0];
Class<?> valueType = arguments[1];
MappingNode mnode = (MappingNode) valueNode;
mnode.setTypes(keyType, valueType);
mnode.setUseClassConstructor(true);
} else if (Collection.class.isAssignableFrom(valueNode.getType())) {
// 如果是Collection集合,则对应的valueNode为MappingNode
Class<?> t = arguments[0];
MappingNode mnode = (MappingNode) valueNode;
mnode.setOnlyKeyType(t);
mnode.setUseClassConstructor(true);
}
}
}
// 获取value的值
Object value =
(memberDescription != null) ? newInstance(memberDescription, key, valueNode)
: constructObject(valueNode);
// Correct when the property expects float but double was constructed
// 如果对应的value为普通类型,直接强制
if (property.getType() == Float.TYPE || property.getType() == Float.class) {
if (value instanceof Double) {
value = ((Double) value).floatValue();
}
}
// Correct when the property a String but the value is binary
if (property.getType() == String.class && Tag.BINARY.equals(valueNode.getTag())
&& value instanceof byte[]) {
value = new String((byte[]) value);
}
// 通过Property,执行set方法,为对象对应属性设置值
if (memberDescription == null || !memberDescription.setProperty(object, key, value)) {
property.set(object, value);
}
} catch (DuplicateKeyException e) {
throw e;
} catch (Exception e) {
throw new ConstructorException(
"Cannot create property=" + key + " for JavaBean=" + object, node.getStartMark(),
e.getMessage(), valueNode.getStartMark(), e);
}
}
return object;
}
}
2.1)遍历node的values集合,即NodeTuple节点元组;
2.2)获取节点的key,即NodeTuple.keyNode中获取;
2.3)获取node.type的TypeDescription;
2.4)执行TypeDescription.getProperty() -> discoverProperty() -> PropertyUtils.getProperty() -> getProperty() -> getPropertiesMap(),获取key对应的Property;
在该方法中,通过Introspector(内省,类似反射技术),解析传入的Class及其父类中包含的属性、方法及对应类型信息封装成Property对象。
Property对象可以方便的获取属性、方法的详细信息(如方法对应的参数个数、参数类型)
2.5)通过constructObject()获取配置的value信息(newInstance()也是调用的该方法),返回Map或String等对象;
在BaseConstructor.constructObjectNoCheck(Node node)方法中,执行constructor.construct(node),获取配置的Map对象信息
2.6)执行property.set(object, value),如animals,则Property为MethodProperty,使用反射,调用set方法,进行赋值;
小结
SnakeYaml解析Yaml时可以通过以下方式实现相对复杂的配置:
1)可以在yaml文件中自定义标签,在代码中通过TypeDescription设置自定义标签对应的Java类型;
2)可以通过自定义Construct,实现Object construct(Node node)接口,返回node对应的Java对象;
在使用自定义的Construct时,需要自定义Constructor【继承SnakeYaml的Constructor】,重写Construct getConstructor(Node node)方法,再改方法中返回自定义的Construct。
实现原理:
在BaseConstructor.constructObjectNoCheck(Node node)方法中,调用getConstructor(Node node)获取一个Construct【自定义了Constructor,所以此处是从自定义的Constructor的getConstructor(Node node)方法中获取自定义的Construct】,通过执行Construct.construct(Node node)获得node对应的对象。
以上为本篇分享的全部内容。
关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。