在运行时从 JAR 文件加载 FXML 控制器

Loading a FXML Controller at Runtime from a JAR file

我正在使用 FXML 在 Java 和 JavaFX 中编写模块化音板。关键方面之一是能够在运行时加载实现插件抽象 class 的任意 class。 插件摘要 class 如下所示:

import javafx.scene.control.Tab;
import java.io.File;
import java.io.FileReader;
import java.net.URLClassLoader;
import java.util.Properties;

public abstract class Plugin {
    protected Tab mainTab;
    protected Tab settingsTab;
    protected Properties propertyFile = new Properties();
    protected File fileName;
    protected URLClassLoader loader;

    public Plugin(){
        mainTab = new Tab();
    }

    public Plugin(String tabName){
        mainTab = new Tab(tabName);
        settingsTab = new Tab(tabName);
    }
    public abstract void save();

    public Tab getMainTab(){
        return mainTab;
    }
    public Tab getSettingsTab(){
        return settingsTab;
    }
    public void initPropertyFile(String filename){
        try{
            File file = new File(System.getenv("APPDATA")+"\Testing\"+filename+".properties");
            if(file.exists())
                propertyFile.load(new FileReader(file));
            else{
                file.createNewFile();
                propertyFile.load(new FileReader(file));
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public void setClassLoader(URLClassLoader loader){
         this.loader = loader;
    }
    public abstract void initController();

    public Properties getPropertyFile(){
        return propertyFile;
    }

}

我使用 URLClassLoader 加载在指定 JAR 文件中找到的任何 classes,然后我实例化插件的任何实例并将它们收集到列表中。我遇到问题的地方是创建一个 FXML 控制器以用于我在运行时实例化的这些 classes。因为这些 classes 和控制器都是在运行时使用自定义 URLClassLoader 加载的,所以我无法在我的 FXML 文件中指定一个 FXML 控制器。如果我尝试这样做,我会收到一条错误消息,指出找不到我用于控制器的 class。如果我没有在 FXML 文件中指定控制器,它可以加载 UI 组件,但我无法在其中指定任何操作。我可以使用 FXMLLoader.setController() 方法设置控制器,但是当我像这样加载控制器时,我使用的 none 按钮有任何操作。我试过使用带有 FXML 注释的 initialize() 方法,但 FXMLLoader 不会自动调用此方法。如果我自己调用初始化,那么 FXML 似乎还没有被注入,但是 FXML 文件指定的 UI 组件以 FXML 文件指定的格式出现在我的屏幕上。

import java.net.URL;

public class Plugin3 extends Plugin {
    FXMLLoader mainTabLoader;
    URL testURL;


    public Plugin3(){
        //Calls Plugin Constructor to initialize mainTab and settingsTab, and set the UI names of the tabs
        super("Plugin3");

        //Try creating FXMLLoader to setup the UI of the plugin
        try {
            mainTabLoader = new FXMLLoader();

            //mainTabLoader.setController(this);
            Plugin3 thisObj = mainTabLoader.getController();
            testURL = getClass().getResource("testFXML.fxml");
            //Load the FXML file

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void save() {

    }

    //Attempts to set the controller of the FXMLLoader mainTabLoader and call the initialize method of that controller
    @Override
    public void initController() {
        try {
            String classPath = "com.example.plugindevelopment.Plugin3Controller";
            mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
           
            getMainTab().setContent(mainTabLoader.load(testURL));
            mainTabLoader.getController().getClass().getDeclaredMethod("initialize").invoke(mainTabLoader.getController());
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

这是控制器:

import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;

public class Plugin3Controller
{
    @FXML
    VBox root;
    @FXML
    Label label;
    @FXML
    Slider slider;
    @FXML
    Button button1;

    protected void onButtonClick(){
        System.out.println("Hello");
    }

    @FXML
    public void initialize(){
        System.out.println("Initializing Plugin3Controller");
        button1 = new Button("Hello");
        button1.setOnAction(event -> onButtonClick());
    }
}

这是 FXML 文件:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <VBox fx:id="root" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
         <children>
            <Label fx:id="label" prefHeight="137.2" text="text" />
            <Slider fx:id="slider">
               <VBox.margin>
                  <Insets top="100.0" />
               </VBox.margin>
               <padding>
                  <Insets top="50.0" />
               </padding></Slider>
            <Button fx:id="button1" mnemonicParsing="false" text="Click Me" />
         </children></VBox>
   </children>
</AnchorPane>

任何人都可以提供任何见解,我将不胜感激。谢谢。

Because both these classes and the controllers are loaded at runtime using a custom URLClassLoader I am unable to just specify a FXML controller inside my FXML files. If I try to, I get an error saying that the class I am using for a controller could not be found.

请注意,您可以 set the Class Loader to be used by the FXMLLoader

您尝试手动设置控制器的解决方案存在一些错误。最重要的是:

        String classPath = "com.example.plugindevelopment.Plugin3Controller";
        mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
       
        getMainTab().setContent(mainTabLoader.load(testURL));

此处 mainTabLoader.load(testURL) 调用 static 方法 FXMLLoader.load(URL)。由于这是一个静态方法,它根本不引用 FXMLLoader 实例 mainTabLoader,尤其是不会知道控制器实例。

你应该使用

        String classPath = "com.example.plugindevelopment.Plugin3Controller";
        mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
        mainTabLoader.setLocation(testURL);
       
        getMainTab().setContent(mainTabLoader.load());

注意对 load() 的调用没有参数,调用 实例 方法 FXMLLoader.load().

一旦 FXMLLoader 正确引用控制器实例,它将自动调用 initialize()。你应该删除行

        mainTabLoader.getController().getClass().getDeclaredMethod("initialize").invoke(mainTabLoader.getController());

你的控制器实现也有错误(我猜这可能是为了解决由上述错误引起的问题的绝望尝试)。

实例化对象并将它们分配给 @FXML 注释字段总是错误,就像您对

所做的那样
button1 = new Button("Hello");

删除此行。它会导致事件处理程序设置在 UI 中未显示的按钮上,而不是设置在 FXML 文件中声明的按钮上。

你应该拥有的一切

public class Plugin3Controller
{
    @FXML
    VBox root;
    @FXML
    Label label;
    @FXML
    Slider slider;
    @FXML
    Button button1;

    protected void onButtonClick(){
        System.out.println("Hello");
    }

    @FXML
    public void initialize(){
        System.out.println("Initializing Plugin3Controller");
        button1.setOnAction(event -> onButtonClick());
    }
}

public class Plugin3 extends Plugin {
    FXMLLoader mainTabLoader;
    URL testURL;

    public Plugin3(){
        //Calls Plugin Constructor to initialize mainTab and settingsTab, and set the UI names of the tabs
        super("Plugin3");

        //Try creating FXMLLoader to setup the UI of the plugin

            mainTabLoader = new FXMLLoader();

            //mainTabLoader.setController(this);

            // This line is patently nonsense: remove it:
            // Plugin3 thisObj = mainTabLoader.getController();
            testURL = getClass().getResource("testFXML.fxml");
            //Load the FXML file

    }

    @Override
    public void save() {

    }

    //Attempts to set the controller of the FXMLLoader mainTabLoader and call the initialize method of that controller
    @Override
    public void initController() {
        try {
            String classPath = "com.example.plugindevelopment.Plugin3Controller";
            mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
            mailTabLoader.setLocation(testURL);
           
            getMainTab().setContent(mainTabLoader.load());
            
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}