从 YAML 文件加载 JavaFX 形状的 LinkedList?

Load LinkedList of JavaFX Shapes from YAML file?

提问人:Runsva 提问时间:10/26/2023 更新时间:10/28/2023 访问量:81

问:

我正在尝试使用 SnakeYAML Java 库从存储在 YAML 文件中的一系列 JavaFX 对象创建 Java。LinkedListShape

我编写了以下简单的 Java 类,用于将对象保存和加载到 YAML 文件中:LinkedList

package com.example.test_1;

import javafx.stage.FileChooser;
import org.yaml.snakeyaml.Yaml;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.LinkedList;

public class YAML
{
    static public LinkedList<Object> YAML_ListLoad()
    {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Load");
        fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("YAML File", "*.yaml"));
        File file = fileChooser.showOpenDialog(App.MainStage);

        if (file == null)
        {
            System.out.println("Unable to load YAML file because the entered directory is invalid!");
        }
        else
        {
            try
            {
                return YAML_ListRead(file.getAbsolutePath());
            }
            catch (IOException e)
            {
                System.out.println("Encountered error while loading YAML file!");
            }
        }

        return null;
    }

    static public LinkedList<Object> YAML_ListRead(String filePath) throws IOException
    {
        Yaml yaml = new Yaml();
        FileReader fileReader = new FileReader(filePath);

        return yaml.load(fileReader);
    }

    static public boolean YAML_ListSave(LinkedList<Object> list)
    {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Save");
        fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("YAML File", "*.yaml"));
        fileChooser.setInitialFileName("file.yaml");
        File file = fileChooser.showSaveDialog(App.MainStage);

        if (file == null)
        {
            System.out.println("Unable to save YAML file because the entered directory is invalid!");
        }
        else
        {
            try
            {
                YAML_ListWrite(file.getAbsolutePath(), list);

                return true;
            }
            catch (IOException e)
            {
                System.out.println("Encountered error while saving YAML file!");
            }
        }

        return false;
    }

    static public void YAML_ListWrite(String filePath, LinkedList<Object> list) throws IOException
    {
        Yaml yaml = new Yaml();
        FileWriter fileWriter = new FileWriter(filePath);
        yaml.dump(list, fileWriter);
    }
}

其中 是一个函数,该函数返回它从用户使用 选择的任何文件加载的对象,并且是将提供的对象保存到用户选择的 YAML 文件的函数。YAML_ListLoadLinkedListFileChooserYAML_ListSaveLinkedList

上面的 YAML 类旨在从以下测试函数运行:

public void Save()
{
    LinkedList<Object> shapes = new LinkedList<Object>();
    shapes.add(new Circle(5, Color.RED));
    shapes.add(new Circle(10, Color.BLUE));

    YAML.YAML_ListSave(shapes);
}

public void Load()
{
    LinkedList<Object> shapes = YAML.YAML_ListLoad();

    System.out.println(shapes);
}

其中只需创建 2 个 JavaFX 对象,并尝试将它们保存在 YAML 文件中,然后尝试将 YAML 文件加载到 .SaveCircleLoadLinkedList

在上面的代码中,保存函数似乎运行良好。运行上面运行器摘录中的函数会导致创建以下 YAML 文件:Save

- !!javafx.scene.shape.Circle
  accessibleHelp: null
  accessibleRole: NODE
  accessibleRoleDescription: null
  accessibleText: null
  blendMode: null
  cache: false
  cacheHint: DEFAULT
  centerX: 0.0
  centerY: 0.0
  clip: null
  cursor: null
  depthTest: INHERIT
  disable: false
  effect: null
  eventDispatcher: !!com.sun.javafx.scene.NodeEventDispatcher {}
  fill: !!javafx.scene.paint.Color {}
  focusTraversable: false
  id: null
  inputMethodRequests: null
  layoutX: 0.0
  layoutY: 0.0
  managed: true
  mouseTransparent: false
  nodeOrientation: INHERIT
  onContextMenuRequested: null
  onDragDetected: null
  onDragDone: null
  onDragDropped: null
  onDragEntered: null
  onDragExited: null
  onDragOver: null
  onInputMethodTextChanged: null
  onKeyPressed: null
  onKeyReleased: null
  onKeyTyped: null
  onMouseClicked: null
  onMouseDragEntered: null
  onMouseDragExited: null
  onMouseDragOver: null
  onMouseDragReleased: null
  onMouseDragged: null
  onMouseEntered: null
  onMouseExited: null
  onMouseMoved: null
  onMousePressed: null
  onMouseReleased: null
  onRotate: null
  onRotationFinished: null
  onRotationStarted: null
  onScroll: null
  onScrollFinished: null
  onScrollStarted: null
  onSwipeDown: null
  onSwipeLeft: null
  onSwipeRight: null
  onSwipeUp: null
  onTouchMoved: null
  onTouchPressed: null
  onTouchReleased: null
  onTouchStationary: null
  onZoom: null
  onZoomFinished: null
  onZoomStarted: null
  opacity: 1.0
  pickOnBounds: false
  radius: 5.0
  rotate: 0.0
  rotationAxis: &id001 {}
  scaleX: 1.0
  scaleY: 1.0
  scaleZ: 1.0
  smooth: true
  stroke: null
  strokeDashOffset: 0.0
  strokeLineCap: SQUARE
  strokeLineJoin: MITER
  strokeMiterLimit: 10.0
  strokeType: CENTERED
  strokeWidth: 1.0
  style: ''
  translateX: 0.0
  translateY: 0.0
  translateZ: 0.0
  userData: null
  viewOrder: 0.0
  visible: true
- !!javafx.scene.shape.Circle
  accessibleHelp: null
  accessibleRole: NODE
  accessibleRoleDescription: null
  accessibleText: null
  blendMode: null
  cache: false
  cacheHint: DEFAULT
  centerX: 0.0
  centerY: 0.0
  clip: null
  cursor: null
  depthTest: INHERIT
  disable: false
  effect: null
  eventDispatcher: !!com.sun.javafx.scene.NodeEventDispatcher {}
  fill: !!javafx.scene.paint.Color {}
  focusTraversable: false
  id: null
  inputMethodRequests: null
  layoutX: 0.0
  layoutY: 0.0
  managed: true
  mouseTransparent: false
  nodeOrientation: INHERIT
  onContextMenuRequested: null
  onDragDetected: null
  onDragDone: null
  onDragDropped: null
  onDragEntered: null
  onDragExited: null
  onDragOver: null
  onInputMethodTextChanged: null
  onKeyPressed: null
  onKeyReleased: null
  onKeyTyped: null
  onMouseClicked: null
  onMouseDragEntered: null
  onMouseDragExited: null
  onMouseDragOver: null
  onMouseDragReleased: null
  onMouseDragged: null
  onMouseEntered: null
  onMouseExited: null
  onMouseMoved: null
  onMousePressed: null
  onMouseReleased: null
  onRotate: null
  onRotationFinished: null
  onRotationStarted: null
  onScroll: null
  onScrollFinished: null
  onScrollStarted: null
  onSwipeDown: null
  onSwipeLeft: null
  onSwipeRight: null
  onSwipeUp: null
  onTouchMoved: null
  onTouchPressed: null
  onTouchReleased: null
  onTouchStationary: null
  onZoom: null
  onZoomFinished: null
  onZoomStarted: null
  opacity: 1.0
  pickOnBounds: false
  radius: 10.0
  rotate: 0.0
  rotationAxis: *id001
  scaleX: 1.0
  scaleY: 1.0
  scaleZ: 1.0
  smooth: true
  stroke: null
  strokeDashOffset: 0.0
  strokeLineCap: SQUARE
  strokeLineJoin: MITER
  strokeMiterLimit: 10.0
  strokeType: CENTERED
  strokeWidth: 1.0
  style: ''
  translateX: 0.0
  translateY: 0.0
  translateZ: 0.0
  userData: null
  viewOrder: 0.0
  visible: true

但是,尝试通过调用将创建的 YAML 文件加载回 会导致许多错误,包括:LinkedListSave

Caused by: Cannot create property=eventDispatcher for JavaBean=Circle[centerX=0.0, centerY=0.0, radius=0.0, fill=0x000000ff]
 in 'reader', line 1, column 3:
    - !!javafx.scene.shape.Circle
      ^
Can't construct a java object for tag:yaml.org,2002:com.sun.javafx.scene.NodeEventDispatcher; exception=java.lang.InstantiationException: NoSuchMethodException:com.sun.javafx.scene.NodeEventDispatcher.<init>()
 in 'reader', line 16, column 20:
      eventDispatcher: !!com.sun.javafx.scene.NodeEvent ... 
                       ^

 in 'reader', line 16, column 20:
      eventDispatcher: !!com.sun.javafx.scene.NodeEvent ... 
                       ^

Can't construct a java object for tag:yaml.org,2002:com.sun.javafx.scene.NodeEventDispatcher; exception=java.lang.InstantiationException: NoSuchMethodException:com.sun.javafx.scene.NodeEventDispatcher.<init>()
 in 'reader', line 16, column 20:
      eventDispatcher: !!com.sun.javafx.scene.NodeEvent ... 
                       ^

在我看来,YAML 文件中标记的条目被视为某种或空条目。YAML 解析器似乎无法解析这些内容。!!null

从 YAML 文件加载一系列 JavaFX 对象的正确方法是什么?甚至可以使用 SnakeYAML 加载 JavaFX 对象吗?Shape

感谢您阅读我的帖子,任何指导都值得赞赏。

java javafx yaml 蛇类

评论

3赞 James_D 10/26/2023
直接持久化 UI 对象可能不是一个好的方法。(例外情况可能包括编写 GUI 构建器,如 SceneBuilder,但即便如此,也可能有更好的方法)。问题在于 UI 对象有很多属性,其中许多属性几乎肯定您不希望保留。例如,持久化任何事件处理程序确实没有意义。您遇到的错误是由于尝试重新创建由不属于公共 API 的类(例如)表示的内部属性而引起的。com.sun.javafx.scene.NodeEvent
0赞 James_D 10/26/2023
更好的方法可能是为应用程序创建模型(使用 MVC、MVP 或 MVVM 类型的方法)。就应用程序中的数据而言,圆圈实际上代表什么?编写仅表示数据的类。然后保留模型而不是 UI,并从从文件加载的模型重新构造 UI。
0赞 James_D 10/26/2023
我不知道 SnakeYAML,所以它可能有办法指定要持久化的内容,但这仍然感觉是错误的方法。
0赞 Runsva 10/26/2023
似乎发生的情况是,每个带有 present 的条目似乎都无法解析。YAML中名称的含义是什么?这是否表示条目无效?!!!!
3赞 Slaw 10/26/2023
不,该文件的解析很好。错误是某些类(例如 )没有无参数构造函数,因此库不知道如何构造它们。也许 SnakeYAML 可以配置为使用缺少的无参数构造函数,我不确定。但正如 James 所解释的,你不应该首先直接序列化 UI 对象;除了缺少无参数构造函数之外,还有其他问题。创建一个仅包含需要保留的数据的模型类,然后保存和加载该类的对象,而不是 UI 对象。NodeEventDispatcher

答:

2赞 flyx 10/26/2023 #1

文档中所述

如果自定义 Java 类符合规范,则可以加载和转储它,而无需任何额外的代码。JavaBean

该类不符合规范,因为它不提供零参数构造函数。这就是错误消息试图告诉您的内容。com.sun.javafx.scene.NodeEventDispatcherJavaBean

您可以编写一个自定义构造函数来解决此限制,但正如其他人所说,直接存储 UI 类不是一个好方法——通常,自动序列化对于被动数据类是可行的,但通常不适用于任何 Java 类。

关于,这些只是应用于以下节点的标签。 应用于包含所有后续键值对的以下映射,而应用于以下空映射。映射是空的,因为该类没有任何公共属性(公共值或具有 getter 和 setter 的值)。!!!!javafx.scene.shape.Circle!!com.sun.javafx.scene.NodeEventDispatcher{}

标签帮助 SnakeYAML 确定它应该将一些映射加载到哪个类。当实际对象的类不等于属性的已定义静态类时,它们是必需的。例如,定义为 ,但序列化对象中的实际值是 .因此,SnakeYAML 添加了标记,以便在加载时知道它需要构造此派生类。 (然后它不起作用,因为该类不是 。eventDispatcherjavafx.event.EventDispatchercom.sun.javafx.scene.NodeEventDispatcherJavaBean

2赞 jewelsea 10/28/2023 #2

基于 YAML 文件格式序列化和反序列化形状的示例。

它使用杰克逊来协助编组操作。Jackson(目前)使用 SnakeYAML 作为其 YAML 实现,尽管这对通过 Jackson API 的使用是完全透明的。

它只允许单一的形状类型(a),但可以毫不费力地扩展到处理其他形状。Circle

应用输出

screenshot

Wrote: /var/folders/87/r96kpgbj5tjdpsgml9gb_g8c0000gn/T/shapes-4277733445362494936.yaml
---
- !<Circle>
  centerX: 40.0
  centerY: 50.0
  radius: 20.0
  fill: "BLUE"
- !<Circle>
  centerX: 20.0
  centerY: 20.0
  radius: 10.0
  fill: "RED"

Read: /var/folders/87/r96kpgbj5tjdpsgml9gb_g8c0000gn/T/shapes-4277733445362494936.yaml
[CircleModel[centerX=40.0, centerY=50.0, radius=20.0, fill=BLUE], CircleModel[centerX=20.0, centerY=20.0, radius=10.0, fill=RED]]

使用 FXML 的替代实现

您可以将对象存储为 FXML,然后可以使用 FXMLLoader 加载它们。

如果您有一些工具来创建形状并以 FXML 格式导出它们,这可能会很好用。

但是,如果需要从应用程序中的模型对象以编程方式创建 FXML 文件,这可能有点困难。我知道没有容易获得的第三方库可以做到这一点。Jackson 与这里用于 YAML 的类似,可以通过一些额外的编程创建基本的 FXML 文件。但是,这超出了本答案的范围。

使用备用数据格式(JSON 或 XML)

若要将 JSON 用作序列化格式,请在类中使用:ShapeRepository

private final JsonFactory serializationFactory = new JsonFactory();

而不是 :YamlFactory

private final JsonFactory serializationFactory = new YAMLFactory();

同样,对于 XML,请参阅:

应用代码

模块信息.java

module com.example.persistentcirclesapp {
    requires javafx.graphics;
    requires com.fasterxml.jackson.dataformat.yaml;
    requires com.fasterxml.jackson.databind;

    exports com.example.persistentcirclesapp;
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>PersistentCirclesApp</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>PersistentCirclesApp</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-graphics</artifactId>
            <version>21.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
            <version>2.15.3</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.15.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

形状应用程序.java

package com.example.persistentcirclesapp;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.List;

public class ShapeApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        ShapeRepository shapeRepository = new ShapeRepository();
        shapeRepository.save(
                List.of(
                        new CircleModel(40, 50, 20, "BLUE"),
                        new CircleModel(20, 20, 10, "RED")
                )
        );

        List<ShapeModel> shapes = shapeRepository.load();

        ShapeRenderer shapeRenderer = new ShapeRenderer();

        stage.setScene(new Scene(shapeRenderer.render(shapes)));
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

形状模型.java

package com.example.persistentcirclesapp;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.WRAPPER_OBJECT
)
@JsonSubTypes({
        @JsonSubTypes.Type(value = CircleModel.class, name = "Circle")
})
public sealed interface ShapeModel permits CircleModel {}

CircleModel.java

package com.example.persistentcirclesapp;

public record CircleModel(
        double centerX,
        double centerY,
        double radius,
        String fill
) implements ShapeModel {}

形状存储库.java

package com.example.persistentcirclesapp;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class ShapeRepository {
    private final Path shapeRepositoryPath;

    private final JsonFactory serializationFactory = new YAMLFactory();
    private final ObjectMapper mapper =
            new ObjectMapper(serializationFactory);

    private final CollectionType shapeCollectionType = mapper.getTypeFactory().constructCollectionType(
            List.class, ShapeModel.class
    );

    private final ObjectWriter writer = new ObjectMapper(serializationFactory)
            .writerFor(shapeCollectionType);

    private final ObjectReader reader = new ObjectMapper(serializationFactory)
            .readerFor(shapeCollectionType);

    public ShapeRepository() throws IOException {
        shapeRepositoryPath = Files.createTempFile("shapes-", ".yaml");
    }

    public ShapeRepository(Path shapeRepositoryPath) {
        this.shapeRepositoryPath = shapeRepositoryPath;
    }

    public void save(List<ShapeModel> shapes) throws IOException {
        writer.writeValue(
                shapeRepositoryPath.toFile(),
                shapes
        );

        System.out.println("Wrote: " + shapeRepositoryPath + "\n" + Files.readString(shapeRepositoryPath));
    }

    public List<ShapeModel> load() throws IOException {
        List<ShapeModel> shapes = reader.readValue(
                shapeRepositoryPath.toFile()
        );

        System.out.println("Read: " + shapeRepositoryPath + "\n" + shapes);

        return shapes;
    }
}

ShapeRenderer.java

package com.example.persistentcirclesapp;

import javafx.scene.layout.Pane;

import java.util.List;

public class ShapeRenderer {
    private final ShapeFactory shapeFactory = new ShapeFactory();

    public Pane render(List<ShapeModel> shapes) {
        Pane pane = new Pane();

        shapes.stream()
                .map(shapeFactory::createShape)
                .forEach(shape ->
                        pane.getChildren().add(shape)
                );

        return pane;
    }
}

ShapeFactory.java

package com.example.persistentcirclesapp;

import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;

public class ShapeFactory {
    public Shape createShape(ShapeModel shapeModel) {
        return switch(shapeModel) {
            case CircleModel circleModel ->
                    new Circle(
                            circleModel.centerX(),
                            circleModel.centerY(),
                            circleModel.radius(),
                            Paint.valueOf(circleModel.fill())
                    );
        };
    }
    
    public ShapeModel createModel(Shape shape) {
        return switch(shape) {
            case Circle circle ->
                    new CircleModel(
                            circle.getCenterX(), 
                            circle.getCenterY(), 
                            circle.getRadius(), 
                            circle.getFill().toString() 
                    );
            default -> 
                    throw new IllegalArgumentException("Unsupported shape type " + shape.getClass().getName());
        };
    }
}