You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaFX开发Minecraft控制台程序遇NullPointerException求助

Minecraft JavaFX控制台程序随机空指针异常问题

错误日志

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot read the array length because "this.runs" is null
    at javafx.graphics@18.0.1/com.sun.javafx.text.PrismTextLayout.addTextRun(PrismTextLayout.java:770)
    at javafx.graphics@18.0.1/com.sun.javafx.text.GlyphLayout.addTextRun(GlyphLayout.java:140)
    at javafx.graphics@18.0.1/com.sun.javafx.text.GlyphLayout.breakRuns(GlyphLayout.java:210)
    at javafx.graphics@18.0.1/com.sun.javafx.text.PrismTextLayout.buildRuns(PrismTextLayout.java:785)
    at javafx.graphics@18.0.1/com.sun.javafx.text.PrismTextLayout.layout(PrismTextLayout.java:1036)
    at javafx.graphics@18.0.1/com.sun.javafx.text.PrismTextLayout.ensureLayout(PrismTextLayout.java:223)
    at javafx.graphics@18.0.1/com.sun.javafx.text.PrismTextLayout.getBounds(PrismTextLayout.java:246)
    at javafx.graphics@18.0.1/javafx.scene.text.Text.getLogicalBounds(Text.java:432)
    at javafx.graphics@18.0.1/javafx.scene.text.Text.doComputeGeomBounds(Text.java:1187)
    at javafx.graphics@18.0.1/javafx.scene.text.Text$1.doComputeGeomBounds(Text.java:149)
    at javafx.graphics@18.0.1/com.sun.javafx.scene.shape.TextHelper.computeGeomBoundsImpl(TextHelper.java:90)
    at javafx.graphics@18.0.1/com.sun.javafx.scene.NodeHelper.computeGeomBounds(NodeHelper.java:116)
    at javafx.graphics@18.0.1/javafx.scene.Node.updateGeomBounds(Node.java:3818)
    at javafx.graphics@18.0.1/javafx.scene.Node.getGeomBounds(Node.java:3780)
    at javafx.graphics@18.0.1/javafx.scene.Node.getLocalBounds(Node.java:3728)
    at javafx.graphics@18.0.1/javafx.scene.Node$MiscProperties$3.computeBounds(Node.java:6818)
    at javafx.graphics@18.0.1/javafx.scene.Node$LazyBoundsProperty.get(Node.java:9712)
    at javafx.graphics@18.0.1/javafx.scene.Node$LazyBoundsProperty.get(Node.java:9682)
    at javafx.graphics@18.0.1/javafx.scene.Node.getBoundsInLocal(Node.java:3408)
    at javafx.controls@18.0.1/javafx.scene.control.skin.TextAreaSkin$ContentView.layoutChildren(TextAreaSkin.java:1324)
    at javafx.graphics@18.0.1/javafx.scene.Parent.layout(Parent.java:1207)
    at javafx.graphics@18.0.1/javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.graphics@18.0.1/javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.graphics@18.0.1/javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.graphics@18.0.1/javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.graphics@18.0.1/javafx.scene.Parent.layout(Parent.java:1214)
    at javafx.graphics@18.0.1/javafx.scene.Scene.doLayoutPass(Scene.java:579)
    at javafx.graphics@18.0.1/javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2499)
    at javafx.graphics@18.0.1/com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:405)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at javafx.graphics@18.0.1/com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:404)
    at javafx.graphics@18.0.1/com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:434)
    at javafx.graphics@18.0.1/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)
    at javafx.graphics@18.0.1/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)
    at javafx.graphics@18.0.1/com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)
    at javafx.graphics@18.0.1/com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)
    at javafx.graphics@18.0.1/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics@18.0.1/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics@18.0.1/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
    at java.base/java.lang.Thread.run(Thread.java:833)

问题描述

使用JavaFX开发的简易Minecraft控制台程序,运行时随机频繁抛出上述空指针异常,无法定位触发原因,附上源码请求分析原因及修复方法。

源码

package com.koolade446.mconsole;

import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventType;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

public class MainController {

    @FXML
    public ComboBox<String> typeBox;
    @FXML
    public ComboBox<String> versionBox;
    @FXML
    public TextArea console;
    @FXML
    public Button startStopButton;
    @FXML
    public Button killButton;
    @FXML
    Button changeVersionButton;
    @FXML
    Label pathContainer;
    @FXML
    TextField commandBox;

    private final Map<String, List<String>> typeMap = new HashMap<>();

    private boolean serverIsStarted = false;
    private ExecutorService executorService;

    private Path directoryPath = Path.of(new File(".").getCanonicalPath());
    private final String[] endpoints = new String[]{
            "servers/spigot",
            "vanilla/vanilla",
            "vanilla/snapshot",
            "modded/forge",
            "modded/fabric"
    };
    File serverJar = new File(directoryPath + "/server.jar");
    private PrintWriter outputStreamWriter = null;

    public MainController() throws IOException {
    }


    public void initialize() throws IOException, URISyntaxException {

        startStopButton.setStyle("-fx-background-image: url('power.png')");
        killButton.setStyle("-fx-background-image: url('kill.png')");
        pathContainer.setText(directoryPath.toString());

        Platform.runLater(() -> {
            try {
                for (String endpoint : endpoints) {
                    List<String> l = new ArrayList<>();
                    URL url = new URL("https://serverjars.com/api/fetchAll/" + endpoint);
                    HttpURLConnection con = (HttpURLConnection) url.openConnection();
                    con.setRequestMethod("GET");
                    con.connect();

                    JSONObject responseJson = new JSONObject(new BufferedReader(new InputStreamReader(con.getInputStream())).lines().collect(Collectors.joining()));

                    con.disconnect();

                    for (Object obj : responseJson.getJSONArray("response")) {
                        JSONObject jsonObject = (JSONObject) obj;
                        l.add(jsonObject.getString("version"));
                    }
                    typeMap.put(endpoint.split("/")[1].replace("/", ""), l);
                }
                typeBox.getItems().addAll(typeMap.keySet());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    public void changeVersionOptions() {
        versionBox.getItems().clear();
        versionBox.setValue(null);
        versionBox.getItems().addAll(typeMap.get(typeBox.getValue()));
    }

    public void updateVersion(ActionEvent actionEvent) throws MalformedURLException {
        printToConsole("INFO", "Attempting to update to " + typeBox.getValue() + " " + versionBox.getValue());
        Platform.runLater(() -> {
            try {
                URL url = new URL("https://serverjars.com/api/fetchJar/" + endpoints[getClosestIndex(endpoints, typeBox.getValue())] + "/" + versionBox.getValue());
                HttpURLConnection con = (HttpURLConnection) url.openConnection();
                con.setRequestMethod("GET");
                printToConsole("INFO", "Connecting to servers...");
                con.connect();

                printToConsole("INFO", "Downloading new jar file...");
                InputStream is = con.getInputStream();
                FileOutputStream fos = new FileOutputStream(serverJar);

                printToConsole("INFO", "Writing new jar file...");
                byte[] jarFileData = is.readAllBytes();

                System.out.println(serverJar.getPath());

                fos.write(jarFileData, 0, jarFileData.length);

                is.close();
                fos.close();
                con.disconnect();

                printToConsole("INFO", "Version successfully updated!");
            } catch (Exception e) {
                printToConsole("ERROR", "Error when updating version. Please make sure you select a type and version");
                e.printStackTrace();
            }
        });
    }

    public void changeFolder(ActionEvent event) throws IOException {
        DirectoryChooser directoryChooser = new DirectoryChooser();
        directoryChooser.setTitle("Select minecraft server folder");
        File file = directoryChooser.showDialog(null);

        directoryPath = Path.of(file.getCanonicalPath());
        pathContainer.setText(directoryPath.toString());
        serverJar = new File(directoryPath + "/server.jar");
    }

    private int getClosestIndex(String[] array, String option) throws Exception {
        int index = -1;
        for (String str : array) {
            index++;
            if (str.contains(option)) return index;
        }
        return 0;
    }

    private void printToConsole(String sender, String message) {
        console.appendText(String.format(
                "[%s] %s\n",
                sender,
                message
        ));
    }

    public void togglePowerState(ActionEvent actionEvent) {
        if (executorService == null || executorService.isTerminated()) {
            //This is the bit that I think is causing it but idk how or how to fix it
            Task<Boolean> runnable = new Task() {
                @Override
                protected Boolean call() {
                    try {
                        Process process;
                        ProcessBuilder pb = new ProcessBuilder("java", "-jar", "-Xmx5G", "-Xms1G", "server.jar", "nogui");
                        pb.directory(new File(directoryPath.toString()));
                        process = pb.start();
                        BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
                        setOutputStreamWriter(new PrintWriter(process.getOutputStream()));
                        String line;
                        while (process.isAlive()) {
                            if ((line = br.readLine()) != null) {
                                printToConsole("MINECRAFT", line);
                            }
                        }
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    return true;
                }
            };


            executorService = Executors.newFixedThreadPool(1);
            executorService.execute(runnable);
            executorService.shutdown();


        } else if (!executorService.isTerminated()) {
            outputStreamWriter.println("stop");
            outputStreamWriter.flush();
        }
    }

    public void killServer(ActionEvent actionEvent) {
        if (!executorService.isTerminated()) executorService.shutdownNow();
    }

    public void sendCommand(ActionEvent event) throws IOException {
        outputStreamWriter.println(commandBox.getText());
        outputStreamWriter.flush();
        commandBox.setText("");
    }

    public void onKeyPress(KeyEvent keyEvent) throws IOException {
        if (keyEvent.getCode().equals(KeyCode.ENTER)) {
            outputStreamWriter.println(commandBox.getText());
            outputStreamWriter.flush();
            commandBox.setText("");
        }
    }

    public void setOutputStreamWriter(PrintWriter outputStreamWriter) {
        this.outputStreamWriter = outputStreamWriter;
    }
}

触发原因

核心问题是JavaFX UI组件只能在JavaFX应用线程中操作,而代码在Task的call()方法(运行在后台线程)中直接调用printToConsole()更新TextArea,违反了JavaFX的线程安全规则。

虽然appendText()方法偶尔能正常工作,但它并非线程安全方法。后台线程频繁并发更新UI组件时,会导致Prism文本布局内部状态(如runs数组)出现不一致,进而触发空指针异常。

修复方法

将所有更新UI的操作切换到JavaFX应用线程中执行,修改printToConsole()方法,用Platform.runLater()包裹UI更新逻辑:

private void printToConsole(String sender, String message) {
    Platform.runLater(() -> {
        console.appendText(String.format(
                "[%s] %s\n",
                sender,
                message
        ));
    });
}

额外优化建议

  1. 处理Process错误流:当前代码仅读取Minecraft服务器的输入流,未处理错误流。错误流堆积会导致进程阻塞,建议在后台线程中同时读取输入流和错误流,并用printToConsole()输出错误信息。
  2. 修正Task泛型类型:将Task<Boolean> runnable = new Task()修改为Task<Boolean> runnable = new Task<Boolean>(),避免编译器警告。
  3. 完善ExecutorService管理:服务器进程结束后,确保executorService状态正确更新,避免重复创建或错误判断。

内容的提问来源于stack exchange,提问作者Kyle Derstein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 11:40:44