Skip to main content

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:

  1. JAR (Java ARchive) - The default and recommended approach for most Spring Boot applications
  2. 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:

xml
<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:

bash
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:

bash
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:

groovy
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:

bash
./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

  1. First, update your main application class to extend SpringBootServletInitializer:
java
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);
}
}
  1. Change the packaging type in your Maven pom.xml:
xml
<packaging>war</packaging>
  1. Make the embedded servlet container's dependency provided:
xml
<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:

bash
mvn clean package

For Gradle, update your build.gradle file:

groovy
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:

bash
./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:

dockerfile
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:

  1. Uses the Eclipse Temurin JDK 17 Alpine image
  2. Creates a volume for temporary files
  3. Copies your JAR file into the container as app.jar
  4. Sets the entry point to run the JAR

Building a Docker Image

After creating your JAR file, build the Docker image:

bash
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:

bash
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:

xml
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>

For Gradle:

groovy
bootJar {
layered {
enabled = true
}
}

Then create a more efficient Dockerfile:

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:

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:

xml
<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:

bash
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

java
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

java
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

java
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

  1. Build the JAR:
bash
mvn clean package
  1. Create a Docker image:
bash
docker build -t book-service:latest .
  1. Run the container:
bash
docker run -p 8080:8080 book-service:latest
  1. Test the API:
bash
# 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:

bash
java -jar -Dspring.profiles.active=prod myapp.jar

In Docker:

dockerfile
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

  1. Spring Boot Reference Documentation on Packaging
  2. Spring Boot Docker Documentation
  3. Cloud Native Buildpacks - An alternative to Dockerfiles
  4. Spring Boot Maven Plugin Documentation
  5. Spring Boot Gradle Plugin Documentation

Exercises

  1. Create a simple Spring Boot REST application and package it as an executable JAR.
  2. Modify the same application to be packaged as a WAR file.
  3. Containerize your application using Docker and the layered JAR approach.
  4. Create a multi-module Spring Boot application and package it as a single executable JAR.
  5. 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! :)