Skip to content

🎯 Meta: conocer el stack empresarial por excelencia. Spring Boot es el framework dominante en grandes empresas, banca y sistemas corporativos. Si te interesa el mundo "enterprise", los sueldos altos de backend corporativo suelen pedir Java + Spring.

Versiones: Java 25 (LTS) · Spring Boot 4.1 · Maven/Gradle.


J.1 · ¿Por qué Java + Spring Boot?

  • El estándar corporativo: bancos, aseguradoras, gobiernos, grandes empresas. Décadas de estabilidad y una comunidad gigantesca.
  • Ecosistema maduro: para cualquier necesidad (seguridad, mensajería, batch, cloud) hay un módulo de Spring probado en producción a escala masiva.
  • Muy demandado y bien pagado, sobre todo en entornos empresariales.
  • Java moderno (25) ya no es verboso como antaño: records, var, virtual threads, pattern matching. Se parece más a lo que ya conoces.

🧠 Spring Boot es a Java lo que Laravel a PHP: "baterías incluidas". La inyección de dependencias que viste en NestJS (cap. 06) es, de hecho, herencia de Spring — Spring la popularizó hace 20 años. Reconocerás muchísimos patrones: controllers, services, repositories, DTOs. No partes de cero.


J.2 · Instalación y arranque

Necesitas JDK 25. La forma más fácil de crear un proyecto es start.spring.io (Spring Initializr): eliges dependencias y te descarga el proyecto listo.

bash
java --version          # 25.x

# Con Spring Boot CLI o Initializr, seleccionas: Web, JPA, PostgreSQL, Validation
# Luego:
./mvnw spring-boot:run   # Maven wrapper (o ./gradlew bootRun con Gradle)
# Arranca en http://localhost:8080

Dependencias típicas: spring-boot-starter-web (API REST), spring-boot-starter-data-jpa (BD), postgresql, spring-boot-starter-validation.


J.3 · Estructura y capas

Spring impone la arquitectura en capas (cap. 11) que ya conoces:

src/main/java/com/tienda/
├── TiendaApplication.java      ← punto de entrada (@SpringBootApplication)
├── controller/                 ← REST controllers (capa de presentación)
├── service/                    ← lógica de negocio
├── repository/                 ← acceso a datos (Spring Data JPA)
├── model/ (entity)             ← entidades (mapean a tablas)
└── dto/                        ← objetos de entrada/salida

J.4 · Entidad (JPA) y record DTO

JPA/Hibernate es el ORM (equivalente a Eloquent/Prisma). Una entidad mapea a una tabla:

java
// model/Producto.java
import jakarta.persistence.*;

@Entity
@Table(name = "productos")
public class Producto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String nombre;

    @Column(nullable = false)
    private BigDecimal precio;     // BigDecimal para dinero (nunca double)

    private Integer stock = 0;

    // getters y setters... (o usa Lombok @Data para evitarlos)
}

Los DTOs con record (Java moderno, inmutable y conciso) + validación:

java
// dto/CrearProductoDto.java
import jakarta.validation.constraints.*;
import java.math.BigDecimal;

public record CrearProductoDto(
    @NotBlank String nombre,
    @NotNull @DecimalMin("0.0") BigDecimal precio,
    @Min(0) Integer stock
) {}

💡 record cambió Java para mejor. Antes, un DTO eran 40 líneas de getters, setters, constructor, equals y hashCode. Ahora es una línea. Java 25 es mucho menos verboso que su fama; gran parte de esa fama viene de código antiguo.


J.5 · Repository (Spring Data — magia real)

Spring Data JPA genera las consultas a partir del nombre del método. No escribes SQL para lo común:

java
// repository/ProductoRepository.java
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductoRepository extends JpaRepository<Producto, Long> {
    // Spring implementa estos métodos SOLO por su nombre:
    List<Producto> findByActivoTrue();
    List<Producto> findByPrecioLessThan(BigDecimal precio);
    Optional<Producto> findByNombre(String nombre);
}

Con solo declarar la interfaz obtienes save(), findById(), findAll(), deleteById(), paginación, y las consultas derivadas del nombre. Cero implementación.

🧠 Consultas derivadas del nombre: findByActivoTrueAndPrecioLessThan(...) → Spring genera el WHERE activo = true AND precio < ? automáticamente. Es de lo más productivo del ecosistema. Para consultas complejas usas @Query con JPQL o SQL nativo.


J.6 · Service y Controller

java
// service/ProductoService.java
import org.springframework.stereotype.Service;

@Service
public class ProductoService {
    private final ProductoRepository repo;

    // Inyección de dependencias por constructor (como NestJS, cap. 06)
    public ProductoService(ProductoRepository repo) {
        this.repo = repo;
    }

    public Producto crear(CrearProductoDto dto) {
        Producto p = new Producto();
        p.setNombre(dto.nombre());
        p.setPrecio(dto.precio());
        p.setStock(dto.stock() != null ? dto.stock() : 0);
        return repo.save(p);
    }

    public Producto buscar(Long id) {
        return repo.findById(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No existe"));
    }
}
java
// controller/ProductoController.java
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/productos")
public class ProductoController {
    private final ProductoService service;

    public ProductoController(ProductoService service) {
        this.service = service;
    }

    @GetMapping
    public List<Producto> listar() {
        return service.listar();
    }

    @GetMapping("/{id}")
    public Producto ver(@PathVariable Long id) {
        return service.buscar(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Producto crear(@Valid @RequestBody CrearProductoDto dto) {   // @Valid activa la validación
        return service.crear(dto);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void borrar(@PathVariable Long id) {
        service.borrar(id);
    }
}

🧠 Reconoces todo, ¿verdad? @RestController, @GetMapping, @RequestBody, @Valid, inyección por constructor... Es el mismo modelo mental de NestJS (que se inspiró en Spring). Cambia la sintaxis (anotaciones de Java), no los conceptos. Por eso dejamos Spring para el final: llegas con todo el vocabulario aprendido.


J.7 · Configuración

Spring usa application.yml (o .properties):

yaml
# src/main/resources/application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/tienda
    username: postgres
    password: ${DB_PASSWORD}        # de variable de entorno (apéndice C)
  jpa:
    hibernate:
      ddl-auto: validate            # en prod: valida, no modifiques el esquema solo
    show-sql: true
server:
  port: 8080

💡 Para migraciones de esquema en Spring, usa Flyway o Liquibase (equivalentes a las migraciones de Laravel/Prisma). Nunca dejes ddl-auto: update en producción — puede alterar tu esquema de formas inesperadas. Usa validate + migraciones versionadas.


J.8 · Tests (JUnit + Spring Boot Test)

Spring trae un framework de testing potentísimo. Test de integración de un endpoint:

java
// src/test/java/.../ProductoControllerTest.java
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class ProductoControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    void creaProductoValido() throws Exception {
        mvc.perform(post("/api/productos")
                .contentType("application/json")
                .content("{\"nombre\":\"Mouse\",\"precio\":49.9}"))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.nombre").value("Mouse"));
    }

    @Test
    void rechazaSinNombre() throws Exception {
        mvc.perform(post("/api/productos")
                .contentType("application/json")
                .content("{\"precio\":10}"))
            .andExpect(status().isBadRequest());       // 400 por @Valid
    }
}

Test unitario de un service con Mockito (mocks, cap. 09):

java
@ExtendWith(MockitoExtension.class)
class ProductoServiceTest {
    @Mock ProductoRepository repo;
    @InjectMocks ProductoService service;

    @Test
    void buscaInexistenteLanza404() {
        when(repo.findById(99L)).thenReturn(Optional.empty());
        assertThrows(ResponseStatusException.class, () -> service.buscar(99L));
    }
}
bash
./mvnw test          # ejecuta los tests

💡 Para tests de integración con BD real, Spring se integra de maravilla con Testcontainers (cap. 09): levanta un PostgreSQL en Docker para los tests con @Testcontainers. Es el estándar de oro del testing en Java.


J.9 · Spring Security: autenticación y autorización

Spring Security es el módulo que resuelve lo que el cap. 20 (autenticación fullstack) cubrió en TypeScript/Python: login, JWT, y qué rutas requieren qué permisos. Se configura declarando una SecurityFilterChain — el equivalente al middleware de auth de NestJS/FastAPI, pero como cadena de filtros:

bash
# Añade la dependencia (Spring Initializr: "Spring Security")
spring-boot-starter-security
java
// config/SecurityConfig.java
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())                          // API stateless: JWT, no cookies de sesión
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()        // login/registro, público
                .requestMatchers(HttpMethod.GET, "/api/productos/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")  // autorización por rol
                .anyRequest().authenticated())
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();       // el mismo bcrypt del cap. 20 y el ap. C
    }
}
java
// El filtro JWT — valida el token en cada request, como el guard de NestJS (cap. 06/20)
public class JwtAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        String token = extraerToken(req);                    // cabecera Authorization: Bearer ...
        if (token != null && jwtService.esValido(token)) {
            String email = jwtService.extraerEmail(token);
            var auth = new UsernamePasswordAuthenticationToken(email, null, List.of());
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(req, res);
    }
}
java
// Autorización a nivel de método — más granular que la cadena de filtros
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
@DeleteMapping("/{id}")
public void borrar(@PathVariable Long id) { ... }

🧠 El vocabulario cambia, el modelo no. SecurityFilterChain ≈ middleware de auth (cap. 20); PasswordEncoderbcrypt.hash (ap. C); @PreAuthorize ≈ guards de NestJS con roles. Spring Security tiene fama de verboso en su configuración, pero resuelve exactamente los mismos problemas que ya viste: hash de contraseñas, emisión/validación de JWT, y control de acceso por ruta o por recurso.

⚠️ Spring Security 7 (con Spring Boot 4.1) añade mitigación SSRF de serie (InetAddressFilter en los clientes HTTP) — relevante si tu backend llama a URLs que vienen de datos de usuario (webhooks salientes, ap. D.5, o adjuntos remotos). Actívalo si tu app hace ese tipo de llamadas.


J.10 · Virtual Threads y despliegue

Java 25 trae virtual threads (Project Loom): concurrencia masiva y barata, parecida a las goroutines de Go (cap. 08). Spring Boot 4 los aprovecha para manejar muchísimas peticiones concurrentes con código sencillo y bloqueante.

Despliegue: Spring Boot empaqueta todo en un JAR ejecutable (java -jar app.jar). En Docker, usa una imagen JRE ligera:

dockerfile
FROM eclipse-temurin:25-jre-alpine
COPY target/tienda-api.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

🧠 Java pesa más en RAM que Go o Rust (la JVM tiene su coste), pero a cambio da un ecosistema inigualable, tooling maduro y rendimiento excelente bajo carga sostenida (el JIT optimiza en caliente). Para sistemas empresariales grandes y de larga vida, sigue siendo una elección sólida.


J.11 · Cuándo elegir Spring Boot

Úsalo para: sistemas empresariales grandes, banca/finanzas/seguros, equipos que ya conocen Java, proyectos de larga vida donde la estabilidad y el ecosistema importan más que la agilidad de un startup.

Piénsatelo si: quieres prototipar muy rápido o el proyecto es pequeño (Laravel/FastAPI serán más ágiles y ligeros).

🧠 Reflexión de cierre: Spring Boot cierra el círculo del libro perfectamente. Empezaste con Laravel (todo incluido), y terminas con su primo empresarial. Los conceptos —MVC, DI, repositories, DTOs, validación, tests— son los mismos que viste en 7 stacks distintos. Esa es la gran lección: domina los conceptos y cualquier framework nuevo es solo sintaxis nueva.


✅ Ejercicio del apéndice

1. Genera un proyecto en start.spring.io con Web, JPA, PostgreSQL, Validation.
2. Crea la API del blog: entidad Articulo, DTO con record + validación,
   repository, service, controller (CRUD completo).
3. Usa una consulta derivada del nombre (ej. findByPublicadoTrue).
4. Configura la conexión a PostgreSQL y añade migraciones con Flyway.
5. Tests: integración con MockMvc + unitario del service con Mockito.
   (Bonus: integración con Testcontainers.)
6. Añade Spring Security: login que emite un JWT, un filtro que lo valida, y
   protege /api/admin/** con hasRole("ADMIN") y un endpoint de borrado con
   @PreAuthorize comprobando que el usuario es dueño del recurso o admin.
7. Empaqueta el JAR, dockerízalo y arráncalo.
8. Compara: ¿cuánto de lo que hiciste te resultó familiar de NestJS y Laravel?
💡 Pistas de la solución
  • Spring Initializr genera el proyecto con la estructura de paquetes correcta — no la crees a mano, ahorra errores tontos de imports.
  • Si findByPublicadoTrue() no compila, revisa el nombre exacto del campo en tu entidad (publicado, no esPublicado ni publico) — Spring Data deriva la consulta literalmente del nombre del método.
  • Para Flyway, los archivos de migración van en src/main/resources/db/migration/ con el patrón V1__crear_tabla_articulos.sql — el número de versión y el doble guion bajo son obligatorios.
  • El test con Testcontainers necesita Docker corriendo en tu máquina; si falla con un error de conexión al motor de contenedores, ese es el primer sospechoso, no tu código.

🧠 Autoevaluación

Porque sigue una convención de nomenclatura estricta (findBy + nombre de campo + operador opcional como LessThan/True) que Spring parsea en tiempo de arranque para construir la consulta JPQL equivalente. Es "convención sobre configuración" llevado al extremo — el mismo principio que otros frameworks del libro aplican de formas distintas.

double usa coma flotante binaria, que no puede representar exactamente muchas cantidades decimales (el mismo problema de redondeo que en el apéndice P sobre pagos). BigDecimal representa el número de forma exacta — imprescindible para dinero, donde un error de redondeo acumulado es un bug de negocio real, no solo estético.

Genera automáticamente el constructor, los getters (sin prefijo get, como .nombre()), equals(), hashCode() y toString() a partir de los campos declarados — todo en una línea, con inmutabilidad garantizada por defecto. Es Java moderno adoptando lo que otros lenguajes del libro (TypeScript, Rust) ya daban por sentado.

Porque Hibernate altera el esquema real de la base de datos automáticamente para que coincida con tus entidades — sin revisión, sin control de versiones, sin posibilidad de rollback seguro. Es el equivalente a dejar que un ORM ejecute migraciones no revisadas directamente en producción; la alternativa correcta es validate + migraciones versionadas con Flyway.

SecurityFilterChain filtra por patrón de URL (ej. todo /api/admin/** requiere rol ADMIN) — es una regla de grano grueso, evaluada antes de llegar al controller. @PreAuthorize permite lógica de autorización más fina, a nivel de método, incluyendo comparar el usuario autenticado con el recurso solicitado (ej. "admin O el dueño del recurso"), algo que una regla de URL no puede expresar por sí sola.


Volver al: README.md · Relacionado: 03-php-laravel.md, 06-node-nestjs.md, 11-arquitectura.md