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 )); }); }
额外优化建议
- 处理Process错误流:当前代码仅读取Minecraft服务器的输入流,未处理错误流。错误流堆积会导致进程阻塞,建议在后台线程中同时读取输入流和错误流,并用
printToConsole()输出错误信息。 - 修正Task泛型类型:将
Task<Boolean> runnable = new Task()修改为Task<Boolean> runnable = new Task<Boolean>(),避免编译器警告。 - 完善ExecutorService管理:服务器进程结束后,确保
executorService状态正确更新,避免重复创建或错误判断。
内容的提问来源于stack exchange,提问作者Kyle Derstein
相关产品推荐
相关产品推荐

