Yaml及解析框架SnakeYaml简介及TypeDescription的使用和原理

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依赖添加

Groovy 复制代码
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.33</version>
        </dependency>

4.2 实体类

java 复制代码
public interface Animal {
}
java 复制代码
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Setter
@Getter
@ToString
public class Cat implements Animal {

    private String color;
    private int age;

}
java 复制代码
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配置类

java 复制代码
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class YamlTest1Configuration {

    private Dog dog;
    private Cat cat;
}

配置类中包含Dog和Cat对象。

4.4 test1.yml配置文件

Groovy 复制代码
dog:
  color: black
  age: 2
cat:
  color: white
  age: 1

4.5 yaml系列化为Java配置类

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配置类

java 复制代码
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配置文件

Groovy 复制代码
animals:
- !Dog    # Dog标签
  color: black
  age: 2
- !Cat    # Cat标签
  color: white
  age: 1

在配置文件中,通过 ! 符号,定义了Dog和Cat标签,用于指定标签下面的信息为对应的标签内容。

5.2.3 yaml系列化为Java配置类

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版本的源码如下:

java 复制代码
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对应的对象。

以上为本篇分享的全部内容。

关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。

相关推荐
ChaITSimpleLove2 天前
K8s 一键部署 MongoDB 的 Replica-Set 和 MongoDB-Express
mongodb·kubernetes·express·高可用·yaml·replica-set
hello world smile15 天前
最全的Flutter中pubspec.yaml及其yaml 语法的使用说明
android·前端·javascript·flutter·dart·yaml·pubspec.yaml
2301_8061313617 天前
yaml文件编写
yaml
peanutfish1 个月前
Chapter 9 RH294 RHEL Automation with Ansible
linux·ansible·yaml
peanutfish1 个月前
Chapter 8 RH294 RHEL Automation with Ansible
linux·ansible·yaml
peanutfish2 个月前
Chapter 4 RH294 RHEL Automation with Ansible
linux·ansible·yaml
peanutfish2 个月前
Chapter 5 RH294 RHEL Automation with Ansible
linux·ansible·yaml
曹开尔3 个月前
hexo+github+zeabur个人博客
arcgis·node.js·github·yaml
全栈小54 个月前
【文心智能体】梗图七夕版,一分钟让你看懂如何优化prompt,以及解析低代码工作流编排实现过程和零代码结合插件实现过程,依然是干货满满,进来康康吧
人工智能·低代码·prompt·yaml·智能体