Skip to content

🔄 Updater 架构 ​

架构 自更新

自更新子系统让 ClassyShark 查询 GitHub Releases、比对版本并下载新版 jar。UpdateManager 是单例门面,按 isGui 二选一挑 CliDownloader 或 GuiDownloader;两者共享同一套模板方法骨架与 Retrofit 网络栈。

整体流程 ​

mermaid
flowchart TD
    A["Main / GuiMode"] --> B["UpdateManager.getInstance()<br/>checkVersionConsole / checkVersionGui"]
    B --> C{"isGui ?"}
    C -- true --> D["GuiDownloader.getInstance()"]
    C -- false --> E["CliDownloader.getInstance()"]
    D & E --> F["checkNewVersion()<br/>call.enqueue(this)"]
    F --> G["Retrofit + Gson<br/>GET repos/google/android-classyshark/releases/latest"]
    G --> H["AbstractReleaseCallback.onResponse"]
    H --> I["onReleaseReceived(release)"]
    I --> J{"release.isNewerThan(current)?"}
    J -- 否 --> STOP["不动作"]
    J -- 是 --> K["新 Thread: warnAboutNew(release)?"]
    K -- CLI: Scanner 读 y/N --> L["CliDownloader"]
    K -- GUI: 恒 true --> L
    L -- 同意 --> M["FileUtils.downloadFileFrom(release)"]
    M --> N["Cli: 打印路径 / Gui: MessageRunnable 弹 changelog"]

UpdateManager:单例门面 ​

UpdateManager 静态持有唯一实例(private static final UpdateManager instance = new UpdateManager()),构造器私有。对外暴露两个语义化方法,内部收敛到一个布尔参:

方法调用方isGui委托下载器
checkVersionConsole()CliMode -updatefalseCliDownloader.getInstance()
checkVersionGui()GuiMode.withtrueGuiDownloader.getInstance()

🎯 门面只做"选下载器"一件事,下载器自身持有版本比较、询问、下载的全部逻辑。

网络层:Retrofit + Gson ​

NetworkManager 惰性构建 Retrofit(baseUrl(GitHubApi.ENDPOINT) + GsonConverterFactory),GitHubApi 是唯一接口,一个 GET 注解声明远端:

java
@GET("repos/google/android-classyshark/releases/latest")
Call<Release> getLatestRelease();

ENDPOINT = "https://api.github.com/" 是接口内常量;解析出的 JSON 由 Gson 反序列化进 Release 模型。

Release 模型 ​

Release 只保留用到的字段,@SerializedName 把下划线 JSON 名映射到 Java 字段:

字段JSON 名用途
namenamemajor.minor 版本串,如 9.0
preReleaseprerelease预发布标记(当前未使用)
bodybodychangelog
assetsassets下载资源数组
createdAtcreated_at发布时间,用于文件名时间戳

三个关键方法:

  • isNewerThan(Release) — 把 name 按 . 切分,先比 major 再比 minor(无 patch 维度,走 major > || (major == && minor >))。
  • getDownloadURL() — assets.length > 0 时取 assets[0] 的 browser_download_url(ReleaseDownloadData 字段经 @SerializedName 映射),否则返回 null。
  • toString() — 输出 REL:\t<name>\nCHANGELOG:\n<body>,直接喂给 CLI 的询问文案。

AbstractDownloader:模板方法 ​

AbstractDownloader 继承 AbstractReleaseCallback(后者实现 Retrofit Callback<Release>,把 onResponse 解包成 onReleaseReceived(body)、失败时 System.err.println),构成模板方法骨架:

java
public void checkNewVersion() {
    Call<Release> call = NetworkManager.getGitHubApi().getLatestRelease();
    call.enqueue(this);                      // 异步,非阻塞
}

public void onReleaseReceived(final Release release) {
    if (release.isNewerThan(current)) {
        new Thread(() -> {
            if (warnAboutNew(release)) {     // 钩子,子类决定"是否询问"
                obtainNew(release);          // 下载
            }
        }).start();
    }
}

模板流程:enqueue 异步 → 回调解包 → 版本比较 → 开新线程 → warnAboutNew(抽象钩子)→ obtainNew → FileUtils.downloadFileFrom → onReleaseDownloaded(第二个抽象钩子,通知产物)。两个钩子正是 CLI 与 GUI 的差异点。

⚠️ onReleaseReceived 的 warnAboutNew 调用包在裸 new Thread 里,CLI 的 Scanner 阻塞读不会卡住 EDT;但 GUI 的 warnAboutNew 恒 true,下载完成后才经 invokeLater 切回 EDT。

CliDownloader vs GuiDownloader ​

维度CliDownloaderGuiDownloader
warnAboutNew打印版本+changelog,Scanner.next() 读 y/N恒 true,直接下载
onReleaseDownloaded打印"已下载到 <绝对路径>"SwingUtilities.invokeLater(new MessageRunnable(name, changelog)) 弹窗
实例同为 getInstance() 单例同为 getInstance() 单例

💡 MessageRunnable 是 Runnable 实现,在 EDT 上弹 JOptionPane 展示新版本名与 changelog——GUI 路径的"线程安全"就靠这一跳。

FileUtils / NamingUtils:幂等下载 ​

FileUtils 的 downloadFileFrom(release) 是幂等的:目标文件已存在则跳过下载直接返回。真实下载 obtainNewJarFrom 用 NIO 通道一次搬运:

java
ReadableByteChannel rbc = Channels.newChannel(url.openStream());
FileOutputStream fos = new FileOutputStream(file);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);

NamingUtils 生成目标文件名:buildNameFrom 取 createdAt 的 T 前日期,拼成 ClassyShark_<YYYY-MM-DD>.jar,落在 extractCurrentPath()(当前工作目录绝对路径)下。

工具职责幂等点
FileUtils.downloadFileFrom下载 + 幂等判断file.exists() 命中即返回
NamingUtils.buildNameFrom生成 ClassyShark_<date>.jar 路径同名文件天然去重
FileUtils.overwriteOld用新版替换当前 jarREPLACE_EXISTING(未接入流程)

📁 overwriteOld 已实现但当前未调用,替换旧 jar 的"原地升级"仍是预留能力。

设计要点 ​

  • 🧩 单例门面 — UpdateManager 全进程唯一,两个下载器也各自单例。
  • 🏭 模板方法 — 骨架固定,warnAboutNew / onReleaseDownloaded 两个钩子决定交互差异。
  • 🔁 幂等下载 — 文件名含发布日期 + 存在性判断,重复检查零成本。
  • 🧵 异步与线程安全 — Retrofit enqueue 非阻塞;CLI 用新线程隔离 Scanner,GUI 用 invokeLater 回 EDT。
  • 🤐 静默失败 — 网络失败仅 stderr 一行,升级失败不影响主程序。

进一步阅读 ​

最后更新:

基于 Apache 2.0 协议发布