Aller au contenu

Construction & packaging Java

La chaîne de build Java repose historiquement sur Maven, avec Gradle comme alternative performante. Au-delà du JAR classique, GraalVM native-image permet de compiler en binaire natif pour les microservices a démarrage ultra-rapide. Cette section couvre les outils de build, le packaging, la containerisation et un exemple de pipeline CI/CD.


Maven vs Gradle

Critère Maven Gradle
Syntaxe XML (verbeux mais predictible) Kotlin DSL ou Groovy DSL (concis)
Performance Build incremental limite Build incremental, cache de build avance
Conventions Très strictes (convention over conf.) Flexibles mais necessitent discipline
Écosystème plugins Très large, mature Large, croissant
Multi-modules Hérité, fonctionne bien Natif, plus performant
Android Non Oui (outil officiel Android)
Adoption entreprise Très haute Haute, en progression

Quel outil choisir

Pour un nouveau projet Spring Boot enterprise, Maven reste le choix le plus safe (meilleure documentation, plugins Spring matures). Gradle est preferable pour les projets multi-modules complexes ou les contraintes de performance de build (monorepos, projets Android/JVM mixtes).


Structure Maven (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- Heritage Spring Boot — gere les versions des dependances -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>21</java.version>
        <testcontainers.version>1.20.0</testcontainers.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Plugin Spring Boot — package en fat JAR executable -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Commandes Maven essentielles

# Compiler le projet
mvn compile

# Executer les tests
mvn test

# Package en JAR (compile + test + package)
mvn package

# Package sans tests
mvn package -DskipTests

# Installer dans le repository local (~/.m2)
mvn install

# Nettoyer les artefacts de build
mvn clean

# Pipeline complet
mvn clean verify

# Afficher l'arbre de dependances
mvn dependency:tree

Structure Gradle (build.gradle.kts)

// build.gradle.kts — Kotlin DSL (recommande pour les nouveaux projets)
plugins {
    java
    id("org.springframework.boot") version "3.4.0"
    id("io.spring.dependency-management") version "1.1.6"
}

group = "com.example"
version = "1.0.0-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.withType<Test> {
    useJUnitPlatform()
}
# Commandes Gradle equivalentes
./gradlew build        # compile + test + package
./gradlew test         # tests uniquement
./gradlew bootRun      # demarrer l'application
./gradlew dependencies # arbre de dependances

JAR executable et WAR

Fat JAR (Uber JAR)

Spring Boot produit par défaut un fat JAR (ou uber JAR) qui contient l'application et toutes ses dépendances, executable directement.

mvn package
java -jar target/demo-1.0.0-SNAPSHOT.jar

# Avec profil de configuration
java -jar target/demo-1.0.0-SNAPSHOT.jar --spring.profiles.active=prod

# Avec variables d'environnement
SPRING_DATASOURCE_URL=jdbc:postgresql://prod-db:5432/mydb \
java -jar target/demo-1.0.0-SNAPSHOT.jar

WAR pour serveur d'applications

<!-- pom.xml — changer le packaging en WAR -->
<packaging>war</packaging>

<dependencies>
    <!-- Exclure Tomcat embarque (fourni par le serveur externe) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

GraalVM native-image

GraalVM permet de compiler une application Java en binaire natif : pas de JVM nécessaire à l'exécution, démarrage en quelques millisecondes, empreinte mémoire réduite.

Configuration (pom.xml)

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <!-- Inclus par le parent Spring Boot 3 -->
</plugin>
# Compilation native (necessite GraalVM JDK installe)
mvn -Pnative native:compile

# Executer le binaire natif
./target/demo

# Sans GraalVM local : build dans un conteneur Docker
mvn -Pnative spring-boot:build-image

Réflexion et configuration native

GraalVM native-image analyse statiquement le code et ne peut pas gérer la réflexion dynamique par défaut. Spring Boot 3 configure automatiquement la réflexion pour ses composants, mais des annotations sont parfois nécessaires.

import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;

// Enregistre la classe pour la reflexion en mode natif
@RegisterReflectionForBinding(CommandeCreeeEvent.class)
@Configuration
public class NativeConfig {
    // Configuration specifique native si necessaire
}

Limitations native-image

Certaines fonctionnalités Java dynamiques (réflexion, proxies dynamiques, chargement de classes à chaud) necessitent une configuration explicite pour GraalVM. Spring Boot 3 géré la majorité des cas, mais des bibliotheques tierces peu connues peuvent poser problème. Testez toujours le binaire natif en CI.


Dockerfile multi-stage

Un Dockerfile multi-stage séparé la phase de build (JDK complet) de la phase d'exécution (JRE léger), reduisant la taille de l'image finale.

# ---- Stage 1 : Build avec Maven et JDK complet ----
FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /app

# Copier les fichiers de dependances en premier (cache Docker)
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
RUN ./mvnw dependency:go-offline -B

# Copier les sources et compiler
COPY src src
RUN ./mvnw package -DskipTests -B

# ---- Stage 2 : Runtime avec JRE leger ----
FROM eclipse-temurin:21-jre-alpine AS runtime

# Utilisateur non-root pour la securite
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

WORKDIR /app

# Copier uniquement le JAR depuis le stage builder
COPY --from=builder /app/target/*.jar app.jar

# Port expose par Spring Boot
EXPOSE 8080

# Point d'entree — options JVM recommandees
ENTRYPOINT ["java", \
    "-XX:+UseContainerSupport", \
    "-XX:MaxRAMPercentage=75.0", \
    "-jar", "app.jar"]
# Build et run de l'image
docker build -t demo-app:1.0 .
docker run -p 8080:8080 \
    -e SPRING_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/demo \
    demo-app:1.0

Jlink (depuis Java 9) permet de créer un JRE minimal contenant uniquement les modules nécessaires à l'application.

# Identifier les modules requis par l'application
jdeps --multi-release 21 --ignore-missing-deps \
    --print-module-deps target/demo.jar

# Creer un JRE minimal avec uniquement les modules necessaires
jlink \
    --module-path $JAVA_HOME/jmods \
    --add-modules java.base,java.logging,java.net.http,java.sql \
    --output jre-custom \
    --strip-debug \
    --compress=2 \
    --no-header-files \
    --no-man-pages

# Taille comparee
du -sh $JAVA_HOME          # ~300 MB — JDK complet
du -sh jre-custom          # ~50-80 MB — JRE minimal

Exemple de pipeline CI/CD (GitHub Actions)

# .github/workflows/ci.yml
name: CI/CD Java

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    services:
      # PostgreSQL pour les tests d'integration Testcontainers
      # (ou laisser Testcontainers gerer Docker directement)
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Java 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: maven  # Cache du repository Maven (~/.m2)

      - name: Build et tests
        run: mvn clean verify -B

      - name: Rapport de couverture JaCoCo
        uses: codecov/codecov-action@v4
        with:
          files: target/site/jacoco/jacoco.xml

      - name: Build image Docker
        if: github.ref == 'refs/heads/main'
        run: |
          docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}