Tools for generating executable JAR/WAR files with embedded containers for Spring Boot applications
—
Advanced layering system for optimizing Docker image builds by strategically separating application code, dependencies, and Spring Boot loader components into distinct layers. This enables efficient caching and faster container image builds.
Core class representing a named layer for organizing archive content.
public class Layer {
/**
* Create a layer with the specified name.
*
* @param name the layer name
*/
public Layer(String name);
/**
* Check if this layer equals another object.
*
* @param obj the object to compare
* @return true if equal
*/
@Override
public boolean equals(Object obj);
/**
* Get the hash code for this layer.
*
* @return the hash code
*/
@Override
public int hashCode();
/**
* Get the string representation of this layer.
*
* @return the layer name
*/
@Override
public String toString();
}Primary interface for providing layer information during the packaging process.
public interface Layers extends Iterable<Layer> {
/**
* Get an iterator over all layers.
*
* @return iterator for layers
*/
@Override
Iterator<Layer> iterator();
/**
* Get a stream of all layers.
*
* @return stream of layers
*/
default Stream<Layer> stream() {
return StreamSupport.stream(spliterator(), false);
}
/**
* Get the layer for an application resource.
*
* @param applicationResource the application resource path
* @return the layer for this resource
*/
Layer getLayer(String applicationResource);
/**
* Get the layer for a library.
*
* @param library the library
* @return the layer for this library
*/
Layer getLayer(Library library);
/**
* Implicit layers implementation that uses default layer assignment.
*/
Layers IMPLICIT = /* default implementation */;
}Index file that describes which layer each entry belongs to, used by Docker image builders.
public class LayersIndex {
/**
* Create a layers index with the specified layers.
*
* @param layers the layers to include in the index
*/
public LayersIndex(Layer... layers);
/**
* Create a layers index from an iterable of layers.
*
* @param layers the layers to include
*/
public LayersIndex(Iterable<Layer> layers);
/**
* Add an entry to the specified layer.
*
* @param layer the layer to add the entry to
* @param name the entry name/path
*/
public void add(Layer layer, String name);
/**
* Write the index to an output stream.
* The index format is compatible with Spring Boot's layered JAR structure.
*
* @param out the output stream to write to
* @throws IOException if writing fails
*/
public void writeTo(OutputStream out) throws IOException;
}Pre-defined layer implementation following Spring Boot conventions for optimal Docker caching.
public abstract class StandardLayers implements Layers {
/**
* Layer for external dependencies.
* Changes infrequently, provides good caching.
*/
public static final Layer DEPENDENCIES = new Layer("dependencies");
/**
* Layer for Spring Boot loader classes.
* Changes only with Spring Boot version updates.
*/
public static final Layer SPRING_BOOT_LOADER = new Layer("spring-boot-loader");
/**
* Layer for snapshot dependencies.
* Separated from stable dependencies for better cache efficiency.
*/
public static final Layer SNAPSHOT_DEPENDENCIES = new Layer("snapshot-dependencies");
/**
* Layer for application code.
* Changes frequently, placed in top layer.
*/
public static final Layer APPLICATION = new Layer("application");
}Default implementation that automatically assigns layers based on content analysis.
public class ImplicitLayerResolver extends StandardLayers {
// Automatically determines appropriate layer for each component
// Uses heuristics to optimize Docker image layer efficiency
}User-defined layer configuration with custom selection logic for fine-grained control over layer assignment.
public class CustomLayers implements Layers {
/**
* Create custom layers with specified selectors.
*
* @param layers the ordered list of layers
* @param applicationSelectors selectors for application resources
* @param librarySelectors selectors for library resources
*/
public CustomLayers(List<Layer> layers,
List<ContentSelector<String>> applicationSelectors,
List<ContentSelector<Library>> librarySelectors);
}Strategy interface for determining which layer content should be assigned to.
public interface ContentSelector<T> {
/**
* Get the target layer for selected content.
*
* @return the layer
*/
Layer getLayer();
/**
* Check if this selector contains the specified item.
*
* @param item the item to check
* @return true if the item belongs to this selector's layer
*/
boolean contains(T item);
}Pattern-based content selector using include and exclude rules.
public class IncludeExcludeContentSelector<T> implements ContentSelector<T> {
// Implementation uses pattern matching for flexible content selection
// Supports wildcards and regular expressions
}Filter interfaces for different types of content.
public interface ContentFilter<T> {
// Base interface for content filtering
}
public class ApplicationContentFilter implements ContentFilter<String> {
// Filter for application resources (class files, properties, etc.)
}
public class LibraryContentFilter implements ContentFilter<Library> {
// Filter for library dependencies
}import org.springframework.boot.loader.tools.*;
import java.io.File;
// Use standard layers for optimal Docker caching
File sourceJar = new File("myapp.jar");
Repackager repackager = new Repackager(sourceJar);
// Enable layered packaging
repackager.setLayers(Layers.IMPLICIT);
// Repackage with layer support
repackager.repackage(Libraries.NONE);
// The resulting JAR will include BOOT-INF/layers.idx
// Docker buildpacks can use this for efficient layer creationimport org.springframework.boot.loader.tools.*;
import org.springframework.boot.loader.tools.layer.*;
import java.io.File;
import java.util.List;
// Define custom layers
Layer infrastructureLayer = new Layer("infrastructure");
Layer businessLogicLayer = new Layer("business-logic");
Layer configurationLayer = new Layer("configuration");
// Create content selectors
List<ContentSelector<String>> applicationSelectors = List.of(
new IncludeExcludeContentSelector<>(configurationLayer,
List.of("**/*.properties", "**/*.yml", "**/*.xml"),
List.of()),
new IncludeExcludeContentSelector<>(businessLogicLayer,
List.of("**/service/**", "**/business/**"),
List.of()),
new IncludeExcludeContentSelector<>(infrastructureLayer,
List.of("**"), // everything else
List.of())
);
List<ContentSelector<Library>> librarySelectors = List.of(
new IncludeExcludeContentSelector<>(infrastructureLayer,
List.of("org.springframework.*", "org.hibernate.*"),
List.of()),
new IncludeExcludeContentSelector<>(businessLogicLayer,
List.of("com.example.business.*"),
List.of())
);
// Create custom layers configuration
Layers customLayers = new CustomLayers(
List.of(infrastructureLayer, businessLogicLayer, configurationLayer),
applicationSelectors,
librarySelectors
);
// Apply to repackager
Repackager repackager = new Repackager(new File("myapp.jar"));
repackager.setLayers(customLayers);
repackager.repackage(Libraries.NONE);import org.springframework.boot.loader.tools.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
// Manually create and write a layer index
Layer appLayer = new Layer("application");
Layer libLayer = new Layer("dependencies");
Layer loaderLayer = new Layer("spring-boot-loader");
LayersIndex index = new LayersIndex(appLayer, libLayer, loaderLayer);
// Add entries to layers
index.add(appLayer, "BOOT-INF/classes/com/example/Application.class");
index.add(appLayer, "BOOT-INF/classes/com/example/service/UserService.class");
index.add(libLayer, "BOOT-INF/lib/spring-boot-3.2.0.jar");
index.add(libLayer, "BOOT-INF/lib/logback-classic-1.4.5.jar");
index.add(loaderLayer, "org/springframework/boot/loader/launch/JarLauncher.class");
// Write index to file
File indexFile = new File("layers.idx");
try (FileOutputStream fos = new FileOutputStream(indexFile)) {
index.writeTo(fos);
}
// The index file can be included in the JAR at BOOT-INF/layers.idximport org.springframework.boot.loader.tools.*;
import java.io.File;
// Configure layers for optimal buildpack performance
public class BuildpackOptimizedLayers extends StandardLayers {
// Additional custom layers for buildpack optimization
public static final Layer BUILDPACK_CACHE = new Layer("buildpack-cache");
public static final Layer CONFIGURATION = new Layer("configuration");
@Override
public Layer getLayer(String applicationResource) {
// Buildpack-specific caching frequently accessed resources
if (applicationResource.endsWith(".properties") ||
applicationResource.endsWith(".yml") ||
applicationResource.endsWith(".yaml")) {
return CONFIGURATION;
}
// Use standard logic for other resources
return super.getLayer(applicationResource);
}
@Override
public Layer getLayer(Library library) {
LibraryCoordinates coords = library.getCoordinates();
if (coords != null) {
// Separate buildpack-specific libraries
if (coords.getGroupId().startsWith("io.buildpacks") ||
coords.getGroupId().startsWith("org.cloudfoundry")) {
return BUILDPACK_CACHE;
}
}
return super.getLayer(library);
}
}
// Use with repackager
Repackager repackager = new Repackager(new File("myapp.jar"));
repackager.setLayers(new BuildpackOptimizedLayers());
repackager.repackage(Libraries.NONE);import org.springframework.boot.loader.tools.*;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
// Analyze layer distribution for optimization
public class LayerAnalyzer {
private final Map<Layer, AtomicLong> layerSizes = new ConcurrentHashMap<>();
private final Map<Layer, AtomicLong> layerCounts = new ConcurrentHashMap<>();
public void analyzeLayers(Layers layers, Libraries libraries) throws IOException {
// Analyze library distribution
libraries.doWithLibraries(library -> {
Layer layer = layers.getLayer(library);
long size = library.getFile().length();
layerSizes.computeIfAbsent(layer, k -> new AtomicLong(0)).addAndGet(size);
layerCounts.computeIfAbsent(layer, k -> new AtomicLong(0)).incrementAndGet();
});
// Print analysis results
System.out.println("Layer Analysis:");
System.out.println("==============");
for (Layer layer : layers) {
long size = layerSizes.getOrDefault(layer, new AtomicLong(0)).get();
long count = layerCounts.getOrDefault(layer, new AtomicLong(0)).get();
System.out.printf("Layer: %s%n", layer);
System.out.printf(" Size: %,d bytes (%.2f MB)%n", size, size / 1024.0 / 1024.0);
System.out.printf(" Count: %,d items%n", count);
System.out.printf(" Avg Size: %,d bytes%n", count > 0 ? size / count : 0);
System.out.println();
}
}
}
// Usage
LayerAnalyzer analyzer = new LayerAnalyzer();
analyzer.analyzeLayers(Layers.IMPLICIT, libraries);import org.springframework.boot.loader.tools.*;
import org.springframework.boot.loader.tools.layer.*;
import java.util.List;
import java.util.function.Predicate;
// Dynamic layer assignment based on runtime criteria
public class DynamicLayers implements Layers {
private final List<Layer> layers;
private final Predicate<String> frequentlyChanging;
private final Predicate<Library> heavyLibraries;
public DynamicLayers(Predicate<String> frequentlyChanging, Predicate<Library> heavyLibraries) {
this.layers = List.of(
StandardLayers.SPRING_BOOT_LOADER,
new Layer("heavy-dependencies"),
StandardLayers.DEPENDENCIES,
new Layer("volatile-app"),
StandardLayers.APPLICATION
);
this.frequentlyChanging = frequentlyChanging;
this.heavyLibraries = heavyLibraries;
}
@Override
public Iterator<Layer> iterator() {
return layers.iterator();
}
@Override
public Layer getLayer(String applicationResource) {
if (frequentlyChanging.test(applicationResource)) {
return new Layer("volatile-app");
}
return StandardLayers.APPLICATION;
}
@Override
public Layer getLayer(Library library) {
if (heavyLibraries.test(library)) {
return new Layer("heavy-dependencies");
}
LibraryCoordinates coords = library.getCoordinates();
if (coords != null && coords.getGroupId().startsWith("org.springframework")) {
return StandardLayers.SPRING_BOOT_LOADER;
}
return StandardLayers.DEPENDENCIES;
}
}
// Usage with intelligent predicates
DynamicLayers dynamicLayers = new DynamicLayers(
// Frequently changing application resources
resource -> resource.contains("/controller/") ||
resource.contains("/service/") ||
resource.endsWith(".properties"),
// Heavy libraries (>10MB)
library -> library.getFile().length() > 10 * 1024 * 1024
);
Repackager repackager = new Repackager(new File("myapp.jar"));
repackager.setLayers(dynamicLayers);
repackager.repackage(Libraries.NONE);The layers index file (BOOT-INF/layers.idx) uses the following format:
- "dependencies":
- "BOOT-INF/lib/spring-boot-3.2.0.jar"
- "BOOT-INF/lib/logback-classic-1.4.5.jar"
- "spring-boot-loader":
- "org/springframework/boot/loader/"
- "application":
- "BOOT-INF/classes/"
- "META-INF/"# Multi-stage build with layer extraction
FROM eclipse-temurin:17-jre as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
# Final image with optimized layers
FROM eclipse-temurin:17-jre
WORKDIR application
# Copy layers in order of change frequency (least to most)
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"]This layering strategy ensures that:
The result is faster Docker builds and more efficient image distribution.
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-boot--spring-boot-loader-tools