第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 | 打包原生安装程序 | 礼品包装机 |
| 提取原生库 | 分发原生库 | 发送快递 |
| 签名 | 验证应用身份 | 盖章 |
| 公证 | 官方认证 | 质量认证 |
费曼测试:你能解释清楚吗?
- fat jar 是什么?
- 把所有依赖都打包到一个 JAR 中
- jpackage 的作用?
- 打包为原生安装程序,让应用像原生应用
- 如何提取原生库?
- 从 JAR 中读取资源,写入临时目录
- Mac 签名和公证的区别?
- 签名:验证开发者身份(盖个人章) - 公证:官方认证(盖官方章)
下一章预告
现在你已经掌握了 SWT 应用的打包与部署,
可以分发应用给用户,
真正投入使用。
下一章,我们将学习 SWT 性能优化,
让应用运行更快、更流畅,
提升用户体验。
练习题:
- 使用 Maven Shade 插件打包一个 fat jar。
- 使用 jpackage 打包一个 Windows .exe 安装程序。
- 编写代码,提取并加载 SWT 原生库。
(提示:Maven Shade 使用 maven-shade-plugin,jpackage 使用 --type exe 参数)