技术栈与第三方依赖
本页系统梳理 Ark-Pets 桌面宠物项目的技术栈构成:Gradle 多模块构建体系、运行时 JDK 模块裁剪(jlink)、渲染着色器资源、JavaFX 桌面 UI(FXML/CSS)、Sentry 错误上报集成,以及跨平台安装包分发管线(distJar → jlink → jpackage → Inno Setup)。
目的与范围
本页覆盖:
- 仓库的构建体系与模块划分(
core/build.gradle、desktop/build.gradle) - 运行时技术栈:JDK 模块列表、JVM 启动参数、平台差异处理(macOS
-XstartOnFirstThread) - 渲染层资源:
assets/shaders/下的 GLSL 着色器源码 - 桌面 UI 层技术:
assets/UI/下的 FXML 布局与 CSS 样式 - 静态资源:字体(思源黑体)、多平台图标、默认配置文件
- 可观测性:Sentry DSN 的注入方式
- 分发管线:Fat JAR 打包、定制 JRE、jpackage 安装包、Windows Inno Setup 脚本
留给兄弟页面的内容:
- 各功能模块的具体实现逻辑(桌面宠物渲染循环、行为状态机、模型加载)不在本页展开
assets/ArkPetsConfigDefault.json中各配置项的业务语义由配置相关页面说明- 具体某个第三方库的调用代码分析,见对应功能实现页
概述
Ark-Pets 是一个基于 Java + Gradle 构建的跨平台桌面应用,其技术选型可以概括为四个层面:
| 层面 | 技术 | 证据来源 |
|---|---|---|
| 构建体系 | Gradle 多模块工程(core + desktop) | desktop/build.gradle 引用 project.appName、project.version 等根项目属性 |
| 运行时 | JDK(构建时通过 ${javaHome} 定位),经 jlink 裁剪为定制 JRE | desktop/build.gradle 的 jlink 任务 |
| 渲染层 | GLSL 着色器(Plain* / Complex* 四套 + 片段低配版) | assets/shaders/ 目录下 5 个 .glsl 文件 |
| 桌面 UI | JavaFX 声明式布局(FXML)+ CSS 样式 | assets/UI/ 下 7 个 .fxml 文件与 Main.css |
设计意图上,项目将渲染核心(core)与桌面启动器(desktop)分离:desktop 模块持有入口类 cn.harryh.arkpets.DesktopLauncher,并以 ../assets 作为资源目录与运行工作目录;这种拆分让核心逻辑与平台相关的启动/分发逻辑解耦。
架构总览
图解要点:
- core → desktop 依赖方向:desktop 启动器依赖 core 核心模块,两者的构建脚本分别为
core/build.gradle与desktop/build.gradle。 - 资源不进入源码目录:desktop 模块通过
sourceSets.main.resources.srcDirs = ["../assets"]把仓库级assets目录声明为资源根,使着色器、UI 布局、字体等资源与 Java 源码解耦。 - 分发任务链:
distJar(打 Fat JAR)→jlink(定制 JRE)→ jpackage 安装包目录,Windows 侧另有 Inno Setup 脚本docs/scripts/ExePacking.iss参与最终安装包制作。
构建体系与模块划分
编码与源码布局
desktop 模块脚本的第一行就固定了全仓库的字符编码,这是中文桌面应用最容易踩的坑之一:
[compileJava, compileTestJava]*.options*.encoding = "UTF-8"
sourceSets.main.java.srcDirs = ["src/"]
sourceSets.main.resources.srcDirs = ["../assets"]Source: desktop/build.gradle
三个声明的意图:
- UTF-8 强制编码:编译期显式指定编码,保证含中文的资源与源码在任何开发机Locale下字节一致,避免 Windows 默认 GBK 导致的乱码。
- 源码目录扁平化:
src/而非 Gradle 约定的src/main/java,属于沿用 libGDX 风格工程布局的做法。 - 资源指向仓库根的
../assets:着色器、FXML、字体等以共享资源目录形式存在于模块之外,core 与 desktop 均可引用。
脚本随后定义了模块级扩展属性与 Eclipse 工程命名:
project.ext.mainClassName = "cn.harryh.arkpets.DesktopLauncher"
project.ext.assetsDir = new File("../assets")
eclipse.project.name = appName + "-desktop"Source: desktop/build.gradle
appName 与 project.version 未在本文件定义,说明它们来自 Gradle 根工程配置——这是判定本项目为多模块工程的直接证据。mainClassName 指向 cn.harryh.arkpets.DesktopLauncher,即全应用唯一入口类。
资源打包策略
发布产物并不打包全部资源,processResources 显式排除模型与日志:
1processResources {
2 includeEmptyDirs = false
3 excludes = [
4 "**/models_enemies/**",
5 "**/models/**",
6 "**/logs/**",
7 "models_data.json"
8 ]
9}Source: desktop/build.gradle
设计意图:模型资产(models/、models_enemies/、models_data.json)体积大且可由用户后续下载/管理,日志目录(logs/)属于运行时产物,都不应进入发行 JAR——这直接控制了分发包体积。includeEmptyDirs = false 进一步避免空目录膨胀归档条目。模型下载相关的实现细节属于兄弟页面范围。
运行时技术栈
开发期运行任务与平台差异
run 与 debug 两个任务是开发调试的主入口,它们的 JVM 参数组装逻辑揭示了三项关键技术约束:
1// Runs the app without debug.
2tasks.register("run", JavaExec) {
3 dependsOn = ["classes"]
4 group = "execute"
5
6 mainClass = mainClassName
7 classpath = sourceSets.main.runtimeClasspath
8 standardInput = System.in
9 workingDir = assetsDir
10
11 setIgnoreExitValue(true)
12
13 jvmArgs += "-Dfile.encoding=UTF-8"
14 if (sentryDsn)
15 jvmArgs += "-Dsentry.dsn=${sentryDsn}"
16
17 if (OperatingSystem.current() == OperatingSystem.MAC_OS) {
18 jvmArgs += "-XstartOnFirstThread" // Required to run on macOS
19 }
20}Source: desktop/build.gradle
逐行解读:
workingDir = assetsDir:应用以assets/为工作目录启动,因此代码中的相对路径资源引用(着色器、字体、配置)均相对该目录解析。-Dfile.encoding=UTF-8:与编译期 UTF-8 声明呼应,保证运行时字符串处理一致。-Dsentry.dsn=...:条件注入 Sentry 数据源名(见下文"可观测性"小节)。-XstartOnFirstThread(仅 macOS):macOS 平台强制在主线程启动 JVM。这是 GLFW/LWJGL 系桌面渲染后端在 macOS 上的硬性要求(macOS 的 Cocoa 事件循环必须在主线程),注释// Required to run on macOS明确标注了这一约束。setIgnoreExitValue(true):进程退出码不使 Gradle 构建失败——桌面应用由用户手动关闭,非零退出是常态。
平台分支使用了 org.gradle.internal.os.OperatingSystem 做运行时探测,判断逻辑可图示为:
debug 任务(第 44–60 行)与 run 完全同构,仅多出 setDebug(true) 以挂起等待调试器连接,此处不再重复摘录。
JDK 模块裁剪(jlink)
分发产物使用 jlink 生成裁剪版 JRE,模块清单集中定义在 ext 块:
1ext {
2 // Environment vars
3 rootDir = project.rootDir
4 javaHome = System.getProperty('java.home')
5 osName = System.getProperty('os.name').toLowerCase(Locale.ROOT).split(' ')[0]
6 osPathSep = File.pathSeparatorChar
7 // Distribution related vars
8 jarLibDir = layout.buildDirectory.dir("libs").get().toString()
9 jarLibName = project.name + "-" + project.version
10 jlinkDir = layout.buildDirectory.dir("jlink").get().toString()
11 jlinkModuleList = "java.base,java.desktop,java.logging,java.management,java.scripting,jdk.crypto.ec,jdk.localedata,jdk.management,jdk.unsupported"
12 jlinkLocalesList = "en-US,zh-CN"
13 jpackageDir = layout.buildDirectory.dir("jpackage").get().toString()
14 issFileRel = "docs/scripts/ExePacking.iss"
15 distDir = layout.buildDirectory.dir("dist").get().toString()
16 distName = project.appName + "-v" + project.version
17}Source: desktop/build.gradle
jlinkModuleList 是理解运行时依赖面的关键,各模块职责如下:
| JDK 模块 | 在本项目中承担的角色 |
|---|---|
java.base | JVM 基础模块,任何运行时必需 |
java.desktop | 桌面 UI 相关基础类(AWT/Swing 及桌面集成能力) |
java.logging | JUL 日志桥接,供依赖库输出日志 |
java.management | JMX 管理接口(Sentry 等诊断类库常用) |
java.scripting | 脚本引擎 API |
jdk.crypto.ec | 椭圆曲线加密,HTTPS 通信(如 Sentry 上报、资源下载)所需 |
jdk.localedata | 本地化数据(配合 jlinkLocalesList = "en-US,zh-CN" 仅保留中英文) |
jdk.management | JVM 运行时管理扩展 |
jdk.unsupported | sun.misc.* 等内部 API,本地渲染后端(LWJGL 系)常用 |
设计意图:jdk.crypto.ec + jdk.localedata(en-US,zh-CN) 的组合表明发行包需要 HTTPS 网络能力且只面向中英文用户;jdk.unsupported 的保留则说明存在依赖内部 API 的本地库。模块白名单 + 语言白名单双重裁剪,把定制 JRE 压到最小可用集合。
jlink 任务的调用方式:
1// Creates a customized Java Runtime Environment for the app.
2tasks.register("jlink", Exec) {
3 dependsOn = ["distJar"]
4 group = "dist"
5
6 doFirst() { delete jlinkDir }
7
8 workingDir project.projectDir
9 inputs.property("runtime", "${jlinkDir}/runtime")
10 commandLine = [
11 "${javaHome}/bin/jlink",
12 '--module-path', "${javaHome}/jmods",
13 '--add-modules', jlinkModuleList,
14 '--output', "${jlinkDir}/runtime",Source: desktop/build.gradle
(该任务的 commandLine 参数列表在源文件中延续至第 120 行之后,此处摘录到已验证部分。)注意 doFirst() { delete jlinkDir }——每次构建先清空输出目录,避免增量残留导致 jlink 报"目录已存在"错误。
渲染层:GLSL 着色器资源
assets/shaders/ 目录包含 5 个着色器源文件,构成两套可切换的渲染管线:
| 文件 | 角色 |
|---|---|
PlainVertex.glsl / PlainFragment.glsl | Plain(基础)管线的顶点/片段着色器 |
ComplexVertex.glsl / ComplexFragment.glsl | Complex(增强)管线的顶点/片段着色器 |
ComplexFragmentLow.glsl | Complex 管线的低配片段着色器(性能降级替代) |
设计意图:双管线 + 低配备选的命名结构,表明渲染层针对不同机器性能提供了"基础/增强/增强低配"三档策略——Complex* 管线的片段着色器单独存在低配版本,而顶点着色器共用一个 ComplexVertex.glsl,说明降级只发生在光栅化阶段而非几何阶段。这与 Spine 系骨骼动画渲染常见的"普通混合 / 复合混合(预乘 alpha、色调叠加)"分层一致。
桌面 UI 层:JavaFX FXML 与 CSS
assets/UI/ 目录包含 8 个资源文件,全部属于 JavaFX 声明式 UI 技术栈:
| 文件 | 推断的 UI 职责 |
|---|---|
RootModule.fxml | 主窗口根布局容器 |
ModelsModule.fxml | 模型管理页(选择/导入桌宠模型) |
BehaviorModule.fxml | 行为配置页(动画/交互行为开关) |
SettingsModule.fxml | 设置页 |
DownloadDialog.fxml | 模型下载对话框 |
LogDialog.fxml | 日志查看对话框 |
AnnounceDialog.fxml | 公告对话框 |
Main.css | 全局样式表(JavaFX CSS 方言) |
技术选型意图:与渲染层使用 GLSL(OpenGL 系、代码式 UI 无关)相对,管理后台 UI 采用 JavaFX FXML 声明式布局——这与 JDK 模块清单中的 java.desktop 相互印证。多个 *Dialog.fxml 对话框的存在说明应用将"下载、日志、公告"等长耗时/低频操作统一收纳为模态窗口,而非嵌入主界面。
静态资源与配置
- 字体:
assets/fonts/SourceHanSansCN-Bold.otf与SourceHanSansCN-Regular.otf(思源黑体中文 Bold/Regular 两字重),随包内置保证无字体环境下中文渲染一致。 - 图标:
assets/icons/同时提供icon.ico(Windows)、icon.icns(macOS)、icon.png(Linux/通用),与三平台 jpackage 分发目标一一对应。 - 默认配置:
assets/ArkPetsConfigDefault.json为默认配置模板,应用启动时加载;各配置项业务语义由配置相关兄弟页面说明。
可观测性:Sentry 集成
构建脚本通过 Gradle 属性注入 Sentry DSN,实现"构建时配置、运行时上报":
def sentryDsn = (project.findProperty("SENTRY_DSN") ?: "").trim()Source: desktop/build.gradle
随后在 run/debug 任务的 JVM 参数中条件追加:
jvmArgs += "-Dfile.encoding=UTF-8"
if (sentryDsn)
jvmArgs += "-Dsentry.dsn=${sentryDsn}"Source: desktop/build.gradle
设计意图:
- DSN 不入库:
findProperty("SENTRY_DSN")从命令行(-PSENTRY_DSN=...)或本地 gradle.properties 读取,避免将上报端点硬编码进开源仓库,防止上游仓库被无关错误数据淹没。 - 未配置则零开销:DSN 为空字符串时不追加 JVM 参数,Sentry 处于未初始化/禁用态——普通开发者本地构建完全不受影响。
java.management模块入列:与 Sentry 采集 JVM/运行环境指标的常规需求相符(见前文 JDK 模块表)。
分发管线
distJar 任务生成自包含的 Fat JAR,将运行时依赖整体展开打进单一归档:
1tasks.register("distJar", Jar) {
2 dependsOn = ["classes"]
3 group = "dist"
4
5 doLast() {
6 copy {
7 from "${jarLibDir}/${jarLibName}.jar"
8 into distDir
9 rename "${jarLibName}.jar", "${distName}.jar"
10 }
11 }
12
13 duplicatesStrategy = DuplicatesStrategy.EXCLUDE
14 manifest {
15 attributes 'Main-Class': mainClassName
16 }
17 dependsOn configurations.runtimeClasspath
18 from {
19 configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
20 }
21 with jar
22}Source: desktop/build.gradle
关键点:
Main-Class清单属性指向DesktopLauncher,使产物java -jar可直接启动。zipTree展开依赖:所有 runtimeClasspath 依赖被解压合并进 JAR——这是典型的 Uber-JAR 策略,省去安装包内维护依赖目录的复杂度。DuplicatesStrategy.EXCLUDE:多个依赖 JAR 含同名资源文件(如META-INF/services/*、module-info.class)时静默保留首个,防止打包因重复条目失败。注意该策略可能丢失 SPI 服务声明,这是 Uber-JAR 的已知代价。doLast重命名拷贝:把desktop-{version}.jar复制为dist/{AppName}-v{version}.jar,统一发行命名。
完整分发流水线
流水线各阶段的依赖链由 dependsOn 显式声明:jlink 任务 dependsOn = ["distJar"],即裁剪 JRE 之前必须先产出可执行 JAR;jpackageDir 与 issFileRel = "docs/scripts/ExePacking.iss" 表明 Windows 安装包由 Inno Setup 脚本做最终封装(该脚本位于 docs/scripts/ 下,不在本次已读文件范围内,安装器配置细节属分发/打包相关页面)。
失败模式与边界情况
基于已验证的构建脚本,本技术栈存在以下值得注意的边界:
- 编码不一致风险:编译期与运行期均强制 UTF-8(
#L1、#L35),但若第三方工具链(如 IDE 或外部打包脚本)绕过 Gradle,中文资源可能乱码——这是该双保险存在的原因。 - jlink 增量残留:
jlink任务通过doFirst() { delete jlinkDir }规避输出目录已存在导致的失败;手工遗留的build/jlink目录不会阻塞构建。 - Uber-JAR 的 SPI/模块元数据丢失:
duplicatesStrategy = DuplicatesStrategy.EXCLUDE遇到多个依赖携带同名META-INF文件时只保留首个,理论上可能破坏 ServiceLoader 发现;同时自动模块名(Automatic-Module-Module)冲突也在被排除之列。 - DSN 未配置时无错误上报:本地默认构建不传
-PSENTRY_DSN,Sentry 不生效——排查用户侧问题需确认发布构建是否注入了 DSN。 - macOS 线程约束:忘加
-XstartOnFirstThread(例如用户绕过gradle run直接java -jar)会导致 macOS 上渲染窗口无法创建/崩溃,属平台硬性约束。 - 资源排除的副作用:
processResources排除了models/等目录,意味着发行包内不存在模型数据,首次使用依赖下载流程(见模型下载相关页面)。
扩展点
- 新增 JDK 依赖模块:修改
jlinkModuleList(desktop/build.gradle#L75)即可扩展运行时能力,例如需要 Nashorn 之外的脚本引擎或jdk.zipfs时追加模块名。 - 新增分发语言:
jlinkLocalesList = "en-US,zh-CN"(#L76)控制裁剪后的 locale 集合,新增界面语言需同步追加。 - 切换/新增构建渠道密钥:遵循
sentryDsn的findProperty模式,用 Gradle 属性注入而非硬编码。 - 调整资源打包范围:
processResources.excludes(#L13-L18)是控制发行体积的唯一开关,新增随包资源目录时需在此登记。
相关链接
- desktop/build.gradle — 桌面模块构建脚本(运行/调试/分发任务全集)
- core/build.gradle — 核心模块构建脚本(本次预算内未能展开,依赖细节待补充)
- assets/ArkPetsConfigDefault.json — 默认配置模板
- assets/shaders/ — GLSL 着色器源码
- assets/UI/RootModule.fxml — JavaFX 主布局入口
说明:
core/build.gradle的依赖声明部分在本次源码工具预算(6/6)耗尽前未能读取,第三方库的具体坐标与版本清单暂缺,待后续在对应核心模块页面补充;本文中关于 core 模块的所有论断均仅基于其在仓库中的存在与 desktop 模块对根工程属性(appName、version)的共享使用。