Novità introdotte in Java Spring Boot 4

Rilasciato a novembre 2025, Spring Boot 4.0 rappresenta l'inizio di una nuova generazione del framework, costruita sulle fondamenta di Spring Framework 7 e Jakarta EE 11. Non si tratta di un semplice aggiornamento incrementale: la modularizzazione completa del codice, il supporto di prima classe a Java 25, l'introduzione di JSpecify per la null safety, il versionamento nativo delle API e i nuovi client HTTP dichiarativi ridefiniscono il modo in cui scriviamo applicazioni cloud-native in Java. In questo articolo analizzeremo nel dettaglio le principali novità, accompagnandole con esempi di codice pratici.

Baseline aggiornata: Java 17, Java 25 e Jakarta EE 11

Spring Boot 4 richiede come requisito minimo Java 17, ma offre supporto di prima classe a Java 25, l'ultima release LTS al momento del rilascio. Le note di migrazione raccomandano esplicitamente di adottare Java 25 per beneficiare delle ultime ottimizzazioni della JVM, dei garbage collector moderni e delle funzionalità linguistiche più recenti. Anche Kotlin riceve un aggiornamento significativo: la baseline minima passa a Kotlin 2.2.

L'allineamento a Jakarta EE 11 comporta l'aggiornamento dei container Servlet (Tomcat 11, Jetty 12.1, Undertow 2.3) e l'adozione delle nuove API jakarta.* in tutte le specifiche supportate. Un esempio minimale di file pom.xml per un progetto Spring Boot 4 con Java 25 si presenta così:

<project>
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-app</artifactId>
    <version>1.0.0</version>

    <properties>
        <!-- Versione di Java richiesta dal progetto -->
        <java.version>25</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

Modularizzazione completa del codice

Una delle novità architetturali più importanti è la modularizzazione completa del codice di Spring Boot. I JAR monolitici del passato sono stati suddivisi in artefatti più piccoli e mirati, ciascuno con una responsabilità ben definita. Questo si traduce in tre vantaggi concreti: minore impronta in memoria, classpath più puliti e tempi di avvio ridotti, soprattutto in ambienti containerizzati e serverless.

Per gli sviluppatori, l'impatto pratico è limitato: gli starter continuano a funzionare come prima e si occupano di importare i moduli necessari. Tuttavia, per chi importa dipendenze in modo manuale, alcune classi sono state spostate in moduli più specifici. Per esempio, se in passato si importavano alcune utilità direttamente da spring-boot, ora potrebbero trovarsi in moduli dedicati come spring-boot-restclient, spring-boot-jms o spring-boot-tx.

JSpecify per la null safety a livello di portfolio

Una delle innovazioni più rilevanti dal punto di vista della qualità del codice è l'adozione, in tutto il portfolio Spring, delle annotazioni JSpecify per la null safety. Le annotazioni proprietarie come @Nullable e @NonNull di Spring sono state sostituite dalle equivalenti standard di JSpecify, garantendo interoperabilità con strumenti di analisi statica come IntelliJ IDEA, Checker Framework e NullAway.

Per usufruire delle annotazioni nei propri progetti, basta aggiungere la libreria JSpecify alle dipendenze:

<dependency>
    <groupId>org.jspecify</groupId>
    <artifactId>jspecify</artifactId>
    <version>1.0.0</version>
</dependency>

Una volta aggiunta la dipendenza, possiamo annotare i metodi e i parametri in modo da rendere esplicito il contratto sui valori nulli:

package com.example.demo.service;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.springframework.stereotype.Service;

// L'annotazione marca tutti i tipi del package come non-null per default
@NullMarked
@Service
public class UserService {

    // Il parametro non puo' essere null: l'IDE segnalera' un errore
    public String formatUsername(String username) {
        return username.trim().toLowerCase();
    }

    // Il valore di ritorno puo' essere null in modo esplicito
    public @Nullable String findEmail(Long userId) {
        if (userId == null || userId < 0) {
            return null;
        }
        // Logica di recupero dell'email
        return "user@example.com";
    }
}

Con questa configurazione, l'IDE evidenzierà in tempo reale qualsiasi tentativo di passare null a formatUsername o di dereferenziare il risultato di findEmail senza un controllo preventivo, prevenendo i classici NullPointerException già durante la fase di scrittura del codice.

API Versioning nativo

Una delle novità più attese è il supporto nativo al versionamento delle API REST. Prima di Spring Boot 4, gestire più versioni di un endpoint significava duplicare i controller, manipolare manualmente gli header oppure adottare strategie basate sui path. Spring Framework 7 introduce un attributo version direttamente nelle annotazioni @RequestMapping, @GetMapping, @PostMapping e simili.

Il primo passo consiste nel configurare la strategia di versionamento nel file application.properties:

# Strategia basata su header HTTP personalizzato
spring.mvc.apiversion.use.header=X-API-Version

# In alternativa, strategia basata su query parameter
# spring.mvc.apiversion.use.query=version

# Versione predefinita quando il client non la specifica
spring.mvc.apiversion.default=1

A questo punto possiamo dichiarare più versioni dello stesso endpoint nello stesso controller:

package com.example.demo.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    // Versione 1: restituisce solo nome e prezzo
    @GetMapping(value = "/{id}", version = "1")
    public ResponseEntity<Map<String, Object>> getProductV1(@PathVariable Long id) {
        Map<String, Object> product = Map.of(
            "id", id,
            "name", "Smartphone",
            "price", 599.00
        );
        return ResponseEntity.ok(product);
    }

    // Versione 2: aggiunge campi descrittivi e disponibilita'
    @GetMapping(value = "/{id}", version = "2")
    public ResponseEntity<Map<String, Object>> getProductV2(@PathVariable Long id) {
        Map<String, Object> product = Map.of(
            "id", id,
            "name", "Smartphone",
            "price", 599.00,
            "description", "Smartphone di ultima generazione",
            "available", true
        );
        return ResponseEntity.ok(product);
    }
}

Il client potrà selezionare la versione desiderata aggiungendo l'header X-API-Version: 2 alla richiesta, e Spring si occuperà automaticamente di instradare la chiamata al metodo corretto. Questo approccio elimina la duplicazione dei controller e rende l'evoluzione delle API molto più gestibile nel tempo.

HTTP Service Clients dichiarativi

Spring Boot 4 promuove a feature di prima classe i client HTTP dichiarativi basati sull'annotazione @HttpExchange. L'idea è simile a quella di Feign in Spring Cloud: dichiariamo un'interfaccia, annotiamo i metodi con le mappature HTTP, e Spring genera automaticamente l'implementazione che effettua le chiamate REST.

Vediamo un esempio completo. Definiamo prima l'interfaccia che descrive il client:

package com.example.demo.client;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

import java.util.List;
import java.util.Map;

// Interfaccia dichiarativa: nessuna implementazione manuale richiesta
@HttpExchange(url = "https://api.example.com")
public interface CatalogClient {

    @GetExchange("/products")
    List<Map<String, Object>> listProducts();

    @GetExchange("/products/{id}")
    Map<String, Object> getProduct(@PathVariable Long id);

    @PostExchange("/products")
    Map<String, Object> createProduct(@RequestBody Map<String, Object> payload);
}

Quindi configuriamo il client come bean Spring usando un RestClient e l'HttpServiceProxyFactory:

package com.example.demo.config;

import com.example.demo.client.CatalogClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
public class HttpClientsConfig {

    // Configurazione del client HTTP dichiarativo
    @Bean
    public CatalogClient catalogClient() {
        RestClient restClient = RestClient.builder()
            .baseUrl("https://api.example.com")
            .build();

        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
            .builderFor(adapter)
            .build();

        return factory.createClient(CatalogClient.class);
    }
}

L'interfaccia CatalogClient diventa così iniettabile come qualsiasi altro bean Spring, e l'implementazione viene generata in modo trasparente. Spring Cloud Commons 5.0 si integra perfettamente con questo meccanismo, aggiungendo supporto per Circuit Breaker e load balancing tramite lo schema lb:// direttamente nelle URL dichiarative.

OpenTelemetry come starter ufficiale

Spring Boot 4 introduce un nuovo starter dedicato a OpenTelemetry: spring-boot-starter-opentelemetry. Questo starter porta con sé tutte le dipendenze necessarie per esportare metriche e tracce tramite il protocollo OTLP, e configura automaticamente l'SDK di OpenTelemetry. Combinato con l'aggiornamento a Micrometer 2.0, il framework offre ora un'osservabilità completa, end-to-end, integrabile con backend come Grafana, Prometheus, Jaeger e Zipkin.

Per attivare l'export OTLP è sufficiente aggiungere la dipendenza e configurare l'endpoint del collector:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
# Endpoint del collector OTLP (es. OpenTelemetry Collector o Grafana Tempo)
management.opentelemetry.endpoint=http://localhost:4318

# Nome del servizio mostrato nei sistemi di tracing
management.opentelemetry.resource-attributes.service.name=demo-app

# Esportazione delle metriche e delle tracce
management.tracing.sampling.probability=1.0
management.metrics.export.otlp.enabled=true

Inoltre, l'auto-configurazione di Micrometer ora supporta l'annotazione @MeterTag sui metodi annotati con @Counted e @Timed, sfruttando un risolutore di espressioni basato su SpEL per generare etichette dinamiche.

RestTestClient: un nuovo client di test non reattivo

Per i test di integrazione, Spring Boot 4 introduce RestTestClient, una controparte non reattiva di WebTestClient. Questo nuovo client offre un'API fluente, integrazione con AssertJ, e può essere collegato a un server live, a MockMvc o a una RouterFunction. Viene auto-configurato all'interno dei test annotati con @SpringBootTest.

Ecco un esempio di test che verifica un endpoint REST utilizzando RestTestClient:

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.client.RestTestClient;

import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

@SpringBootTest(webEnvironment = RANDOM_PORT)
class ProductControllerTest {

    // Iniezione del client di test auto-configurato
    @Autowired
    private RestTestClient restTestClient;

    @Test
    void shouldReturnProductV2WithDescription() {
        // Esecuzione della richiesta e verifica fluente del risultato
        restTestClient.get()
            .uri("/api/products/1")
            .header("X-API-Version", "2")
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$.name").isEqualTo("Smartphone")
            .jsonPath("$.description").exists()
            .jsonPath("$.available").isEqualTo(true);
    }
}

Bean Registrar: registrazione programmatica di bean

Spring Framework 7, e di conseguenza Spring Boot 4, introduce il contratto BeanRegistrar, un'alternativa moderna a BeanDefinitionRegistryPostProcessor. Questo nuovo meccanismo consente di registrare più bean in modo programmatico, gestendo scenari complessi che i metodi @Bean tradizionali non possono esprimere in modo elegante.

package com.example.demo.config;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanRegistrar;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(DynamicHandlersRegistrar.class)
public class DynamicHandlersConfig {
}

// Registrazione programmatica di piu' bean simili in un solo passaggio
class DynamicHandlersRegistrar implements BeanRegistrar {

    @Override
    public void register(BeanDefinitionRegistry registry) {
        // Iterazione su una lista di handler da registrare dinamicamente
        for (String handlerName : new String[]{"emailHandler", "smsHandler", "pushHandler"}) {
            RootBeanDefinition definition = new RootBeanDefinition(NotificationHandler.class);
            definition.getConstructorArgumentValues()
                      .addGenericArgumentValue(handlerName);
            registry.registerBeanDefinition(handlerName, definition);
        }
    }
}

Spring Security: server di autorizzazione integrato e MFA

Spring Security riceve aggiornamenti significativi in Spring Boot 4. Lo Spring Authorization Server, che fornisce un'implementazione completa di OAuth 2.0, è ora parte integrante di Spring Security stesso, eliminando la necessità di una dipendenza separata. Il PKCE è abilitato di default per i client pubblici, in linea con le best practice di sicurezza moderne.

Tra le altre novità troviamo il supporto integrato all'autenticazione multi-fattore (MFA) per le applicazioni servlet, con una pagina di login predefinita che mostra fattori aggiuntivi in base ai parametri factor.type e factor.reason. Sono inoltre disponibili nuovi encoder basati su Password4j per Argon2, BCrypt, SCrypt, PBKDF2 e Balloon Hashing, alternative moderne e robuste per lo storage delle password.

package com.example.demo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.Password4jPasswordEncoder;

@Configuration
public class PasswordEncoderConfig {

    // Encoder basato su Argon2id, raccomandato per nuove applicazioni
    @Bean
    public PasswordEncoder passwordEncoder() {
        return Password4jPasswordEncoder.argon2();
    }
}

Supporto migliorato per GraalVM e immagini native

Spring Boot 4 prosegue il percorso verso il supporto completo a GraalVM ed è pienamente allineato a GraalVM 24. Il processo di Ahead-of-Time (AOT) è stato potenziato, riducendo i tempi di build e l'impronta di memoria all'avvio. Spring Data introduce inoltre i repository AOT: i metodi di query vengono trasformati in codice sorgente durante la fase AOT e compilati insieme al resto dell'applicazione, eliminando l'overhead di interpretazione runtime.

Per costruire un'immagine nativa è sufficiente eseguire il comando Maven:

# Compilazione dell'applicazione come immagine nativa GraalVM
./mvnw -Pnative native:compile

# Esecuzione del binario nativo prodotto
./target/demo-app

JmsClient: la nuova API per JMS

L'auto-configurazione per JMS introduce il supporto alla nuova API JmsClient, che offre un approccio più moderno e fluente rispetto a JmsTemplate. Quest'ultimo rimane comunque supportato per garantire la retrocompatibilità.

package com.example.demo.messaging;

import jakarta.jms.ConnectionFactory;
import org.springframework.jms.core.JmsClient;
import org.springframework.stereotype.Service;

@Service
public class OrderEventPublisher {

    private final JmsClient jmsClient;

    public OrderEventPublisher(ConnectionFactory connectionFactory) {
        // Costruzione di un JmsClient a partire dalla ConnectionFactory
        this.jmsClient = JmsClient.create(connectionFactory);
    }

    public void publishOrderCreated(Long orderId) {
        // API fluente per l'invio del messaggio
        jmsClient.destination("orders.created")
                 .withTimeToLive(60_000)
                 .send("Ordine creato: " + orderId);
    }
}

Altre migliorie degne di nota

Tra i numerosi miglioramenti minori, ne segnaliamo alcuni particolarmente rilevanti. Il supporto a Redis include ora l'auto-configurazione di una topologia Static Master/Replica tramite la proprietà spring.data.redis.masterreplica.nodes, supportata esclusivamente dal client Lettuce. L'auto-configurazione di Redis è stata inoltre aggiornata per integrare MicrometerTracing al posto del precedente MicrometerCommandLatencyRecorder, fornendo sia metriche che span basati sull'API Observation.

Le proprietà di configurazione ora supportano l'annotazione @ConfigurationPropertiesSource per esporre tipi definiti in moduli diversi, con metadati generati automaticamente dall'annotation processor. Il supporto a Testcontainers per MongoDB include ora MongoDBAtlasLocalContainer tramite @ServiceConnection, semplificando ulteriormente i test di integrazione.

Spring Cloud 2025.1 (nome in codice "Oakwood") è il release train allineato con Spring Boot 4.0 e adotta Jakarta EE 11, Jackson 3 come libreria di serializzazione predefinita e le annotazioni JSpecify in tutto il portfolio. Spring Cloud Gateway 5.0 introduce un predicato di versionamento delle API per Server WebFlux, integrandosi con il modello di versionamento di Spring Framework 7 per instradare le richieste verso il servizio upstream corretto.

Conclusioni

Spring Boot 4 non è una semplice evoluzione, ma una trasformazione strutturale del framework. La modularizzazione, il versionamento nativo delle API, la null safety basata su JSpecify, i client HTTP dichiarativi, l'integrazione con OpenTelemetry e il rinnovato supporto a GraalVM compongono un'offerta moderna, pensata per applicazioni cloud-native scalabili e manutenibili. La migrazione da Spring Boot 3.5 richiede attenzione, in particolare per quanto riguarda le dipendenze spostate nei nuovi moduli e le deprecazioni introdotte, ma i benefici in termini di produttività, performance e qualità del codice rendono l'aggiornamento un investimento ampiamente giustificato per qualsiasi team che voglia mantenere il proprio stack al passo con i tempi.