我有一个
jpackage
构建脚本,它采用我的一个项目(所有项目都具有完全相同的结构)并创建一个可执行文件。该脚本可以运行并生成一个安装程序,我可以运行它来创建我的可执行文件。
但是,我创建的可执行文件太大。对于一个简单的 Microsoft Paint 克隆,它大约有 70MB 大,所以这是 磁盘空间。我不一定有任何期望,但我需要这个数字下降。 这是该项目的
module-info.java
。
module PaintModule
{
requires java.base;
requires java.desktop;
}
这是构建脚本(的一个片段)。
private void createExecutable() throws Exception
{
System.out.println("STARTING TO RUN JPACKAGE");
final var jpackageCommand =
java.util.spi.ToolProvider
.findFirst("jpackage")
.orElseThrow()
;
final String executableName = prompt("Enter your executable name");
final String description = prompt("Enter the description for your executable");
jpackageCommand
.run
(
System.out,
System.err,
"--verbose",
"--type", "msi",
"--name", STR."\{executableName}",
"--install-dir", STR."davidalayachew_applications/\{executableName}",
"--vendor", "David Alayachew",
"--module-path", STR."\{this.projectDirectory.resolve(Path.of("run", "executable", "jar")).toString()}",
"--module", STR."\{this.moduleName}/\{this.packageName}.Main",
"--win-console",
"--win-dir-chooser",
"--win-shortcut",
"--win-shortcut-prompt",
"--java-options", "--enable-preview",
"--dest", STR."\{this.projectDirectory.resolve(Path.of("run", "executable", "installer")).toString()}",
"--description", STR."\{description}"
)
;
}
如您所见,我的项目是模块化的。我正在 Java 22 上运行。我想使用标准库附带的基本工具(
jpackage
、jlink
等)使文件大小尽可能小。关于如何缩小这个尺寸有什么见解吗?
此外,这个项目没有任何资源或任何东西。这些都是基本的 Swing 代码。而且代码本身还不到100KB。我为 jar 创建命令打开了详细日志记录——除了我的
.class
文件和清单之外什么都没有被捆绑到 jar 中。需要明确的是,我将模块化 jar 位置作为我的模块路径。有一次,我认为问题出在我从 jar 构建安装程序这一事实上,但有问题的 jar 是 37KB。值得怀疑。
我可以做什么来降低这个数字?最好的情况是 <10MB, but I will take whatever drops that number down. I don't need an installer or any of that other stuff, I just want this application to run on a windows machine.
而且,我怀疑它有帮助,但这里是完整的构建脚本,以防您想自己运行它。
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class CREATE_EXECUTABLE_FROM_PROJECT
{
/** I am expecting there to be an environment variable on the machine for this key. If not, add it. */
private static final int CURRENT_JAVA_VERSION = Integer.parseInt(System.getenv("CURRENT_JAVA_VERSION"));
/** This is the folder that all of my projects are in. */
private final Path workingDirectory = Path.of("C:", "Users", "david", "_WORKSPACE", "_PROGRAMMING", "_JAVA").toAbsolutePath();
private final Path projectDirectory;
private final Path sourceCodeDirectory;
private final String projectName;
private final String moduleName;
private final String packageName;
public CREATE_EXECUTABLE_FROM_PROJECT() throws Exception
{
final JFileChooser projectChooser = new JFileChooser(this.workingDirectory.toFile());
projectChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
projectChooser.setDialogTitle("Choose a project folder");
var userResponse = projectChooser.showOpenDialog(null);
if (JFileChooser.APPROVE_OPTION != userResponse)
{
throw new IllegalArgumentException("User did not press approve, so taking no action.");
}
this.projectDirectory = projectChooser.getSelectedFile().toPath().toAbsolutePath();
this.sourceCodeDirectory =
this
.projectDirectory
.resolve("src")
.resolve("main")
.resolve("java")
.toAbsolutePath()
;
this.projectName = this.projectDirectory.getFileName().toString();
this.moduleName = this.projectName + "Module";
this.packageName = this.projectName + "Package";
}
public static void main(final String[] args) throws Exception
{
enum ExecutionOption
{
COMPILE_CODE,
RUN_CODE,
CREATE_JAR,
RUN_JAR,
CREATE_EXECUTABLE,
;
}
final CREATE_EXECUTABLE_FROM_PROJECT exeCreator;
CHOOSE_PROJECT:
{
try
{
exeCreator = new CREATE_EXECUTABLE_FROM_PROJECT();
}
catch (final Exception exception)
{
System.out.println(exception.getMessage());
return;
}
}
final java.util.EnumMap<ExecutionOption, Boolean> map = new java.util.EnumMap<>(ExecutionOption.class);
CHOOSE_EXECUTION_OPTIONS:
{
final var list = new javax.swing.JPanel(new java.awt.GridLayout(0, 1));
final var instructions =
new javax.swing.JLabel("<html>Choose your execution options.<br>They will occur in numerical order.</html>");
list.add(instructions);
for (var option : ExecutionOption.values())
{
final String label = "<html>" + (option.ordinal() + 1) + "\t" + option.name().replaceAll("_", " ") + "</html>";
final var checkBox = new javax.swing.JCheckBox(label, true);
map.put(option, true);
checkBox.addActionListener(_ -> map.put(option, checkBox.isSelected()));
list.add(checkBox);
}
final var userResponse =
JOptionPane
.showConfirmDialog
(
null,
list,
"Choose your execution options.",
JOptionPane.OK_CANCEL_OPTION
)
;
if (JOptionPane.OK_OPTION != userResponse || map.entrySet().stream().noneMatch(entry -> entry.getValue()))
{
System.out.println("User did not select an execution option.");
return;
}
}
CONFIRM_BEFORE_RUNNING:
{
final int userResponse =
JOptionPane
.showConfirmDialog
(
null,
STR.
"""
<html>
Are you sure you want to create an executable for \{exeCreator.projectName}?<br>
projectDirectory = \{exeCreator.projectDirectory}<br>
projectName = \{exeCreator.projectName}<br>
moduleName = \{exeCreator.moduleName}<br>
packageName = \{exeCreator.packageName}
</html>
"""
)
;
if (userResponse != JOptionPane.YES_OPTION)
{
return;
}
}
EXECUTE_OPTIONS:
for (var entry : map.entrySet())
{
if (false == entry.getValue())
{
continue EXECUTE_OPTIONS;
}
var option = entry.getKey();
switch (option)
{
case COMPILE_CODE -> exeCreator.compileCode();
case RUN_CODE -> exeCreator.runCode();
case CREATE_JAR -> exeCreator.createJar();
case RUN_JAR -> exeCreator.runJar();
case CREATE_EXECUTABLE -> exeCreator.createExecutable();
}
}
}
private static String prompt(final String question)
{
String response = "";
do
{
response = JOptionPane.showInputDialog(null, question);
}
while (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(null, STR."Please confirm that this is correct -- {\{response}}"));
return response;
}
private void compileCode() throws Exception
{
System.out.println("STARTING TO COMPILE MODULAR SOURCE CODE");
final JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
Objects.requireNonNull(javac);
final StandardJavaFileManager standardJavaFileManager = javac.getStandardFileManager(null, null, null);
final var compilerOptions =
List
.of
(
// STR."--module-source-path=\{mainModulePath}",
STR."--module-source-path=\{this.sourceCodeDirectory.toString()}",
STR."--module-path=\{this.workingDirectory.toString()}",
STR."--module=\{this.moduleName}",
"--add-modules=com.formdev.flatlaf",
"--enable-preview",
"--source=" + CURRENT_JAVA_VERSION,
"-d", STR."\{this.projectDirectory.resolve("classes").toString()}"
)
;
final var compilationTask = javac.getTask(null, null, null, compilerOptions, null, null);
compilationTask.call();
}
private void runCode() throws Exception
{
System.out.println("STARTING TO RUN MODULAR SOURCE CODE");
final ProcessBuilder processBuilder =
new
ProcessBuilder
(
"java",
"--enable-preview",
STR."--patch-module=\{this.moduleName}=src/main/resources",
"--module-path=classes;..",
STR."--module=\{this.moduleName}/\{this.packageName}.Main"
)
.inheritIO()
.directory(this.projectDirectory.toFile())
;
final Process process = processBuilder.start();
final int exitCode = process.waitFor();
System.out.println("exitCode = " + exitCode);
if (exitCode != 0)
{
throw new IllegalArgumentException("Failure");
}
}
private void createJar() throws Exception
{
System.out.println("STARTING TO BUILD A MODULAR JAR");
final var jarCommand =
java.util.spi.ToolProvider
.findFirst("jar")
.orElseThrow()
;
final Path pathToJar = this.projectDirectory.resolve(Path.of("run", "executable", "jar", this.projectName));
final Path pathToResources = this.projectDirectory.resolve(Path.of("src", "main", "resources"));
final Path pathToModuleClassFiles = this.projectDirectory.resolve("classes").resolve(this.moduleName);
jarCommand
.run
(
System.out,
System.err,
"--verbose",
"--create",
STR."--file=\{pathToJar.toString()}.jar",
STR."--main-class=\{this.packageName}.Main",
"-C", STR."\{pathToModuleClassFiles.toString()}", ".",
"-C", STR."\{pathToResources.toString()}", "."
)
;
}
private void runJar() throws Exception
{
System.out.println("STARTING TO RUN A MODULAR JAR");
final ProcessBuilder processBuilder =
new
ProcessBuilder
(
"java",
"--enable-preview",
"--module-path=run/executable/jar",
STR."--module=\{this.moduleName}/\{this.packageName}.Main"
)
.inheritIO()
.directory(this.projectDirectory.toFile())
;
final Process process = processBuilder.start();
final int exitCode = process.waitFor();
System.out.println("exitCode = " + exitCode);
}
private void createExecutable() throws Exception
{
System.out.println("STARTING TO RUN JPACKAGE");
final var jpackageCommand =
java.util.spi.ToolProvider
.findFirst("jpackage")
.orElseThrow()
;
final String executableName = prompt("Enter your executable name");
final String description = prompt("Enter the description for your executable");
jpackageCommand
.run
(
System.out,
System.err,
"--verbose",
"--type", "msi",
"--name", STR."\{executableName}",
"--install-dir", STR."davidalayachew_applications/\{executableName}",
"--vendor", "David Alayachew",
"--module-path", STR."\{this.projectDirectory.resolve(Path.of("run", "executable", "jar")).toString()}",
"--module", STR."\{this.moduleName}/\{this.packageName}.Main",
"--win-console",
"--win-dir-chooser",
"--win-shortcut",
"--win-shortcut-prompt",
"--java-options", "--enable-preview",
"--dest", STR."\{this.projectDirectory.resolve(Path.of("run", "executable", "installer")).toString()}",
"--description", STR."\{description}"
)
;
}
}
java.base
和 java.desktop
是大模块。 java.base
在 Windows 中约为 21 MB,在 Linux 中约为 24 MB。 java.desktop
在 Windows 中约为 12 MB,在 Linux 中约为 14 MB。因此,仅模块就构成了 33–38 MB。
除此之外,还需要添加本机库,总共大约 20-30 MB。其中大部分是 jvm.so 或 jvm.dll。
因此,使用
java.desktop
的应用程序映像已经至少 50 MB,不包括应用程序类和资源。 (这些数字会因不同的 Java 版本和平台而异。)
通过将
--jlink-options --compress=2
添加到 jpackage
命令中,您可能会获得稍微更好的结果,但在实践中,我发现这并没有太大区别。 (我依稀记得读过 jmod 和图像模块 blob 已经包含嵌入式 zip 格式的数据。)
这可能看起来不守规矩,但大多数大型非 Java 应用程序都是同样的方式。有时他们使用系统上已安装的库,但通常不这样做,在这种情况下,它们的大小将与 Java 应用程序映像相当。
尽管我很喜欢轻量级可执行文件的想法,但对于现代应用程序来说 70 MB 可能是正常的。