Zademy

Spring Boot 4 y Spring Framework 7: La Nueva Era de Java para Microservicios Rápidos y Seguros

Java
SpringBoot; Java
words palabras

Llevo años trabajando con Spring Boot y, francamente, pocas versiones me han generado tantas ganas de actualizar como la 4.0 (disponible desde noviembre de 2025). Junto con Spring Framework 7, no estamos hablando de un bump de versiones con mejoras incrementales. Es un cambio real en cómo se estructura el framework: más modular, más rápido, más seguro por diseño.

Requisitos mínimos

Spring Boot 4 sube el piso tecnológico. No es opcional.

El mínimo sigue siendo Java 17, pero te recomiendo Java 21 o superior (idealmente Java 25) si quieres aprovechar Virtual Threads, que en la práctica pueden reemplazar buena parte de lo que hacías con WebFlux. La alineación con Jakarta EE 11 es completa, así que Servlet 6.1, JPA 3.2 y Bean Validation 3.1 vienen incluidos. Kotlin necesita la versión 2.2 o superior, y el soporte para GraalVM 24+ es nativo.

Rendimiento: modularidad y AOT

El cambio más importante de esta versión no lo ves a primera vista. Es estructural.

Modularidad del core

Antes, spring-boot-autoconfigure era un artefacto enorme que contenía lógica de configuración para prácticamente todo, incluso cosas que nunca usabas. Eso generaba ruido innecesario en el classpath.

Spring Boot 4 partió ese módulo en artefactos más pequeños y enfocados. La autoconfiguración ahora se distribuye en módulos especializados que solo se cargan cuando los necesitas. En la práctica, esto significa arranque más rápido (menos código que escanear), menor consumo de memoria (solo cargas lo que usas), y un IDE que no te sugiere propiedades irrelevantes. Las imágenes nativas con GraalVM también se benefician, porque hay menos código que analizar.

Compilación AOT y GraalVM

El soporte para GraalVM 24+ está pulido a fondo. El procesamiento AOT mejora en varios frentes: compilación más rápida, menos memoria al arrancar, mejor detección de código muerto, y compatibilidad más sólida con reflection hints.

La anotación @ConfigurationPropertiesSource permite modularizar aún más, procesando solo las propiedades de configuración que tu aplicación realmente necesita.

Seguridad contra nulos con JSpecify

El NullPointerException lleva años siendo el bug más común en producción en Java. Spring Framework 7 y Spring Boot 4 adoptan JSpecify como estándar de null safety en todo el portafolio.

JSpecify es un esfuerzo colaborativo entre Google, JetBrains, Meta, Oracle y otros. La idea es simple: anotar explícitamente qué valores pueden ser nulos y cuáles no. @Nullable marca lo que puede estar ausente. @NonNull garantiza que nunca lo será. @NullMarked establece la regla por defecto para todo un paquete: todo es no-nulo a menos que digas lo contrario.

@NullMarked en acción

Marcas un paquete entero como seguro contra nulos con un archivo package-info.java:

// src/main/java/com/tuempresa/servicio/package-info.java
@org.jspecify.annotations.NullMarked
package com.tuempresa.servicio;

A partir de ahí, tu IDE te avisa mientras escribes si intentas usar algo que podría ser nulo donde no debería. IntelliJ IDEA 2025.3+ lo soporta nativamente.

@NullMarked
package com.ejemplo.usuarios;

public class UsuarioService {

    // El IDE advertirá si intentas pasar null
    public Usuario crearUsuario(@NonNull String nombre, @Nullable String apellido) {
        // nombre nunca es null - seguro de usar directamente
        String nombreCompleto = nombre.toUpperCase();

        // apellido puede ser null - debes verificar
        if (apellido != null) {
            nombreCompleto += " " + apellido.toUpperCase();
        }

        return new Usuario(nombreCompleto);
    }
}

Para mí esto es de las cosas más útiles de la versión. Detectar un NPE mientras escribes, no cuando explota en producción a las 3 AM, cambia bastante la experiencia.

Clientes HTTP declarativos

Spring Boot 4 trae clientes HTTP declarativos nativos. Si alguna vez usaste Spring Cloud OpenFeign, sabes de qué hablo, pero ahora viene integrado en el framework, sin dependencias externas.

RestTemplate quedó oficialmente deprecado en Spring Framework 7 y será removido en versiones futuras. Era verboso, difícil de probar, y no aprovechaba nada del Java moderno. Su reemplazo es RestClient, con una API fluida estilo builder. Pero lo realmente interesante es la capacidad de declarar clientes HTTP con @HttpExchange:

package com.tuempresa.clients;

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

@HttpExchange("/api/productos")
public interface ProductoClient {

    @GetExchange("/{id}")
    Producto obtenerProducto(@PathVariable("id") String id);

    @GetExchange
    List<Producto> listarProductos();

    @PostExchange
    Producto crearProducto(@RequestBody Producto producto);
}
@Configuration
public class ClientConfig {

    @Bean
    public ProductoClient productoClient(RestClient.Builder builder) {
        RestClient restClient = builder
            .baseUrl("https://api.ejemplo.com")
            .build();

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

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

Defines qué llamar, no cómo llamar. Es más fácil de mockear en tests, es type-safe, y no necesitas dependencias adicionales. Además, con el atributo clientType puedes cambiar entre RestClient (por defecto) y WebClient (reactivo) sin tocar el código del cliente.

Resiliencia nativa y versionamiento de APIs

Resiliencia sin dependencias externas

Las anotaciones que antes vivían en Spring Retry ahora son parte del core de Spring Framework 7: @Retryable para reintentos, @ConcurrencyLimit para limitar llamadas concurrentes, y @EnableResilientMethods para activar todo.

@Service
@EnableResilientMethods
public class PagoService {

    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public PagoRespuesta procesarPago(PagoRequest request) {
        // Si falla, se reintenta hasta 3 veces con 1 segundo de espera
        return pagoGateway.procesar(request);
    }

    @ConcurrencyLimit(maxConcurrency = 10)
    public Report generarReporte() {
        // Máximo 10 llamadas concurrentes
        return reportService.generar();
    }
}

Versionamiento de APIs sin dolor

El versionamiento de APIs REST siempre fue un dolor. Spring Framework 7 lo resuelve con el atributo version en @RequestMapping:

@RestController
@RequestMapping("/api/pedidos")
public class PedidoController {

    // Versión 1: Respuesta simple
    @GetMapping(version = "1", produces = MediaType.APPLICATION_JSON_VALUE)
    public List<PedidoV1> getPedidosV1() {
        return pedidoService.obtenerPedidosSimples();
    }

    // Versión 2: Respuesta enriquecida con metadatos
    @GetMapping(version = "2", produces = MediaType.APPLICATION_JSON_VALUE)
    public PedidoResponseV2 getPedidosV2() {
        return pedidoService.obtenerPedidosCompletos();
    }
}
@Configuration
public class ApiVersioningConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        // Estrategia por header
        configurer.apiVersioning(builder -> builder
            .strategy(VersionStrategy.HEADER)
            .headerName("API-Version")
        );

        // O por path: /v1/pedidos, /v2/pedidos
        // O por parámetro de query: /pedidos?version=1
    }
}

La estrategia de versionamiento se configura de forma centralizada. Puedes cambiar entre path, header o query parameter sin reescribir tus controllers. Pequeño detalle, gran calidad de vida.

Observabilidad

Si estás en la nube, necesitas ver qué pasa adentro. Spring Boot 4 mejora bastante aquí.

Micrometer 2.0 viene actualizado, hay un starter oficial de OpenTelemetry incluido, y logs, métricas y trazas se correlacionan automáticamente. La anotación @Observed genera métricas y trazas para cualquier método sin código extra:

@Service
public class InventarioService {

    @Observed(name = "inventario.verificar", contextualName = "verificarDisponibilidad")
    public boolean verificarDisponibilidad(String productoId) {
        // Automáticamente genera métricas de:
        // - Tiempo de ejecución
        // - Número de llamadas
        // - Tasa de errores
        return inventarioRepository.existeStock(productoId);
    }
}

Otra cosa que me pareció acertada: el Actuator ahora monitorea certificados SSL. Si un certificado está por expirar (por defecto, 14-30 días antes), el endpoint /actuator/health lo reporta:

{
  "status": "UP",
  "components": {
    "ssl": {
      "status": "WARNING",
      "details": {
        "certificate": "api.ejemplo.com",
        "expiresIn": "12 days",
        "expiryDate": "2025-12-05"
      }
    }
  }
}

Cuantas veces me habría ahorrado un incidente de producción por un certificado expirado.

El patrón Client reemplaza a los Templates

Si has usado Spring, usaste Templates: JdbcTemplate, RestTemplate, JmsTemplate. Spring Framework 7 está migrando del patrón Template Method a un patrón Client basado en builders e interfaces funcionales.

RestTemplate pasa a RestClient y @HttpExchange (deprecado). JdbcTemplate convive con el nuevo JdbcClient. JmsTemplate tiene su reemplazo en JmsClient.

JdbcClient

El antes y el después se nota mucho en JDBC. Lo que antes era un RowMapper anónimo de 10 líneas ahora es una línea con lambda:

public List<Persona> findAll() {
    return jdbcTemplate.query(
        "SELECT id, nombre, edad FROM persona",
        new RowMapper<Persona>() {
            @Override
            public Persona mapRow(ResultSet rs, int rowNum) throws SQLException {
                Persona p = new Persona();
                p.setId(rs.getLong("id"));
                p.setNombre(rs.getString("nombre"));
                p.setEdad(rs.getInt("edad"));
                return p;
            }
        }
    );
}
public List<Persona> findAll() {
    return jdbcClient.sql("SELECT id, nombre, edad FROM persona")
        .query((rs, rowNum) -> new Persona(
            rs.getLong("id"),
            rs.getString("nombre"),
            rs.getInt("edad")
        ))
        .list();
}

// O incluso más simple con mapeo automático
public List<Persona> findAll() {
    return jdbcClient.sql("SELECT * FROM persona")
        .query(Persona.class)
        .list();
}
public Optional<Persona> findById(Long id) {
    return jdbcClient.sql("SELECT * FROM persona WHERE id = :id")
        .param("id", id)
        .query(Persona.class)
        .optional();
}
public int actualizarEdad(Long id, int nuevaEdad) {
    return jdbcClient.sql("UPDATE persona SET edad = :edad WHERE id = :id")
        .param("edad", nuevaEdad)
        .param("id", id)
        .update();
}

Más conciso, aprovecha lambdas, se integra mejor con Optional, y mejora el soporte para AOT y GraalVM.

Testing

Hay nuevos test slices para aislar componentes sin levantar todo el contexto:

@HttpServiceClientTest
class ProductoClientTest {

    @Autowired
    private ProductoClient productoClient;

    @Test
    void deberiaObtenerProducto() {
        // Solo carga la configuración necesaria para clientes HTTP
        Producto producto = productoClient.obtenerProducto("123");
        assertThat(producto).isNotNull();
    }
}

TestRestTemplate evoluciona a TestRestClient con mejor tipado:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class IntegrationTest {

    @Autowired
    private TestRestClient restClient;

    @Test
    void deberiaCrearUsuario() {
        Usuario nuevoUsuario = new Usuario("Juan", "Pérez");

        Usuario creado = restClient.post()
            .uri("/api/usuarios")
            .body(nuevoUsuario)
            .exchange()
            .expectStatus().isCreated()
            .expectBody(Usuario.class)
            .returnResult();

        assertThat(creado.getId()).isNotNull();
    }
}

Migrar desde Spring Boot 3

La migración no es trivial, pero tampoco es un infierno. Los cambios obligatorios son cuatro: Jackson pasa de 2.x a 3.x, el namespace migra completamente de javax.* a jakarta.*, el mínimo es Java 17 (recomiendo 21+), y Kotlin necesita 2.2+.

Las deprecaciones que más impacto tienen: RestTemplate pasa a RestClient o clientes declarativos, las configuraciones con lambdas son obligatorias en Spring Security, y varias propiedades cambiaron de nombre.

Spring recomienda OpenRewrite para automatizar buena parte del trabajo:

<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>5.42.0</version>
    <configuration>
        <activeRecipes>
            <recipe>org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0</recipe>
        </activeRecipes>
    </configuration>
</plugin>

Este plugin actualiza dependencias, cambia namespaces de javax a jakarta, migra APIs deprecadas y ajusta archivos de configuración. No hace todo, pero se lleva gran parte del trabajo mecánico.

¿Vale la pena migrar?

Sí. La modularización y AOT mejoran los tiempos de arranque y el consumo de memoria de forma tangible. JSpecify reduce los NPE. Las APIs declarativas eliminan boilerplate. Los patrones de resiliencia vienen nativos. La observabilidad es de primera clase. Y todo está optimizado para contenedores y cloud.

No es un upgrade que hagas en una tarde. Pero si estás construyendo algo serio, esta es la base sobre la que vas a trabajar los próximos años. Merece el esfuerzo.