1. Introduction
In this tutorial, we’re going to look at how to build a Spring Boot project into a thin JAR file, using the spring-boot-thin-launcher project.
Spring Boot is known for its “fat” JAR deployments, where a single executable artifact contains both the application code and all of its dependencies.
Boot is also widely used to develop microservices. This can sometimes be at odds with the “fat JAR” approach because including the same dependencies over and over in many artifacts can become an important waste of resources.
2. Prerequisites
First of all, we need a Spring Boot project, of course. In this article, we’ll look at Maven builds, and Gradle builds in their most common configurations.
It’s impossible to cover all the build systems and build configurations out there, but, hopefully, we’ll view enough of the general principles that you should be able to apply them to your specific setup.
2.1. Maven Projects
In a Boot project built with Maven, we ought to have the Spring Boot Maven plugin configured in our project’s pom.xml file, its parent, or one of its ancestors:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin>
Here, we’re referring to version 2.0.2.RELEASE of the plugin, the latest at the time of writing. The version of Spring Boot dependencies is usually decided by using a BOM or inheriting from a parent POM as in our reference project:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> <relativePath/> </parent>
2.2. Gradle Projects
In a Boot project built with Gradle, we’ll have the Boot Gradle plugin:
buildscript { ext { springBootPlugin = 'org.springframework.boot:spring-boot-gradle-plugin' springBootVersion = '2.0.1.RELEASE' } repositories { mavenCentral() } dependencies { classpath("${springBootPlugin}:${springBootVersion}") } } // elided apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' springBoot { mainClassName = 'org.baeldung.DemoApplication' }
Note that, in this article, we’ll be considering only Boot 2.x and later projects. The Thin Launcher also supports earlier versions, but it requires a slightly different Gradle configuration that we’re omitting for simplicity. Please look at the project’s homepage for more details.
3. How to Create a Thin JAR?
The Spring Boot Thin Launcher is a small library that reads an artifact’s dependencies from a file bundled in the archive itself, downloads them from a Maven repository and finally launches the main class of the application.
So, when we build a project with the library, we get a JAR file with our code, a file enumerating its dependencies, and the main class from the library that performs the above tasks.
Of course, things are a bit more nuanced than our simplified explanation; we’ll discuss some topics in depth later in the article.
4. Basic Usage
Let’s now see how to build a “thin” JAR from our regular Spring Boot application.
We’ll launch the application with the usual java -jar <my-app-1.0.jar>, with optional additional command line arguments that control the Thin Launcher. We’ll see a couple of them in the following sections; the project’s homepage contains the full list.
4.1. Maven Projects
In a Maven project, we have to modify the declaration of the Boot plugin (see section 2.1) to include a dependency on the custom “thin” layout:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <dependencies> <!-- The following enables the "thin jar" deployment option. --> <dependency> <groupId>org.springframework.boot.experimental</groupId> <artifactId>spring-boot-thin-layout</artifactId> <version>1.0.11.RELEASE</version> </dependency> </dependencies> </plugin>
The launcher will read dependencies from the pom.xml file that Maven stores in the generated JAR in the META-INF/maven directory.
We’ll perform the build as usual, e.g., with mvn install.
If we want to be able to produce both thin and fat builds (for example in a project with multiple modules) we can declare the custom layout in a dedicated Maven profile.
4.2. Maven and Dependencies: thin.properties
We can also have Maven generate a thin.properties file in addition to pom.xml. In that case, the file will contain the complete list of dependencies, including transitive ones, and the launcher will prefer it over the pom.xml.
The mojo (plugin) for doing so is spring-boot-thin-maven-plugin:properties, and by default, it outputs the thin.properties file in src/main/resources/META-INF, but we can specify its location with the thin.output property:
$ mvn org.springframework.boot.experimental:spring-boot-thin-maven-plugin:properties -Dthin.output=.
Please note that the output directory must exist for the goal to succeed, even if we’ve kept the default one.
4.3. Gradle Projects
In a Gradle project, instead, we add a dedicated plugin:
buildscript { ext { //... thinPlugin = 'org.springframework.boot.experimental:spring-boot-thin-gradle-plugin' thinVersion = '1.0.11.RELEASE' } //... dependencies { //... classpath("${thinPlugin}:${thinVersion}") } } //elided apply plugin: 'maven' apply plugin: 'org.springframework.boot.experimental.thin-launcher'
To obtain a thin build, we’ll tell Gradle to execute the thinJar task:
~/projects/baeldung/spring-boot-gradle $ ./gradlew thinJar
4.4. Gradle and Dependencies: pom.xml
In the code example in the previous section, we’ve declared the Maven plugin in addition to the Thin Launcher (as well as the Boot and Dependency Management plugins that we’d already seen in the Prerequisites section).
That’s because, just like in the Maven case that we’ve seen earlier, the artifact will contain and make use of a pom.xml file enumerating the application’s dependencies. The pom.xml file is generated by a task called thinPom, which is an implicit dependency of any jar task.
We can customize the generated pom.xml file with a dedicated task. Here, we’ll just replicate what the thin plugin already does automatically:
task createPom { def basePath = 'build/resources/main/META-INF/maven' doLast { pom { withXml(dependencyManagement.pomConfigurer) }.writeTo("${basePath}/${project.group}/${project.name}/pom.xml") } }
To use our custom pom.xml file, we add the above task to the jar task’s dependencies:
bootJar.dependsOn = [createPom]
4.5. Gradle and Dependencies: thin.properties
We can also have Gradle generate a thin.properties file rather than pom.xml, as we did earlier with Maven.
The task that generates the thin.properties file is called thinProperties, and it’s not used by default. We can add it as a dependency of the jar task:
bootJar.dependsOn = [thinProperties]
5. Storing Dependencies
The whole point of thin jars is to avoid bundling the dependencies with the application. However, dependencies don’t magically disappear, they’re simply stored elsewhere.
In particular, the Thin Launcher uses the Maven infrastructure to resolve dependencies, so:
- it checks the local Maven repository, which by default lies in ~/.m2/repository but can be moved elsewhere;
- then, it downloads missing dependencies from Maven Central (or any other configured repository);
- finally, it caches them in the local repository, so that it won’t have to download them again the next time we run the application.
Of course, the download phase is the slow and error-prone part of the process, because it requires access to Maven Central through the Internet, or access to a local proxy, and we all know how those things are generally unreliable.
Fortunately, there are various ways of deploying the dependencies together with the application(s), for example in a prepackaged container for cloud deployment.
5.1. Running the Application for Warm-up
The simplest way to cache the dependencies is to do a warm-up run of the application in the target environment. As we’ve seen earlier, this will cause the dependencies to be downloaded and cached in the local Maven repository. If we run more than one app, the repository will end up containing all the dependencies without duplicates.
Since running an application can have unwanted side effects, we can also perform a “dry run” that only resolves and downloads the dependencies without running any user code:
$ java -Dthin.dryrun=true -jar my-app-1.0.jar
Note that, as per Spring Boot conventions, we can set the -Dthin.dryrun property also with a –thin.dryrun command line argument to the application or with a THIN_DRYRUN system property. Any value except false will instruct the Thin Launcher to perform a dry run.
5.2. Packaging the Dependencies During the Build
Another option is to collect the dependencies during the build, without bundling them in the JAR. Then, we can copy them to the target environment as part of the deployment procedure.
This is generally simpler because it’s not necessary to run the application in the target environment. However, if we’re deploying multiple applications, we’ll have to merge their dependencies, either manually or with a script.
The format in which the Thin Plugin for Maven and Gradle packages the dependencies during a build is the same as a Maven local repository:
root/ repository/ com/ net/ org/ ...
In fact, we can point an application using the Thin Launcher to any such directory (including a local Maven repository) at runtime with the thin.root property:
$ java -jar my-app-1.0.jar --thin.root=my-app/deps
We can also safely merge multiple such directories by copying them one over another, thus obtaining a Maven repository with all the necessary dependencies.
5.3. Packaging the Dependencies With Maven
To have Maven package the dependencies for us, we use the resolve goal of the spring-boot-thin-maven-plugin. We can invoke it manually or automatically in our pom.xml:
<plugin> <groupId>org.springframework.boot.experimental</groupId> <artifactId>spring-boot-thin-maven-plugin</artifactId> <version>${thin.version}</version> <executions> <execution> <!-- Download the dependencies at build time --> <id>resolve</id> <goals> <goal>resolve</goal> </goals> <inherited>false</inherited> </execution> </executions> </plugin>
After building the project, we’ll find a directory target/thin/root/ with the structure that we’ve discussed in the previous section.
5.4. Packaging the Dependencies With Gradle
If we’re using Gradle with the thin-launcher plugin, instead, we have a thinResolve task available. The task will save the application and its dependencies in the build/thin/root/ directory, similarly to the Maven plugin of the previous section:
$ gradlew thinResolve
Please note that, at the time of writing, the thin-launcher plugin has a bug that prevents the dependencies to be saved if thin.properties is used: https://github.com/dsyer/spring-boot-thin-launcher/issues/53.
6. Conclusions and Further Reading
In this article, we’ve looked at how to make our thin jar. We’ve also seen how to use the Maven infrastructure to download and store their dependencies.
The homepage of the thin launcher has a few more HOW-TO guides for scenarios such as cloud deployments to Heroku, as well as the full list of supported command line arguments.
The implementation of all the Maven examples and code snippets can be found in the GitHub project – as a Maven project, so it should be easy to import and run as is.
Similarly, all Gradle examples refer to this GitHub project.