第16章:SWT 应用的打包与部署

第16章:SWT 应用的打包与部署


作者:步子哥 (steper@foxmail.com)


16.1 使用 Eclipse Export Wizard

导出为可执行 JAR

步骤:
1. 在 Eclipse 中,右键点击项目 → Export...
2. 选择 "Runnable JAR file" → Next
3. 选择 "Launch configuration"(如果有主类)
4. 选择 "Export destination"(JAR 文件路径)
5. 选择 "Library handling"(库处理方式)
   - Extract required libraries into generated JAR:提取库到 JAR(推荐)
   - Package required libraries into generated JAR:打包库到 JAR
6. 点击 Finish

费曼解释:导出为 JAR 的原理

导出为 JAR = 打包行李
- 把所有衣服、鞋子、化妆品(代码、依赖库)
- 放进一个大箱子(JAR 文件)
- 方便携带(分发)

类比:
导出为 JAR = 快递打包
- 把所有商品(代码、依赖库)
- 放进一个大箱子(JAR 文件)
- 方便快递(分发)

包含依赖项

库处理方式:
1. Extract required libraries into generated JAR(推荐)
   - 把所有依赖库解压到 JAR 中
   - 优点:只有一个 JAR 文件,分发简单
   - 缺点:JAR 文件较大

2. Package required libraries into generated JAR
   - 把所有依赖库打包到 JAR 中
   - 优点:保持依赖库的结构
   - 缺点:JAR 文件较大

3. Copy required libraries into a sub-folder next to the generated JAR
   - 把所有依赖库复制到子文件夹
   - 优点:JAR 文件较小
   - 缺点:需要一起分发子文件夹

费曼解释:不同库处理方式的区别

方式 1:打包所有(Extract)
- 所有东西都放进一个大箱子(JAR)
- 优点:只有一个箱子,好携带
- 缺点:箱子很大

方式 2:分别打包(Package)
- 每样东西都放进小箱子(依赖库)
- 优点:保持结构
- 缺点:箱子很大

方式 3:分开携带(Copy)
- 大箱子只放主要东西(JAR)
- 小箱子放其他东西(依赖库)
- 优点:大箱子小
- 缺点:需要一起分发

类比:
方式 1:打包所有
- 把所有衣服、鞋子、化妆品放进一个大旅行箱
- 优点:只有一个箱子,好携带
- 缺点:箱子很大

方式 2:分别打包
- 每样衣服、鞋子、化妆品都放进小袋子
- 优点:保持结构
- 缺点:袋子很多

方式 3:分开携带
- 大箱子只放主要衣服
- 小箱子放鞋子、化妆品
- 优点:大箱子小
- 缺点:需要一起携带

平台特定的依赖处理

问题:
- SWT 有平台特定的依赖(Windows、Mac、Linux)
- 只打包当前平台的依赖,其他平台无法运行

解决方案:
- 打包所有平台的依赖
- 或者:分别为每个平台打包

方式 1:打包所有平台(推荐)
- 包含 Windows、Mac、Linux 的原生库
- 优点:一个 JAR,跨平台
- 缺点:JAR 文件很大

方式 2:分别打包
- Windows:打包 Windows 原生库
- Mac:打包 Mac 原生库
- Linux:打包 Linux 原生库
- 优点:JAR 文件小
- 缺点:需要分发多个版本

费曼解释:平台特定的依赖处理

打包所有平台 = 多语言说明书
- 说明书包含英语、中文、西班牙语
- 优点:一本说明书,满足所有语言用户
- 缺点:说明书很厚

分别打包 = 单语言说明书
- 英语说明书:英语
- 中文说明书:中文
- 西班牙语说明书:西班牙语
- 优点:说明书很薄
- 缺点:需要分发多本

16.2 使用 Maven Shade 插件

构建 fat jar

<!-- pom.xml -->
<project>
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.example</groupId>
    <artifactId>my-swt-app</artifactId>
    <version>1.0.0</version>
    
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    
    <dependencies>
        <!-- SWT 核心库 -->
        <dependency>
            <groupId>org.eclipse.platform</groupId>
            <artifactId>org.eclipse.swt</artifactId>
            <version>3.124.200</version>
        </dependency>
        
        <!-- Windows 原生库 -->
        <dependency>
            <groupId>org.eclipse.platform</groupId>
            <artifactId>org.eclipse.swt.win32.win32.x86_64</artifactId>
            <version>3.124.200</version>
        </dependency>
        
        <!-- Mac 原生库 -->
        <dependency>
            <groupId>org.eclipse.platform</groupId>
            <artifactId>org.eclipse.swt.cocoa.macosx.x86_64</artifactId>
            <version>3.124.200</version>
        </dependency>
        
        <!-- Linux 原生库 -->
        <dependency>
            <groupId>org.eclipse.platform</groupId>
            <artifactId>org.eclipse.swt.gtk.linux.x86_64</artifactId>
            <version>3.124.200</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!-- Maven Shade 插件:打包 fat jar -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.4.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <!-- 主类 -->
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.example.Main</mainClass>
                                </transformer>
                            </transformers>
                            
                            <!-- 过滤签名文件 -->
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

费曼解释:Maven Shade 插件的作用

Maven Shade 插件 = 专业打包员
- Eclipse Export = 自己打包(可能不够专业)
- Maven Shade = 专业打包员打包(更专业)

类比:
Maven Shade 插件 = 专业搬家队
- 自己搬家 = 自己把东西搬上车(可能不够专业)
- 专业搬家队 = 专业人员搬上车(更专业)

平台特定的依赖处理

<!-- 使用 Maven Profiles 处理平台特定依赖 -->
<profiles>
    <!-- Windows -->
    <profile>
        <id>windows</id>
        <activation>
            <os>
                <family>windows</family>
            </os>
        </activation>
        <dependencies>
            <dependency>
                <groupId>org.eclipse.platform</groupId>
                <artifactId>org.eclipse.swt.win32.win32.x86_64</artifactId>
                <version>3.124.200</version>
            </dependency>
        </dependencies>
    </profile>
    
    <!-- Mac -->
    <profile>
        <id>mac</id>
        <activation>
            <os>
                <family>mac</family>
            </os>
        </activation>
        <dependencies>
            <dependency>
                <groupId>org.eclipse.platform</groupId>
                <artifactId>org.eclipse.swt.cocoa.macosx.x86_64</artifactId>
                <version>3.124.200</version>
            </dependency>
        </dependencies>
    </profile>
    
    <!-- Linux -->
    <profile>
        <id>linux</id>
        <activation>
            <os>
                <family>linux</family>
            </os>
        </activation>
        <dependencies>
            <dependency>
                <groupId>org.eclipse.platform</groupId>
                <artifactId>org.eclipse.swt.gtk.linux.x86_64</artifactId>
                <version>3.124.200</version>
            </dependency>
        </dependencies>
    </profile>
</profiles>

费曼解释:Maven Profiles 的原理

Maven Profiles = 多个套餐
- Windows 套餐:包含 Windows 依赖
- Mac 套餐:包含 Mac 依赖
- Linux 套餐:包含 Linux 依赖
- Maven 根据当前平台选择对应套餐

类比:
Maven Profiles = 多个餐盒
- 美国餐盒:汉堡、薯条、可乐
- 中式餐盒:米饭、宫保鸡丁、可乐
- 日本餐盒:寿司、味增汤、可乐
- 餐厅根据客人国籍选择对应餐盒

16.3 使用 jpackage(Java 14+)

打包为原生安装程序

# 使用 jpackage 打包为 Windows 安装程序(.exe)
jpackage --name "My App" \
         --type exe \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main

# 使用 jpackage 打包为 Windows 安装程序(.msi)
jpackage --name "My App" \
         --type msi \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main

# 使用 jpackage 打包为 Mac 应用程序(.app)
jpackage --name "My App" \
         --type app-image \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main

# 使用 jpackage 打包为 Mac 安装程序(.dmg)
jpackage --name "My App" \
         --type dmg \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main

# 使用 jpackage 打包为 Linux 安装程序(.deb)
jpackage --name "My App" \
         --type deb \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main

# 使用 jpackage 打包为 Linux 安装程序(.rpm)
jpackage --name "My App" \
         --type rpm \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main

费曼解释:jpackage 的作用

jpackage = 专业打包机
- 打包机把 JAR 文件包装成原生安装程序
- Windows:包装成 .exe 或 .msi
- Mac:包装成 .app 或 .dmg
- Linux:包装成 .deb 或 .rpm
- 用户感觉像原生应用

类比:
jpackage = 礼品包装机
- 礼品(JAR 文件)
- 包装机把礼品包装成礼盒(原生安装程序)
- 不同节日包装不同礼盒(不同平台包装不同安装程序)

Windows:.exe、.msi

# 打包为 Windows .exe
jpackage --name "My App" \
         --type exe \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main \
         --icon icon.ico \
         --vendor "My Company" \
         --app-version 1.0.0 \
         --file-associations "txt:My App Text File"

# 打包为 Windows .msi
jpackage --name "My App" \
         --type msi \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main \
         --icon icon.ico \
         --vendor "My Company" \
         --app-version 1.0.0 \
         --file-associations "txt:My App Text File" \
         --win-menu \
         --win-shortcut

费曼解释:.exe 和 .msi 的区别

.exe = 绿色软件
- 无需安装,双击即可运行
- 类似:压缩包解压后直接运行

.msi = 安装包
- 需要安装程序
- 安装后,可以在"添加/删除程序"中卸载
- 类似:Office 安装包

类比:
.exe = 便携式水杯
- 拿来就用,无需安装

.msi = 固定式水杯
- 需要安装(固定在桌面上)
- 可以卸载(从桌上拿走)

Mac:.app、.dmg

# 打包为 Mac .app
jpackage --name "My App" \
         --type app-image \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main \
         --icon icon.icns \
         --vendor "My Company" \
         --app-version 1.0.0

# 打包为 Mac .dmg
jpackage --name "My App" \
         --type dmg \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main \
         --icon icon.icns \
         --vendor "My Company" \
         --app-version 1.0.0 \
         --mac-package-name "My App"

费曼解释:.app 和 .dmg 的区别

.app = 应用程序
- 双击即可运行
- 类似:Windows 的 .exe

.dmg = 磁盘镜像
- 需要挂载(打开)
- 挂载后,拖拽 .app 到 Applications 文件夹
- 类似:Windows 的 .iso

类比:
.app = Windows 的 .exe
- 双击即可运行

.dmg = Windows 的 .iso
- 需要挂载(解压)
- 挂载后,拖拽应用程序到 Program Files

Linux:.deb、.rpm

# 打包为 Linux .deb
jpackage --name "my-app" \
         --type deb \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main \
         --icon icon.png \
         --vendor "My Company" \
         --app-version 1.0.0 \
         --linux-package-name my-app \
         --linux-deb-maintainer "mycompany@example.com" \
         --linux-menu-group "Development"

# 打包为 Linux .rpm
jpackage --name "my-app" \
         --type rpm \
         --input inputDir \
         --dest outputDir \
         --main-jar my-app.jar \
         --main-class com.example.Main \
         --icon icon.png \
         --vendor "My Company" \
         --app-version 1.0.0 \
         --linux-package-name my-app \
         --linux-rpm-license-type "GPLv3" \
         --linux-menu-group "Development"

费曼解释:.deb 和 .rpm 的区别

.deb = Ubuntu/Debian 的安装包
- 使用 dpkg 或 apt 安装
- 类似:Windows 的 .msi

.rpm = Fedora/RHEL/CentOS 的安装包
- 使用 rpm 或 yum/dnf 安装
- 类似:Windows 的 .msi

类比:
.deb = Ubuntu 的软件商店
- 类似:Windows 的应用商店

.rpm = Fedora 的软件商店
- 类似:Windows 的应用商店

16.4 分发原生库

提取原生库到临时目录

// 提取原生库到临时目录
public class NativeLibraryExtractor {
    public static void extractNativeLibrary(String libraryName, String targetPath) throws Exception {
        // 读取原生库
        String resourcePath = "/native/" + libraryName;
        try (InputStream in = NativeLibraryExtractor.class.getResourceAsStream(resourcePath);
             FileOutputStream out = new FileOutputStream(targetPath)) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        }
    }
    
    public static void loadNativeLibrary(String libraryName) throws Exception {
        String osName = System.getProperty("os.name").toLowerCase();
        String osArch = System.getProperty("os.arch").toLowerCase();
        
        // 构建原生库名
        String fileName;
        if (osName.contains("win")) {
            fileName = libraryName + ".dll";
        } else if (osName.contains("mac")) {
            fileName = "lib" + libraryName + ".jnilib";
        } else if (osName.contains("nux")) {
            if (osArch.contains("64")) {
                fileName = "lib" + libraryName + ".so";
            } else {
                fileName = "lib" + libraryName + ".so";  // 假设都是 64 位
            }
        } else {
            throw new UnsupportedOperationException("Unsupported OS: " + osName);
        }
        
        // 提取原生库
        String tempDir = System.getProperty("java.io.tmpdir");
        String targetPath = tempDir + File.separator + fileName;
        extractNativeLibrary(fileName, targetPath);
        
        // 设置 java.library.path
        System.setProperty("java.library.path", tempDir);
        
        // 加载原生库
        System.loadLibrary(libraryName);
    }
}

// 使用示例
public class Main {
    public static void main(String[] args) {
        try {
            // 提取并加载原生库
            NativeLibraryExtractor.loadNativeLibrary("swt");
            
            // 运行应用
            Display display = new Display();
            Shell shell = new Shell(display);
            shell.setText("My App");
            shell.open();
            
            while (!shell.isDisposed()) {
                if (!display.readAndDispatch()) {
                    display.sleep();
                }
            }
            
            display.dispose();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

费曼解释:提取原生库的原理

提取原生库 = 发送快递
- 你在 JAR 中打包了原生库(快递)
- 用户下载 JAR(接收快递)
- 程序启动时,把原生库提取到临时目录(打开快递)
- 加载原生库(使用快递)

类比:
提取原生库 = 分发教材
- 教材打包在 JAR 中
- 学生下载 JAR
- 程序启动时,把教材提取到临时目录
- 学生阅读教材

设置 java.library.path

// 方法 1:设置系统属性
System.setProperty("java.library.path", "path/to/native/libs");

// 方法 2:使用 JVM 参数
// java -Djava.library.path=path/to/native/libs -jar my-app.jar

// 方法 3:在代码中动态加载
URL url = new File("path/to/native/libs").toURI().toURL();
System.setProperty("java.library.path", 
    System.getProperty("java.library.path") + File.pathSeparator + 
    url.toURI().getPath());

// 加载原生库
System.loadLibrary("swt");

费曼解释:java.library.path 的作用

java.library.path = 搜索路径
- JVM 在这个路径中查找原生库
- 找到就加载,找不到就抛出异常

类比:
java.library.path = 搜索路线
- 侦探(JVM)根据路线图(java.library.path)搜索
- 找到线索(原生库)就破案(加载)
- 找不到线索就宣布无果(抛出异常)

16.5 签名与公证(Mac)

签名应用

# 使用 codesign 签名应用
codesign --sign "Developer ID Application: My Company" \
         --force \
         --deep \
         My.app

# 验证签名
codesign -vvv --deep --strict My.app

费曼解释:签名的含义

签名 = 盖章
- 你开发了一个应用(商品)
- 你在应用上盖章(签名)
- 证明这个应用是你开发的(商品是你生产的)

类比:
签名 = 品牌商标
- 品牌在商品上贴商标(签名)
- 证明这个商品是正品(签名有效)

公证应用

# 使用公证工具公证应用
xcrun notarytool submit My.app \
    --apple-id "mycompany@example.com" \
    --password "@keychain:Developer ID Application: My Company" \
    --team-id "XXXXXXXXXX"

# 下载公证凭证
xcrun notarytool get-notary-info --submission-id <submission-id>

# 精贴公证凭证到应用
xcrun stapler staple My.app

# 验证公证
spctl -a -v -t execute My.app

费曼解释:公证的含义

公证 = 官方证明
- 你开发了一个应用(商品)
- 官方(Apple)检查商品
- 官方盖章证明商品合规(公证)
- 用户可以放心使用(不会弹出警告)

类比:
公证 = 质量认证
- 你开发了一个商品(应用)
- 质检部门(Apple)检查商品
- 质检部门贴上"合格"标签(公证)
- 消费者可以放心购买(不会弹出警告)

16.6 本章小结

打包与部署总结

方式作用类比
Eclipse Export导出 JAR自己打包
Maven Shade打包 fat jar专业打包员
jpackage打包原生安装程序礼品包装机
提取原生库分发原生库发送快递
签名验证应用身份盖章
公证官方认证质量认证

费曼测试:你能解释清楚吗?

  1. fat jar 是什么?

- 把所有依赖都打包到一个 JAR 中

  1. jpackage 的作用?

- 打包为原生安装程序,让应用像原生应用

  1. 如何提取原生库?

- 从 JAR 中读取资源,写入临时目录

  1. Mac 签名和公证的区别?

- 签名:验证开发者身份(盖个人章) - 公证:官方认证(盖官方章)

下一章预告

现在你已经掌握了 SWT 应用的打包与部署,
可以分发应用给用户,
真正投入使用。

下一章,我们将学习 SWT 性能优化,
让应用运行更快、更流畅,
提升用户体验。

练习题:

  1. 使用 Maven Shade 插件打包一个 fat jar。
  2. 使用 jpackage 打包一个 Windows .exe 安装程序。
  3. 编写代码,提取并加载 SWT 原生库。

(提示:Maven Shade 使用 maven-shade-plugin,jpackage 使用 --type exe 参数)

← 返回目录