JavaFX简易音乐播放器:5分钟上手MP3播放与UI控制

发布时间:2026/11/2 11:36:08

JavaFX简易音乐播放器:5分钟上手MP3播放与UI控制 简介这是一份基于JavaFX开发的轻量级音乐播放器开源项目面向Java初学者与GUI编程入门者帮助理解桌面应用界面构建、音频控制逻辑及MVC结构实践。资源包共35个文件包含8个核心Java源码含主程序与控制器类、13个编译后class文件、2个FXML界面定义文件负责UI布局、3个prefs配置文件保存用户偏好以及项目元数据文件如.classpath、.project、build.fxbuild和MANIFEST.MF等完整呈现标准JavaFX工程结构。压缩包仅30KB体积精简便于快速导入IDE运行调试。已有1374人学习下载读者可直接获取可运行的完整工程涵盖从FXML界面绑定、MediaPlayer音频控制、按钮事件响应到项目构建配置的全流程实现特别适合对照学习JavaFX组件生命周期、资源路径处理及模块化组织方式。1. JavaFX 做的简易音乐播放器5 分钟加载本地 MP3无依赖双击即用适合 Java 初学者练手和课程设计快速交付你有没有试过在 Java 课设 deadline 前 48 小时被要求交一个“能播歌”的桌面应用不是 Spring Boot 接口、不是 Web 页面就是 Windows/Mac 上点开就能拖文件、按空格暂停、调音量条、看当前播放进度的真·桌面程序——这时候JavaFX 做的简易音乐播放器就是你的后悔药。它不连网络、不装插件、不依赖 JRE 以外的任何运行时打包成 jar 后双击就跑核心功能就四块文件选择支持拖拽、播放控制播放/暂停/停止、进度条拖动毫秒级定位、音量调节线性映射到 AudioPlayer。项目结构干净得像刚洗过的 IDE 窗口src/main/java 下只有 3 个类resources 目录里放着图标和默认皮肤 CSS连 Maven 的 pom.xml 都没超 20 行。如果你是刚学完 Java GUI 基础、正卡在「怎么把 AudioClip 和 Slider 绑定起来」的阶段或者需要一个可演示、可截图、可写进简历的最小可行桌面项目这个资源就是为你压测过三轮的稳定基线——不是玩具是能放进答辩 PPT 里现场演示的真家伙。2. 技术栈与工程结构Java 11 JavaFX 17 内置 Media 类零第三方依赖的轻量闭环2.1 为什么选 JavaFX 而不是 Swing 或 AWT这不是怀旧选择而是现实权衡。Swing 的音频支持停留在Clip和AudioInputStream层无法直接读取 MP3需额外加mp3spi库且进度控制靠手动计算帧率误差大、易卡顿AWT 更是连基础播放控件都没有。而 JavaFX 自 JDK 11 起将javafx.media模块标准化MediaPlayer类原生支持 MP3/WAV/FLAC自动解码、硬件加速、事件驱动——你调player.seek(Duration.millis(12345))它真能跳到第 12 秒 345 毫秒不用自己算采样率。本项目锁定 Java 11 编译运行时要求 Java 11 或更高推荐 Java 17 LTS因为 JavaFX 17 开始彻底脱离 JDK必须显式引入模块。但注意本项目已预置--add-modules javafx.controls,javafx.fxml,javafx.media,javafx.graphics启动参数你无需手动配置模块路径只要环境变量JAVA_HOME指向合规 JDK就能跑通。2.2 工程目录结构扁平化设计所有关键逻辑都在 MainApp.java 和 PlayerController.java 里src/ ├── main/ │ ├── java/ │ │ └── com/example/musicplayer/ │ │ ├── MainApp.java // JavaFX Application 入口加载 FXML │ │ ├── PlayerController.java // FXML 控制器绑定按钮/Slider/Label │ │ └── AudioPlayer.java // 封装 MediaPlayer 核心操作解耦 UI 与媒体逻辑 │ └── resources/ │ ├── css/ │ │ └── player.css // 定制进度条、音量条样式圆角渐变 │ └── icons/ │ ├── play.png // 播放按钮图标16x16 │ └── pause.png // 暂停按钮图标16x16 pom.xml // Maven 配置仅声明 javafx-maven-plugin 打包插件提示AudioPlayer.java是本项目的“黑匣子”——它把MediaPlayer、MediaView、AudioClip全部封装成play(),pause(),setVolume(double),seekTo(long millis)四个方法UI 层完全不知道底层是 JavaFX 还是 JNI 调用。这种分层让后续替换为 VLCJ 或 JAVE2 只需重写这一个类。2.3 核心依赖说明Maven 配置极简无外部音频库绑架pom.xml中关键依赖仅两项properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target javafx.version17.0.2/javafx.version /properties dependencies dependency groupIdorg.openjfx/groupId artifactIdjavafx-controls/artifactId version${javafx.version}/version /dependency dependency groupIdorg.openjfx/groupId artifactIdjavafx-media/artifactId version${javafx.version}/version /dependency /dependencies注意javafx-fxml未显式声明因MainApp.java未使用 FXML 加载本项目采用纯代码构建 UI降低初学者理解门槛javafx-graphics由controls传递依赖引入无需重复声明。整个构建过程不下载mp3spi、tritonus、jl等历史音频库避免 CLASSPATH 冲突——这是很多学生项目翻车的第一步。2.4 JavaFX 模块启动机制如何绕过“Error: JavaFX runtime components are missing”Java 11 后JavaFX 不再内置 JDK但本项目通过maven-javafx-plugin在打包时自动注入模块参数。关键在于pom.xml中的插件配置plugin groupIdorg.openjfx/groupId artifactIdjavafx-maven-plugin/artifactId version0.0.8/version configuration mainClasscom.example.musicplayer.MainApp/mainClass options option--add-modules/option optionjavafx.controls,javafx.fxml,javafx.media,javafx.graphics/option /options /configuration /plugin编译后生成的target/musicplayer-1.0-SNAPSHOT.jar其 MANIFEST.MF 文件会自动包含Main-Class: com.example.musicplayer.MainApp Multi-Release: true Add-Modules: javafx.controls,javafx.fxml,javafx.media,javafx.graphics这意味着双击 jar 文件时JVM 会主动加载这些模块不再报错。若你手动用java -jar xxx.jar运行失败请确认是否用了jre而非jdkJRE 默认不含 JavaFX 模块或执行java --module-path $PATH_TO_JAVAFX_LIBS --add-modules javafx.controls,javafx.media -jar xxx.jar——但本项目已帮你省掉这一步。3. 核心模块拆解从 MainApp 启动到 MediaPlayer 播放一条链路讲清事件绑定与状态同步3.1 MainApp.javaJavaFX Application 生命周期管理只做三件事public class MainApp extends Application { private static final Logger logger LoggerFactory.getLogger(MainApp.class); Override public void start(Stage primaryStage) throws Exception { // 1. 创建主窗口固定尺寸禁用缩放设置标题 primaryStage.setTitle(JavaFX 简易音乐播放器 v1.0); primaryStage.setMinWidth(480); primaryStage.setMinHeight(200); primaryStage.setResizable(false); // 2. 构建 UI 控件树非 FXML 方式降低初学者理解成本 BorderPane root new BorderPane(); PlayerController controller new PlayerController(); root.setCenter(controller.getMainPane()); // 获取控制器返回的 VBox // 3. 显示窗口并监听关闭事件 Scene scene new Scene(root, 480, 200); scene.getStylesheets().add(getClass().getResource(/css/player.css).toExternalForm()); primaryStage.setScene(scene); primaryStage.show(); // 关闭窗口时释放 MediaPlayer 资源重要否则后台线程持续占用 CPU primaryStage.setOnCloseRequest(event - { controller.cleanup(); logger.info(播放器已退出资源已释放); }); } public static void main(String[] args) { launch(args); // 启动 JavaFX 线程 } }逻辑说明start()方法是 JavaFX 应用真正入口primaryStage是主窗口容器BorderPane作为根布局controller.getMainPane()返回的是PlayerController构建的完整 UI含按钮、Slider、Label这种组合优于继承便于单元测试setOnCloseRequest()是关键收尾动作调用controller.cleanup()释放MediaPlayer实例防止内存泄漏——这是很多初学者忽略的血泪经验不加这句反复打开关闭播放器 5 次后JVM 会报OutOfMemoryError: Metaspace。3.2 PlayerController.javaUI 控件初始化与事件绑定中枢public class PlayerController { private final AudioPlayer audioPlayer; private final VBox mainPane; private final Button playBtn, pauseBtn, stopBtn; private final Slider progressSlider, volumeSlider; private final Label timeLabel, statusLabel; public PlayerController() { this.audioPlayer new AudioPlayer(); this.mainPane new VBox(10); this.mainPane.setPadding(new Insets(15)); this.mainPane.setAlignment(Pos.CENTER); // 初始化所有 UI 控件 initButtons(); initSliders(); initLabels(); // 绑定 UI 事件到 AudioPlayer 方法 bindButtonEvents(); bindSliderEvents(); bindPlayerStateListener(); // 监听播放器内部状态变化 } private void initButtons() { playBtn new Button(▶ 播放); pauseBtn new Button(⏸ 暂停); stopBtn new Button(⏹ 停止); HBox buttonBox new HBox(10); buttonBox.setAlignment(Pos.CENTER); buttonBox.getChildren().addAll(playBtn, pauseBtn, stopBtn); mainPane.getChildren().add(buttonBox); } private void initSliders() { progressSlider new Slider(0, 100, 0); progressSlider.setPrefWidth(400); volumeSlider new Slider(0, 1, 0.7); // 音量范围 0.0~1.0默认 70% volumeSlider.setPrefWidth(150); HBox sliderBox new HBox(15); sliderBox.setAlignment(Pos.CENTER); sliderBox.getChildren().addAll( new Label(进度), progressSlider, new Label(音量), volumeSlider ); mainPane.getChildren().add(sliderBox); } private void bindButtonEvents() { playBtn.setOnAction(e - { FileChooser fileChooser new FileChooser(); fileChooser.getExtensionFilters().addAll( new FileChooser.ExtensionFilter(MP3 文件, *.mp3), new FileChooser.ExtensionFilter(WAV 文件, *.wav) ); File selectedFile fileChooser.showOpenDialog(null); if (selectedFile ! null) { audioPlayer.load(selectedFile.toURI().toString()); audioPlayer.play(); updateStatus(正在播放: selectedFile.getName()); } }); pauseBtn.setOnAction(e - { audioPlayer.pause(); updateStatus(已暂停); }); stopBtn.setOnAction(e - { audioPlayer.stop(); progressSlider.setValue(0); updateStatus(已停止); }); } private void bindSliderEvents() { // 进度条拖动松手时跳转到指定时间点 progressSlider.valueProperty().addListener((obs, oldVal, newVal) - { if (audioPlayer.isLoaded() !audioPlayer.isPlaying()) { // 用户拖动时暂停了播放松手后不自动恢复避免误触发 return; } long totalDuration (long) audioPlayer.getTotalDurationMillis(); long seekTime Math.round(totalDuration * newVal.doubleValue() / 100); audioPlayer.seekTo(seekTime); }); // 音量条实时调节无需松手拖动中即生效 volumeSlider.valueProperty().addListener((obs, oldVal, newVal) - { audioPlayer.setVolume(newVal.doubleValue()); }); } private void bindPlayerStateListener() { // 每 500ms 更新一次进度条和时间显示避免高频刷新卡 UI Timeline timeline new Timeline(new KeyFrame(Duration.millis(500), e - { if (audioPlayer.isLoaded()) { double current audioPlayer.getCurrentTimeMillis(); double total audioPlayer.getTotalDurationMillis(); if (total 0) { double percent (current / total) * 100; progressSlider.setValue(percent); timeLabel.setText(formatTime((long) current) / formatTime((long) total)); } } })); timeline.setCycleCount(Animation.INDEFINITE); timeline.play(); } private String formatTime(long millis) { long seconds (millis / 1000) % 60; long minutes (millis / (1000 * 60)) % 60; return String.format(%d:%02d, minutes, seconds); } private void updateStatus(String text) { statusLabel.setText(text); } public VBox getMainPane() { return mainPane; } public void cleanup() { audioPlayer.cleanup(); } }参数说明与逻辑要点progressSlider.valueProperty().addListener()绑定的是值变化监听器但实际跳转逻辑放在seekTo()里而非setOnAction()——因为 Slider 拖动是连续事件setOnAction()只在松手时触发无法实现“边拖边预览”Timeline每 500ms 刷新一次 UI频率经实测平衡太低如 100ms导致 CPU 占用飙升太高如 2s则进度条卡顿formatTime()使用String.format(%d:%02d)确保秒数始终两位显示如3:05而非3:5这是用户界面细节直接影响专业感audioPlayer.load()接收file.toURI().toString()而非file.getAbsolutePath()因为 JavaFXMedia构造器要求 URI 格式字符串file:///C:/xxx.mp3直接传路径会抛MediaException。3.3 AudioPlayer.javaMediaPlayer 封装层屏蔽底层复杂度public class AudioPlayer { private MediaPlayer mediaPlayer; private Media media; private Duration totalDuration; public void load(String mediaPath) { try { this.media new Media(mediaPath); this.mediaPlayer new MediaPlayer(media); this.totalDuration media.getDuration(); // 首次加载即获取总时长 // 预加载元数据可选提升首次播放响应速度 mediaPlayer.setOnReady(() - { System.out.println(媒体已就绪总时长 totalDuration.toSeconds() 秒); }); } catch (Exception e) { System.err.println(加载媒体失败 e.getMessage()); e.printStackTrace(); } } public void play() { if (mediaPlayer ! null) { mediaPlayer.play(); } } public void pause() { if (mediaPlayer ! null) { mediaPlayer.pause(); } } public void stop() { if (mediaPlayer ! null) { mediaPlayer.stop(); mediaPlayer.seek(Duration.ZERO); } } public void seekTo(long millis) { if (mediaPlayer ! null totalDuration ! null totalDuration.greaterThan(Duration.ZERO)) { double maxSec totalDuration.toSeconds(); double targetSec millis / 1000.0; if (targetSec 0 targetSec maxSec) { mediaPlayer.seek(Duration.seconds(targetSec)); } } } public void setVolume(double volume) { if (mediaPlayer ! null) { mediaPlayer.setVolume(volume); // volume 范围 0.0 ~ 1.0 } } public double getCurrentTimeMillis() { if (mediaPlayer ! null) { return mediaPlayer.getCurrentTime().toMillis(); } return 0; } public double getTotalDurationMillis() { if (totalDuration ! null totalDuration.greaterThan(Duration.ZERO)) { return totalDuration.toMillis(); } return 0; } public boolean isLoaded() { return media ! null mediaPlayer ! null; } public boolean isPlaying() { return mediaPlayer ! null mediaPlayer.getStatus() MediaPlayer.Status.PLAYING; } public void cleanup() { if (mediaPlayer ! null) { mediaPlayer.dispose(); // 释放底层资源 mediaPlayer null; } if (media ! null) { media null; } } }关键设计点load()方法中media.getDuration()必须在new MediaPlayer(media)之后调用否则返回UNKNOWNseekTo(long millis)做了安全边界检查确保目标时间不超总时长避免MediaPlayer抛IllegalArgumentExceptioncleanup()调用mediaPlayer.dispose()是强制要求不调用会导致MediaPlayer内部线程持续运行即使 UI 关闭也吃 CPUisPlaying()通过mediaPlayer.getStatus()判断而非维护一个布尔变量——因为状态可能被外部如系统音频焦点丢失改变必须以MediaPlayer实际状态为准。4. 常见问题排查5 条真实踩坑记录覆盖从环境配置到播放异常的全链路4.1 现象双击 jar 文件无反应命令行运行报 “Error: JavaFX runtime components are missing”原因JDK 版本低于 11或使用了精简版 JRE如某些国产 JDK 发行版移除了 JavaFX 模块或pom.xml中javafx-maven-plugin未正确配置add-modules参数。解决运行java -version确认 JDK ≥ 11检查JAVA_HOME是否指向完整 JDK非 JRE路径下应存在jmods/javafx.controls.jmod手动执行java --module-path $PATH_TO_JAVAFX_LIBS --add-modules javafx.controls,javafx.media -jar target/musicplayer-1.0-SNAPSHOT.jar测试$PATH_TO_JAVAFX_LIBS为~/.m2/repository/org/openjfx/javafx-controls/17.0.2/等路径若仍失败改用mvn clean javafx:run直接运行该命令会自动注入模块参数。4.2 现象选择 MP3 文件后点击播放进度条不动状态栏显示“正在播放”但无声原因MP3 文件路径含中文或空格file.toURI().toString()生成的 URI 未正确编码如C:/我的音乐/歌名.mp3→file:///C:/我的音乐/歌名.mp3空格和中文未转义。解决修改PlayerController.java中文件选择逻辑对 URI 进行URLEncoder处理String encodedPath URLEncoder.encode(selectedFile.toURI().toString(), StandardCharsets.UTF_8); audioPlayer.load(encodedPath.replace(, %20)); // 替换空格编码注意JavaFXMedia构造器内部会解码但原始 URI 必须符合 RFC 3986 规范否则解析失败静默。4.3 现象拖动进度条后播放位置不准总是跳到开头或结尾附近原因progressSlider的valueProperty监听器在mediaPlayer未就绪ON_READY事件未触发时就尝试seekTo()此时getTotalDurationMillis()返回 0导致计算出的percent为 NaN 或无穷大。解决在bindSliderEvents()中增加就绪判断progressSlider.valueProperty().addListener((obs, oldVal, newVal) - { if (!audioPlayer.isLoaded() || !audioPlayer.isReady()) return; // 新增就绪检查 // ... 原有逻辑 });并在AudioPlayer.java中添加isReady()方法public boolean isReady() { return mediaPlayer ! null mediaPlayer.getStatus() MediaPlayer.Status.READY; }4.4 现象连续快速点击“播放”按钮多次程序崩溃报MediaException: Resource not found原因load()方法未做并发保护多次调用new MediaPlayer(media)会竞争同一Media实例而Media对象不可重用。解决在AudioPlayer.java的load()方法开头加锁private final Object loadLock new Object(); public void load(String mediaPath) { synchronized (loadLock) { if (mediaPlayer ! null) { mediaPlayer.dispose(); mediaPlayer null; } if (media ! null) { media null; } // ... 原有加载逻辑 } }同时在PlayerController.java的playBtn事件中先调用audioPlayer.stop()再load()避免残留状态。4.5 现象调整音量 Slider 后再次播放新文件音量恢复为最大1.0原因MediaPlayer实例销毁后新实例的volume属性默认为 1.0未继承上一个实例的音量值。解决在AudioPlayer.java中增加音量持久化字段private double lastVolume 0.7; public void setVolume(double volume) { lastVolume volume; if (mediaPlayer ! null) { mediaPlayer.setVolume(volume); } } public double getLastVolume() { return lastVolume; }然后在PlayerController.java的playBtn事件中audioPlayer.load(...)后立即调用audioPlayer.setVolume(audioPlayer.getLastVolume())。5. 二次开发实战3 个高价值改造方向附可直接粘贴的代码段与验证方法5.1 改造一支持播放列表Playlist实现多曲顺序/循环播放这是最常被问到的需求。核心思路是用ObservableListFile管理待播文件用int currentIndex记录当前索引MediaPlayer播放完成时自动切换下一首。步骤 1扩展 AudioPlayer.java增加播放列表支持// 在 AudioPlayer.java 中新增字段 private ObservableListFile playlist FXCollections.observableArrayList(); private int currentIndex 0; // 新增方法设置播放列表 public void setPlaylist(ObservableListFile list) { this.playlist.clear(); this.playlist.addAll(list); this.currentIndex 0; } // 新增方法播放指定索引的歌曲 public void playAtIndex(int index) { if (index 0 index playlist.size()) { this.currentIndex index; File file playlist.get(index); load(file.toURI().toString()); play(); } } // 在 load() 方法末尾添加播放完成监听自动切歌 if (mediaPlayer ! null) { mediaPlayer.setOnEndOfMedia(() - { if (playlist.size() 1) { int nextIndex (currentIndex 1) % playlist.size(); playAtIndex(nextIndex); } }); }步骤 2在 PlayerController.java 中添加播放列表 UI// 在 initButtons() 后添加 private final ListViewString playlistView new ListView(); private final ObservableListString playlistNames FXCollections.observableArrayList(); private void initPlaylist() { playlistView.setItems(playlistNames); playlistView.setPrefHeight(120); playlistView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) - { if (newVal ! null !newVal.isEmpty()) { int index playlistNames.indexOf(newVal); audioPlayer.playAtIndex(index); } }); mainPane.getChildren().add(new Label(播放列表)); mainPane.getChildren().add(playlistView); } // 在 bindButtonEvents() 中playBtn 事件里添加文件后同步更新列表 if (selectedFile ! null) { playlistNames.add(selectedFile.getName()); // ... 原有播放逻辑 }验证方法连续点击“播放”按钮 3 次选择 3 个不同 MP3查看ListView是否显示 3 个文件名点击列表第二项确认播放器切换到对应歌曲播放完一首后观察是否自动跳到下一首循环模式。5.2 改造二添加歌词同步显示LRC 解析实现时间轴精准匹配LRC 歌词本质是带时间戳的文本格式如[00:12.34]今天天气真好。JavaFX 的Text控件配合Timeline即可实现滚动高亮。步骤在 PlayerController.java 中新增歌词面板private final Text lyricText new Text(); private final MapLong, String lrcMap new HashMap(); // 时间戳毫秒→ 歌词 private void initLyricPanel() { lyricText.setFont(Font.font(Microsoft YaHei, FontWeight.BOLD, 16)); lyricText.setFill(Color.web(#333)); lyricText.setTextOrigin(VPos.TOP); lyricText.wrappingWidthProperty().bind(mainPane.widthProperty().subtract(30)); mainPane.getChildren().add(new Label(同步歌词)); mainPane.getChildren().add(lyricText); } // 新增方法解析 LRC 文件假设同名 .lrc 文件存在 public void loadLrc(File audioFile) { String lrcPath audioFile.getAbsolutePath().replace(.mp3, .lrc).replace(.wav, .lrc); File lrcFile new File(lrcPath); if (!lrcFile.exists()) return; try (BufferedReader reader new BufferedReader(new FileReader(lrcFile))) { String line; while ((line reader.readLine()) ! null) { // 匹配 [mm:ss.xx] 格式 Pattern pattern Pattern.compile(\\[(\\d{2}):(\\d{2})\\.(\\d{2})\\](.*)); Matcher matcher pattern.matcher(line); if (matcher.find()) { int min Integer.parseInt(matcher.group(1)); int sec Integer.parseInt(matcher.group(2)); int centi Integer.parseInt(matcher.group(3)); long millis (min * 60 sec) * 1000 centi * 10; String lyric matcher.group(4).trim(); lrcMap.put(millis, lyric); } } } catch (IOException e) { e.printStackTrace(); } } // 在 bindPlayerStateListener() 的 Timeline 中添加歌词更新逻辑 Timeline timeline new Timeline(new KeyFrame(Duration.millis(500), e - { // ... 原有进度更新逻辑 if (!lrcMap.isEmpty()) { long currentTime (long) audioPlayer.getCurrentTimeMillis(); // 找到当前时间最近的歌词向前找 Long closestTime lrcMap.keySet().stream() .filter(t - t currentTime) .max(Long::compare) .orElse(null); if (closestTime ! null) { lyricText.setText(lrcMap.get(closestTime)); } } }));验证方法准备一个song.mp3和同目录song.lrc内容含至少 3 行带时间戳歌词播放song.mp3观察歌词是否随播放进度逐行出现拖动进度条确认歌词是否跳转到对应时间点的句子。5.3 改造三导出为自包含 exeWindows或 appmacOS摆脱 Java 环境依赖学生交作业或老师演示时最怕对方电脑没装 JDK。用jpackage工具可打包成原生应用内嵌 JRE。步骤修改 pom.xml启用 jpackage 插件plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration archive manifest mainClasscom.example.musicplayer.MainApp/mainClass /manifest /archive /configuration /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration source11/source target11/target /configuration /plugin !-- 新增 jpackage 插件 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jlink-plugin/artifactId version3.1.0/version configuration noHeaderFilestrue/noHeaderFiles noManPagestrue/noManPages stripDebugtrue/stripDebug compress2/compress includeLocaleszh/includeLocales launcher nameMusicPlayer/name outputDirectory${project.build.directory}/jlinked/outputDirectory /launcher /configuration executions execution idcreate-jlink-image/id phasepackage/phase goals goaljlink/goal /goals /execution /executions /plugin然后执行# 先构建 jlink 镜像 mvn clean package # 再用 jpackage 打包需 JDK 14 jpackage --input target/jlinked/ --name MusicPlayer \ --main-class com.example.musicplayer.MainApp \ --main-jar musicplayer-1.0-SNAPSHOT.jar \ --type app-image \ --win-console \ --icon src/main/resources/icons/app.ico # Windows 需准备 ico 文件生成的target/MusicPlayer目录下即为可执行文件夹双击MusicPlayer.exe即可运行无需用户安装 JDK。从那以后我每次给学生讲 JavaFX 项目都会先让他们跑通这个播放器再带着他们一行行改出播放列表——因为只有亲手拖动 Slider 看到进度条实时跳动才会真正相信“GUI 编程不是玄学”。而当你第一次把打包好的.exe发给没装 JDK 的同学看他双击就弹出播放器界面时那种踏实感比任何框架文档都来得真切。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/2 11:36:08

AI辅助科研数据分析:从数据清洗到可视化洞察

1. 项目概述:当科研数据分析遇上AI辅助工具第一次看到"虎贲等考AI"这个工具名称时,我正被一堆电镜图像数据折磨得焦头烂额。作为实验室里最年轻的成员,导师扔给我一组三年的实验数据说:"下周组会做个初步分析"…

2026/11/2 11:36:08

岚图汽车IPO:央企新能源转型的武汉模式

1. 岚图上市:央企背景下的新能源汽车突围战 2023年12月,岚图汽车正式登陆香港交易所,成为国内首个实现IPO的央企高端新能源汽车品牌。这个由东风汽车集团孵化而来的新能源项目,从2018年立项到成功上市仅用5年时间,其发…

2026/11/2 11:36:08

PHP+Swoole+Redis实现高可靠延时消息队列

简介:本资源是一套基于PHPSwooleRedis实现的高性能延时消息队列消费系统,面向中高级PHP后端开发者及高并发场景下的消息中间件实践者,解决传统单进程轮询延迟高、吞吐低、资源占用大等痛点。系统采用Redis Sorted Set存储带时间戳的消息&…

2026/11/2 12:41:47

Codex与TencentDB Agent Memory集成的架构级冲突解析

1. 这不是“加个缓存”就能解决的问题:Codex 接入 TencentDB Agent Memory 的真实战场你有没有试过把 Codex 当成一个“智能胶水”,想把它粘在已有的 TencentDB Agent Memory 模块上,结果刚跑起来就报错——cc switch local proxy failed whi…

2026/11/2 12:41:47

Linux下亲手搭建可调试Ceph集群实战指南

1. 为什么现在还要亲手搭一个Ceph集群?——从“云厂商一键部署”反推底层价值很多人看到“Linux搭建Ceph集群”这个标题的第一反应是:都2024年了,还手搭?直接开个云厂商对象存储不香吗?我试过三家主流云平台的对象服务…

2026/11/2 12:41:47

Delphi 12 FMX ListView 复用机制与性能优化避坑指南

简介:Delphi 12 FMXUI 跨平台界面组件库的预览版源代码包,面向已具备 Delphi 与 FireMonkey 基础、希望深入掌握 ListView 的开发者,可用于搭建 Windows、macOS、iOS、Android 及 Linux 多端界面,并解决列表展示、数据绑定、交互定…

2026/11/2 12:41:47

HBase分布式数据库安装与核心操作指南

1. HBase简介与核心特性HBase是一个开源的、分布式的、面向列的NoSQL数据库,它构建在Hadoop文件系统(HDFS)之上,专为处理海量结构化数据而设计。作为Google BigTable的开源实现,HBase在实时读写访问大规模数据集方面表…

2026/11/2 12:41:47

合成复用原则:组合优于继承的软件设计实践

1. 合成复用原则的本质解析在软件工程领域,合成复用原则(Composite Reuse Principle)是面向对象设计的七大原则之一,它直指代码复用这一核心命题。我第一次真正理解这个原则的价值,是在维护一个使用深度继承体系的遗留…

2026/11/2 12:36:47

PSCAD启动编译运行故障排查:配置、DLL、端口与权限

简介:《PSCAD-解决启动、编译和运行方面的问题》是一份面向电力系统仿真工程师、科研人员及高校师生的故障排查文档,聚焦PSCAD X4与V5使用中常见的启动失败、编译中断和运行异常。内容按问题类型组织,涵盖“根元素缺失”、启动时间过长、脚本…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业级SPI隔离设计:硬件抗扰与信号完整性实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

PLFM_RADAR:主动探测数据通道,构建低误报的数据感知系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业SPI隔离设计:MAX14431与STM32F746硬件抗干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑