Spring Application Packaging
Introduction
When you've built a Spring application and are ready to move it from development to a production environment, you need to package it properly. Application packaging is the process of bundling your Spring application code, dependencies, and resources into a distributable format that can be deployed to various environments.
In this guide, we'll explore different ways to package Spring applications, focusing on the most common approaches used in modern development workflows. We'll cover JAR files, WAR files, Docker containers, and other packaging options to help you choose the right approach for your needs.
Understanding Spring Boot Packaging Options
Spring Boot applications can be packaged in several ways, with the two most common formats being:
- JAR (Java ARchive) - The default and recommended approach for most Spring Boot applications
- WAR (Web ARchive) - Traditional packaging for Java web applications, used when deploying to external servers
Let's explore each option in detail.
Packaging as an Executable JAR
Spring Boot's most popular feature is its ability to create self-contained executable JAR files, often called "fat JARs" or "uber JARs." These JARs include all of your application's dependencies and can be run directly using the java -jar
command.
Setting Up a Maven Project for JAR Packaging
In a Maven-based project, your pom.xml
file should include the Spring Boot Maven Plugin:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Building the JAR
To create the executable JAR, run:
mvn clean package
This will build your application and create a JAR file in the target
directory. The output will look similar to:
[INFO] Building jar: /path/to/your-project/target/myapp-0.0.1-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
Running the JAR
You can run the packaged JAR file using:
java -jar target/myapp-0.0.1-SNAPSHOT.jar
If your application runs on port 8080, you should see output like:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0)
...
2023-12-01T12:00:00.000Z INFO 1 --- [main] c.e.demo.DemoApplication: Started DemoApplication in 2.456 seconds (JVM running for 2.892)
Setting Up a Gradle Project for JAR Packaging
If you're using Gradle, your build.gradle
file should include:
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}
// ... other configuration
bootJar {
mainClassName = 'com.example.demo.DemoApplication'
}
Building with Gradle
To create the executable JAR with Gradle:
./gradlew build
The JAR file will be created in the build/libs
directory.
Packaging as a WAR File
If you need to deploy your Spring Boot application to an external application server like Tomcat, JBoss, or WebSphere, you'll need to package it as a WAR file.
Converting a Spring Boot Application to WAR
- First, update your main application class to extend
SpringBootServletInitializer
:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class DemoApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DemoApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
- Change the packaging type in your Maven
pom.xml
:
<packaging>war</packaging>
- Make the embedded servlet container's dependency provided:
<dependencies>
<!-- ... other dependencies ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
Building the WAR
For Maven, use:
mvn clean package
For Gradle, update your build.gradle
file:
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
id 'war'
}
// Mark the embedded servlet container as provided
configurations {
providedRuntime
}
dependencies {
// ... other dependencies ...
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
}
Then build the WAR:
./gradlew build
Docker Containerization
In modern cloud-native environments, packaging Spring applications as Docker containers is increasingly common.
Creating a Dockerfile
Create a file named Dockerfile
in your project's root directory:
FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
This Dockerfile:
- Uses the Eclipse Temurin JDK 17 Alpine image
- Creates a volume for temporary files
- Copies your JAR file into the container as
app.jar
- Sets the entry point to run the JAR
Building a Docker Image
After creating your JAR file, build the Docker image:
docker build -t myapp:latest .
You should see output like:
[+] Building 10.2s (7/7) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 155B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/eclipse-temurin:17-jdk-alpine 1.2s
=> [1/3] FROM docker.io/eclipse-temurin:17-jdk-alpine 8.5s
=> [2/3] VOLUME /tmp 0.0s
=> [3/3] COPY target/myapp-0.0.1-SNAPSHOT.jar app.jar 0.2s
=> exporting to image 0.2s
=> => exporting layers 0.2s
=> => writing image sha256:a29d... 0.0s
=> => naming to docker.io/library/myapp:latest 0.0s
Running the Docker Container
Run your containerized Spring application:
docker run -p 8080:8080 myapp:latest
Advanced Packaging Techniques
Layered JARs
Spring Boot 2.3.0+ supports creating layered JARs, which can significantly improve Docker image building.
Update your Maven configuration:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
For Gradle:
bootJar {
layered {
enabled = true
}
}
Then create a more efficient Dockerfile:
FROM eclipse-temurin:17-jdk-alpine as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM eclipse-temurin:17-jre-alpine
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Spring Boot Thin Launcher
For applications with many dependencies, you can use the Spring Boot Thin Launcher to separate application code from dependencies.
Add the thin launcher plugin to your pom.xml
:
<plugin>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-maven-plugin</artifactId>
<version>1.0.31.RELEASE</version>
<executions>
<execution>
<id>resolve</id>
<goals>
<goal>resolve</goal>
</goals>
<inherited>false</inherited>
</execution>
</executions>
</plugin>
Executable JARs with Custom Scripts
You can customize the startup script for your executable JAR by configuring the Spring Boot Maven Plugin:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
This makes your JAR file executable directly on Unix-like systems:
chmod +x target/myapp-0.0.1-SNAPSHOT.jar
./target/myapp-0.0.1-SNAPSHOT.jar
Real-World Example: Packaging a REST API
Let's package a simple Spring Boot REST API that manages a list of books.
Project Structure
book-service/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── bookservice/
│ │ │ ├── BookServiceApplication.java
│ │ │ ├── model/
│ │ │ │ └── Book.java
│ │ │ └── controller/
│ │ │ └── BookController.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
├── pom.xml
└── Dockerfile
Book Model
package com.example.bookservice.model;
public class Book {
private Long id;
private String title;
private String author;
private Integer year;
// Constructors, getters, and setters
public Book() {}
public Book(Long id, String title, String author, Integer year) {
this.id = id;
this.title = title;
this.author = author;
this.year = year;
}
// Getters and setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Integer getYear() {
return year;
}
public void setYear(Integer year) {
this.year = year;
}
}
Book Controller
package com.example.bookservice.controller;
import com.example.bookservice.model.Book;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final ConcurrentHashMap<Long, Book> books = new ConcurrentHashMap<>();
private final AtomicLong idCounter = new AtomicLong();
public BookController() {
// Add some sample books
Book book1 = new Book(idCounter.incrementAndGet(), "Spring in Action", "Craig Walls", 2019);
Book book2 = new Book(idCounter.incrementAndGet(), "Clean Code", "Robert Martin", 2008);
books.put(book1.getId(), book1);
books.put(book2.getId(), book2);
}
@GetMapping
public List<Book> getAllBooks() {
return new ArrayList<>(books.values());
}
@GetMapping("/{id}")
public Book getBook(@PathVariable Long id) {
return books.get(id);
}
@PostMapping
public Book addBook(@RequestBody Book book) {
book.setId(idCounter.incrementAndGet());
books.put(book.getId(), book);
return book;
}
@DeleteMapping("/{id}")
public void deleteBook(@PathVariable Long id) {
books.remove(id);
}
}
Main Application
package com.example.bookservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BookServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BookServiceApplication.class, args);
}
}
Packaging and Deploying the App
- Build the JAR:
mvn clean package
- Create a Docker image:
docker build -t book-service:latest .
- Run the container:
docker run -p 8080:8080 book-service:latest
- Test the API:
# Get all books
curl http://localhost:8080/api/books
# Expected output:
# [{"id":1,"title":"Spring in Action","author":"Craig Walls","year":2019},
# {"id":2,"title":"Clean Code","author":"Robert Martin","year":2008}]
# Add a new book
curl -X POST -H "Content-Type: application/json" -d '{"title":"Microservices Patterns","author":"Chris Richardson","year":2018}' http://localhost:8080/api/books
# Expected output:
# {"id":3,"title":"Microservices Patterns","author":"Chris Richardson","year":2018}
This example demonstrates how to package, containerize, and run a Spring Boot REST API.
Packaging for Different Environments
When deploying to different environments (dev, test, prod), you may need different configurations:
Using Spring Profiles
In application.properties
:
spring.profiles.active=dev
Create environment-specific properties files:
application-dev.properties
application-test.properties
application-prod.properties
When running the JAR, specify the profile:
java -jar -Dspring.profiles.active=prod myapp.jar
In Docker:
FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "/app.jar"]
Summary
In this guide, we've explored various approaches to packaging Spring applications:
- JAR packaging - The default and recommended approach for most Spring Boot applications
- WAR packaging - For deploying to traditional application servers
- Docker containerization - For cloud-native deployments
- Advanced techniques like layered JARs and thin launchers
The right packaging approach depends on your deployment requirements, infrastructure, and organizational constraints. Spring Boot's flexibility allows you to choose the most appropriate option for your specific use case.
Additional Resources
- Spring Boot Reference Documentation on Packaging
- Spring Boot Docker Documentation
- Cloud Native Buildpacks - An alternative to Dockerfiles
- Spring Boot Maven Plugin Documentation
- Spring Boot Gradle Plugin Documentation
Exercises
- Create a simple Spring Boot REST application and package it as an executable JAR.
- Modify the same application to be packaged as a WAR file.
- Containerize your application using Docker and the layered JAR approach.
- Create a multi-module Spring Boot application and package it as a single executable JAR.
- Implement environment-specific configurations using Spring profiles and test running the application in different environments.
By mastering these packaging techniques, you'll be well-prepared to deploy Spring applications to various environments in a professional manner.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)