Building an Executable JAR
Package the application, its dependencies and a launcher into one jar you start with java -jar, and know what is inside it.
./mvnw package # target/<name>-<version>.jar
./gradlew bootJar # build/libs/<name>-<version>.jar
java [jvm options] -jar app.jar [--property=value]A Spring Boot application ships as one file: an executable jar that holds your compiled classes, every dependency jar and a small launcher. The target machine needs a Java runtime and nothing else: no application server, no classpath to assemble.
What the build produces
With spring-boot-maven-plugin in the pom, ./mvnw package first builds an ordinary jar, then the plugin's repackage goal rewrites it. The original is kept beside it as .jar.original; that one is not runnable, so never deploy it. In Gradle, ./gradlew bootJar writes the executable jar to build/libs, and the jar task also produces a -plain.jar you can ignore.
Inside the executable jar:
BOOT-INF/classes: your classes andapplication.properties.BOOT-INF/lib: dependency jars, stored unchanged and nested.org/springframework/boot/loader: the launcher that knows how to read nested jars.META-INF/MANIFEST.MF:Main-Classnames the launcher andStart-Classnames your@SpringBootApplicationclass.
Because the dependencies are nested, java -cp app.jar com.example.Main does not work. Start it with java -jar.
Running it
java [JVM options] -jar app.jar [--spring.property=value ...]
Anything after the jar name is an application argument, and --server.port=9090 style arguments override properties. Options such as -Xmx or -XX:MaxRAMPercentage must come before -jar, or the JVM never sees them. Environment variables work too, which is how containers usually configure the jar.
Know which build is running
Add the plugin's build-info goal. It writes META-INF/build-info.properties, Spring Boot turns that into a BuildProperties bean, and the actuator info endpoint shows the version and build time. When an incident starts, the first question is which version is deployed; this answers it without guessing from tags.
Mistakes that reach production
- Running
./mvnw spring-boot:runon a server. It needs the source tree and Maven, and it is meant for development. - Shipping DevTools. The plugin leaves it out of the repackaged jar by default; do not add it back.
- Building on a laptop with a different JDK from the one in production. Build in CI, from a clean checkout, with the same Java version you run.
- Skipping tests with
-DskipTestsin the only pipeline step that runs them.
Setting project.build.outputTimestamp makes builds reproducible: the same commit gives the same jar bytes, so a checksum tells you whether two artefacts really differ.
Example
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<!-- Writes META-INF/build-info.properties into the jar -->
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<java.version>25</java.version>
<!-- Fixed entry timestamps: the same commit builds the same bytes -->
<project.build.outputTimestamp>2026-01-01T00:00:00Z</project.build.outputTimestamp>
</properties>
When to use it
- Ship a REST API to a virtual machine that only has a Java 25 runtime installed.
- Confirm during an incident which version and build time is actually running in production.
- Produce the same jar checksum from the same commit so the artefact tested in staging is provably the one released.
More examples
Build, inspect and run the jar
Listing the jar shows the three parts the launcher relies on. The <code>.original</code> file is the plain jar Maven built before repackaging; it has none of your dependencies, which is why deploying it fails with <code>ClassNotFoundException</code> at startup. The last line shows the order rule: JVM options first, then <code>-jar</code>, then application arguments.
# Package (tests run as part of the build)
./mvnw -B clean package
# Two files: the executable jar and the plain one Maven built first
ls target/*.jar*
# target/bookings-1.4.0.jar target/bookings-1.4.0.jar.original
# Look inside: your classes, your dependencies, the launcher
unzip -l target/bookings-1.4.0.jar | grep -E 'BOOT-INF/(classes|lib)/|MANIFEST' | head
# JVM options go before -jar, application arguments after it
java -XX:MaxRAMPercentage=75 -jar target/bookings-1.4.0.jar --server.port=9090
Expose the build version from code
<code>ObjectProvider</code> makes the dependency optional: the <code>BuildProperties</code> bean only exists when <code>build-info.properties</code> is on the classpath, which is not the case when you run the main class from an IDE without a Maven build. A version endpoint like this is useful for front ends and smoke tests that need to confirm a deploy landed.
import java.time.Instant;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.info.BuildProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
record VersionResponse(String version, Instant builtAt) {}
@RestController
public class VersionController {
private final ObjectProvider<BuildProperties> build;
VersionController(ObjectProvider<BuildProperties> build) {
this.build = build;
}
@GetMapping("/api/version")
VersionResponse version() {
// The bean only exists when the build-info goal ran, so an IDE run still works.
BuildProperties info = build.getIfAvailable();
if (info == null) {
return new VersionResponse("development", null);
}
return new VersionResponse(info.getVersion(), info.getTime());
}
}
Build details on the info endpoint
The build contributor reads the same file as the bean above. The git contributor needs a <code>git.properties</code> file, which a git commit id plugin can generate during the build. Exposing <code>info</code> is generally safe; keep the more revealing endpoints off the public port.
# Show build (and git, if present) details on /actuator/info
management.endpoints.web.exposure.include=health,info
management.info.build.enabled=true
management.info.git.enabled=true
Discussion