JApp is a modern Java program packaging format.
Its design goal is to be a better alternative to shadow jar (fat jar) and launch4j, and to be the optimal solution for single-file packaging and distribution of Java programs.
For Java programs, we now have many ways to package them, each with different advantages and disadvantages:
This is the packaging method currently promoted by OpenJDK officials.
jlink trims the JDK to retain only the required modules, and then packages the JDK and the program together.
Advantage:
- The program comes with a Java runtime environment, so users do not need to install additional dependencies;
- Developers can choose the Java runtime to use, avoiding compatibility issues as much as possible.
Disadvantage:
-
jlink compromises the cross-platform capabilities of Java programs. This makes it more difficult for users other than Windows/Linux/macOS systems and x86/ARM architectures (such as Linux RISC-V/LoongArch and AIX users) to use Java programs.
Most Java programs are actually perfectly cross-platform and can run on any platform that supports the JVM. However, jlink packages the program with the native files of a specific platform, so that the packaged program can only run on one platform.
Although jlink supports cross-building and can package program for other platforms. However, this is different from Golang, which can easily generate small executable files for different platforms. jlink needs to download a JDK for each target platform, and the packaged program ranges from tens of megabytes to hundreds of megabytes.
For each additional target platform, an additional 200MB of JDK must be downloaded to the packaging device, the packaging time will increase by tens of seconds to several minutes, and finally an additional file of tens to hundreds of MB must be distributed. This reduces the incentive for developers to provide distribution to more platforms.
Another thing to consider is, where do we download these JDKs from? Most developers choose a vendor they trust (such as Eclipse Adoptium, BellSoft, and Azul) and download all JDKs from him. This means that the compatibility of the programs they distribute often depends on this vendor.
The platforms supported by these vendors cover most of the common platforms, but there are some niche platforms that are not taken care of. For example, platforms like FreeBSD, Alpine Linux, and Linux LoongArch 64, few JDK vendors considers them, and Java on these platforms is often provided by the system's package manager. Therefore, these platforms are rarely considered by developers who use jlink to package programs.
-
Since the program is always distributed with the Java runtime environment, this greatly increases the size of the programs and the Java runtime can no longer be shared between multiple Java programs.
For end users, the countless chromium on the disk has already troubled many people, and jlink has brings countless Java standard libraries/JVMs to their disks.
For servers, the Java runtime environment does not need to change often. Traditionally, the Java runtime environment can be installed on the system, or in a base image. If you want to use jlink, you have to transfer the entire Java standard library/JVM for every update, which is a complete waste.
-
Currently, jlink packages a zip archive instead of a single executable file. Users need to find a place to unzip/install the program before they can use it, which is not so convenient and flexible.
The Hermetic Java project plans to address this issue, but it is unknown when it will be completed.
Shadow(Fat) JAR technology packages the class files and resources of a Java program and all its dependencies into a single JAR file.
Advantage:
- Single file, small, cross-platform, and fast to package.
Disadvantage:
- Users need to install Java to run it;
- It cannot choose which Java to start itself with;
- It is not possible to pass JVM options to the JVM and only provides limited control over the JVM through the manifest file;
- Since a JAR file can contain only one module, it does not work well with JPMS and often only works with the classpath.
- Pack multiple modular or non-modular JARs into one file;
-
Unlike Shadow JAR (Fat JAR), JApp has good support for the Java module system; Resources from different JARs will be isolated under different prefixes instead of being mixed.
For example, if we put Gson and Apache Commons Lang 3 as modules into a JApp file, their
module-info.class
URIs are as follows:japp:/modules/com.google.gson/module-info.class japp:/modules/org.apache.commons.lang3/module-info.class
JApp also supports using the class path and module path at the same time. After adding the above two modules, you can also put Guava into the class path, the URI of class
com.google.common.collect.Multimap
is as follows:japp:/classpath/guava-32.1.3-jre.jar/com/google/common/collect/Multimap.class
-
- JApp files can declare dependencies on JARs from other sources (such as maven repositories). The contents of these JARs are not included in the JApp file, but are resolved on demand before running, and then added to the module path or classpath like the JARs within the JApp file.
- Using the Zstandard compression method, the file size is smaller than JAR;
-
JApp compresses files using the zstd, which decompresses faster and has smaller file sizes than the deflate compression method used by JAR. In addition, JApp also compresses file metadata and shares strings in the constant pool of Java Class files, so JApp files are usually smaller than JAR files.
As a test case, I packed the aya language as a japp file, the original fat jar is 6.81MiB, while the resulting JApp file is only 5.08MiB (-25.40%).
-
- Automatically select a suitable Java Runtime to start the program based on user-specified conditions;
- Users can specify some conditions (such as Java version >= 17), and then the JApp launcher will find a suitable Java Runtime installed by the user to start the program based on these conditions.
- JApp files can contain JVM options (such as
--add-exports
,--enable-native-access
,-D
, etc.), which are passed to the JVM at runtime; - It supports shebang, so you can run it with just
./myapp.japp <args>
; - Supports conditional addition of JVM options, classpath, and module paths.
Work in progress:
- More tests;
- Reimplement launcher in native language;
- The japp launcher's job is to find suitable Java, synthesize JVM options, and class/module paths based on conditions. In the current prototype it is implemented in Java, which brings some limitations, I will rewrite it in native language in the future.
- Implement a manager that manages a set of Java;
- Now that the japp launcher will only scan Java from a fixed list of paths, we need a way to manage the list of available Java runtimes instead.
- Support for filtering unused classes;
- Support for embedding configuration files in JAR;
- Support for Java 8.
To be investigated:
- Support bundling and loading native libraries;
- Supports reading JMod files when creating;
- Proguard support;
- Build time optimization;
- Embed japp file data in the launcher instead of appending it at the end.
Welcome to discuss in Discussions.
NOTE: This project is in its early stages. Some designs have been simplified for convenience, and they will be improved in the future. The japp file created so far should be used for testing only; The format of japp files is not yet stable and is subject to change.
To try this project, you first need to build it:
./gradlew
Then, package your program as a japp file:
(For Linux/macOS)
./bin/japp.sh create -o myapp.japp --module-path <your-app-module-path> --class-path <your-app-class-path> <main-class>
(For Windows)
.\bin\japp.ps1 create -o myapp.japp --module-path <your-app-module-path> --class-path <your-app-class-path> <main-class>
Now you can run it:
(For Linux/macOS)
./myapp.japp <args>
(For Windows)
.\bin\japp.ps1 run myapp.japp <args>
The japp create
command accepts the following basic options:
-o <output file>
JApp packages class paths, module paths, JVM options, etc. into config groups. You can add these things to the config group using the following command line options:
--module-path <module path>
--class-path <class path>
--add-opens <module>/<package>=<target-module>(,<target-module>)*
--add-exports <module>/<package>=<target-module>(,<target-module>)*
--enable-native-access <module name>[,<module name>...]
-D<name>=<value>
-m <main module>
<main class>
A config group can have a set of sub-config groups.
By default, these options are added to the root config group.
Use the --group
command line option to start a new sub-config group, and use the --end-group
option to end it.
Each config group can specify a condition using the --condition <condition>
option.
Conditions represent requirements for the Java runtime and environment.
For example, condition java(version: 11, arch: x86-64|aarch64)
indicates
that the Java runtime version must be at least 11 and the architecture must be x86-64 or AArch64.
You can also combine multiple conditions using &&
or ||
, such as java(version: 11) || java(arch: x86-64)
.
The japp launcher will search for a suitable Java runtime based on the condition of the root config group; If there is no Java runtime that meets the condition, an error will be reported.
The conditions of sub-config groups are used to determine whether the group should be applied.
Example:
./bin/japp.sh create -o myapp.japp \
--condition java(version: 11) --module-path ./myapp.jar \
--group --condition java(version: 22) --enable-native-access=org.glavo.myapp --end-group \
-m org.glavo.myapp
In the above example, assuming that myapp.jar
exists in the current directory (the java module name is org.glavo.myapp
),
this command will generate a japp file named myapp.japp
.
The main module (also the only module in the myapp.japp
) is org.glavo.myapp
.
All you need to run it is this:
./myapp.japp
The japp launcher looks for a Java runtime version 11 or higher to run the program.
If the Java runtime found is of version 22 or higher, the JVM option --enable-native-access=org.glavo.myapp
is added.
The --module-path
and --classpath
options above accept arguments similar to the java
/javac
command:
(For Windows, please replace the path separator with ;
)
--module-path <path 1>:...:<path n>
For the java
/javac
command, each path must be a file.
But for japp, each path can contain an optional prefix [<key1>=<value1>,...,<key n>=<value n>]
to specify options.
We can use this syntax to specify to look for jars from the maven repository instead of locally:
[type=maven]<group>/<artifact>/<version>
For example, the following option will add gson to the module path:
--module-path [type=maven]com.google.code.gson/gson/2.10
By default, this dependency is bundled into the japp file just like a normal module path item.
However, you can use the bundle=false
option to tell japp to only declare a dependency on it and not bundle its contents into the japp file:
--module-path [type=maven,bundle=false]com.google.code.gson/gson/2.10
When running this japp file, the japp launcher will first download the dependencies locally, then add it to the module path and then start the program.
Thanks to PLCT Lab for supporting me.
This project is developed using JetBrains IDEA. Thanks to JetBrains for providing me with a free license.