25. Temas Avanzados y Expertos
⚡ 25.1 Spring WebFlux
Section titled “⚡ 25.1 Spring WebFlux”¿Qué es WebFlux?
Section titled “¿Qué es WebFlux?”Spring WebFlux es el framework reactivo de Spring para construir aplicaciones no bloqueantes y escalables.
| Característica | Spring MVC | Spring WebFlux |
|---|---|---|
| Modelo | Síncrono/Bloqueante | Asíncrono/No bloqueante |
| Servidor | Tomcat, Jetty | Netty, Undertow |
| Threads | Thread por request | Event loop |
| Escalabilidad | Vertical | Horizontal |
| Caso de uso | CRUD tradicional | Streaming, alta concurrencia |
Dependencia
Section titled “Dependencia”<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId></dependency>Tipos reactivos
Section titled “Tipos reactivos”// Mono: 0 o 1 elementoMono<Usuario> usuario = usuarioRepository.findById(id);
// Flux: 0 a N elementosFlux<Producto> productos = productoRepository.findAll();
// Operadores comunesMono<String> resultado = Mono.just("Hola") .map(String::toUpperCase) .flatMap(s -> Mono.just(s + " Mundo")) .filter(s -> s.length() > 5) .defaultIfEmpty("Vacío");
Flux<Integer> numeros = Flux.range(1, 10) .filter(n -> n % 2 == 0) .map(n -> n * 2) .take(3);Controller reactivo
Section titled “Controller reactivo”@RestController@RequestMapping("/api/productos")@RequiredArgsConstructorpublic class ProductoController {
private final ProductoService productoService;
@GetMapping public Flux<ProductoDTO> listar() { return productoService.listar(); }
@GetMapping("/{id}") public Mono<ResponseEntity<ProductoDTO>> obtener(@PathVariable String id) { return productoService.obtenerPorId(id) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()); }
@PostMapping @ResponseStatus(HttpStatus.CREATED) public Mono<ProductoDTO> crear(@Valid @RequestBody Mono<ProductoCreateDTO> dto) { return dto.flatMap(productoService::crear); }
@DeleteMapping("/{id}") public Mono<Void> eliminar(@PathVariable String id) { return productoService.eliminar(id); }
// Streaming (Server-Sent Events) @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ProductoDTO> stream() { return productoService.listar() .delayElements(Duration.ofSeconds(1)); }}Repository reactivo (R2DBC)
Section titled “Repository reactivo (R2DBC)”<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-r2dbc</artifactId></dependency><dependency> <groupId>io.r2dbc</groupId> <artifactId>r2dbc-postgresql</artifactId></dependency>public interface ProductoRepository extends ReactiveCrudRepository<Producto, Long> {
Flux<Producto> findByCategoria(String categoria);
Mono<Producto> findByNombre(String nombre);
@Query("SELECT * FROM productos WHERE precio > :precio") Flux<Producto> findByPrecioMayorQue(BigDecimal precio);}
@Service@RequiredArgsConstructorpublic class ProductoService {
private final ProductoRepository repository;
public Flux<ProductoDTO> listar() { return repository.findAll() .map(this::toDTO); }
public Mono<ProductoDTO> crear(ProductoCreateDTO dto) { return Mono.just(dto) .map(this::toEntity) .flatMap(repository::save) .map(this::toDTO); }}WebClient (cliente HTTP reactivo)
Section titled “WebClient (cliente HTTP reactivo)”@Servicepublic class ExternalApiService {
private final WebClient webClient;
public ExternalApiService(WebClient.Builder builder) { this.webClient = builder .baseUrl("https://api.externa.com") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); }
public Mono<DatosDTO> obtenerDatos(String id) { return webClient.get() .uri("/datos/{id}", id) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new NotFoundException("No encontrado"))) .bodyToMono(DatosDTO.class) .timeout(Duration.ofSeconds(5)) .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))); }
public Flux<EventoDTO> streamEventos() { return webClient.get() .uri("/eventos/stream") .accept(MediaType.TEXT_EVENT_STREAM) .retrieve() .bodyToFlux(EventoDTO.class); }}🚀 25.2 Spring Native y GraalVM
Section titled “🚀 25.2 Spring Native y GraalVM”¿Qué es Spring Native?
Section titled “¿Qué es Spring Native?”Compila aplicaciones Spring a ejecutables nativos usando GraalVM, logrando:
- Arranque instantáneo (~50ms vs ~2s)
- Menor consumo de memoria (~50MB vs ~200MB)
- Ideal para serverless y contenedores
Configuración
Section titled “Configuración”<plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId></plugin>
<!-- En spring-boot-maven-plugin --><configuration> <image> <builder>paketobuildpacks/builder:tiny</builder> <env> <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE> </env> </image></configuration>Compilar imagen nativa
Section titled “Compilar imagen nativa”# Con Mavenmvn -Pnative native:compile
# Con Buildpacks (Docker)mvn spring-boot:build-image -Pnative
# Ejecutar./target/mi-appHints para reflexión
Section titled “Hints para reflexión”// Registrar clases para reflexión@RegisterReflectionForBinding({ ProductoDTO.class, UsuarioDTO.class})@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}
// O con archivo de configuración// src/main/resources/META-INF/native-image/reflect-config.json[{ "name": "com.miapp.dto.ProductoDTO", "allDeclaredConstructors": true, "allDeclaredMethods": true, "allDeclaredFields": true}]📡 25.3 WebSockets
Section titled “📡 25.3 WebSockets”Configuración
Section titled “Configuración”@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic", "/queue"); config.setApplicationDestinationPrefixes("/app"); config.setUserDestinationPrefix("/user"); }
@Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") .setAllowedOrigins("*") .withSockJS(); }}Controller WebSocket
Section titled “Controller WebSocket”@Controller@RequiredArgsConstructorpublic class ChatController {
private final SimpMessagingTemplate messagingTemplate;
// Recibir mensaje y broadcast a todos @MessageMapping("/chat.send") @SendTo("/topic/mensajes") public MensajeDTO enviarMensaje(MensajeDTO mensaje) { mensaje.setTimestamp(LocalDateTime.now()); return mensaje; }
// Mensaje privado a usuario específico @MessageMapping("/chat.privado") public void mensajePrivado(MensajePrivadoDTO mensaje) { messagingTemplate.convertAndSendToUser( mensaje.getDestinatario(), "/queue/privado", mensaje ); }
// Notificación desde servicio public void notificarNuevoPedido(PedidoDTO pedido) { messagingTemplate.convertAndSend("/topic/pedidos", pedido); }}Cliente JavaScript
Section titled “Cliente JavaScript”import SockJS from 'sockjs-client';import { Stomp } from '@stomp/stompjs';
const socket = new SockJS('http://localhost:8080/ws');const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {// Suscribirse a topic públicostompClient.subscribe('/topic/mensajes', (message) => { const data = JSON.parse(message.body); console.log('Mensaje recibido:', data);});
// Suscribirse a cola privadastompClient.subscribe('/user/queue/privado', (message) => { const data = JSON.parse(message.body); console.log('Mensaje privado:', data);});
// Enviar mensajestompClient.send('/app/chat.send', {}, JSON.stringify({ contenido: 'Hola!', usuario: 'Juan'}));});⏰ 25.4 Scheduling y Tareas
Section titled “⏰ 25.4 Scheduling y Tareas”Habilitar scheduling
Section titled “Habilitar scheduling”@SpringBootApplication@EnableScheduling@EnableAsyncpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}Tareas programadas
Section titled “Tareas programadas”@Service@Slf4jpublic class TareasService {
// Cada 5 minutos @Scheduled(fixedRate = 300000) public void limpiarCache() { log.info("Limpiando caché..."); }
// 5 segundos después de terminar la anterior @Scheduled(fixedDelay = 5000) public void procesarCola() { log.info("Procesando cola..."); }
// Expresión cron: todos los días a las 2:00 AM @Scheduled(cron = "0 0 2 * * *") public void backupDiario() { log.info("Ejecutando backup..."); }
// Cada hora en horario laboral (L-V, 9-18) @Scheduled(cron = "0 0 9-18 * * MON-FRI") public void reporteHorario() { log.info("Generando reporte..."); }
// Con zona horaria @Scheduled(cron = "0 0 8 * * *", zone = "America/Mexico_City") public void tareaConZona() { log.info("Tarea con zona horaria..."); }}Configuración de pool
Section titled “Configuración de pool”@Configurationpublic class SchedulingConfig implements SchedulingConfigurer {
@Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(taskScheduler()); }
@Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.setThreadNamePrefix("scheduled-"); scheduler.setErrorHandler(t -> log.error("Error en tarea", t)); return scheduler; }}🔄 25.5 GraphQL
Section titled “🔄 25.5 GraphQL”Dependencia
Section titled “Dependencia”<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-graphql</artifactId></dependency>Schema
Section titled “Schema”# src/main/resources/graphql/schema.graphqlstype Query { productos: [Producto!]! producto(id: ID!): Producto productosPorCategoria(categoria: String!): [Producto!]!}
type Mutation { crearProducto(input: ProductoInput!): Producto! actualizarProducto(id: ID!, input: ProductoInput!): Producto eliminarProducto(id: ID!): Boolean!}
type Producto { id: ID! nombre: String! precio: Float! categoria: Categoria! createdAt: String}
type Categoria { id: ID! nombre: String! productos: [Producto!]!}
input ProductoInput { nombre: String! precio: Float! categoriaId: ID!}Controller GraphQL
Section titled “Controller GraphQL”@Controller@RequiredArgsConstructorpublic class ProductoGraphQLController {
private final ProductoService productoService; private final CategoriaService categoriaService;
@QueryMapping public List<Producto> productos() { return productoService.listar(); }
@QueryMapping public Producto producto(@Argument Long id) { return productoService.obtenerPorId(id); }
@MutationMapping public Producto crearProducto(@Argument ProductoInput input) { return productoService.crear(input); }
// Resolver para campo anidado @SchemaMapping(typeName = "Producto", field = "categoria") public Categoria categoria(Producto producto) { return categoriaService.obtenerPorId(producto.getCategoriaId()); }
// Batch loading para evitar N+1 @BatchMapping public Map<Producto, Categoria> categoria(List<Producto> productos) { Set<Long> categoriaIds = productos.stream() .map(Producto::getCategoriaId) .collect(Collectors.toSet());
Map<Long, Categoria> categorias = categoriaService.obtenerPorIds(categoriaIds) .stream() .collect(Collectors.toMap(Categoria::getId, c -> c));
return productos.stream() .collect(Collectors.toMap(p -> p, p -> categorias.get(p.getCategoriaId()))); }}📊 25.6 Observabilidad Avanzada
Section titled “📊 25.6 Observabilidad Avanzada”Micrometer + Prometheus
Section titled “Micrometer + Prometheus”<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId></dependency># application.propertiesmanagement.endpoints.web.exposure.include=health,info,prometheus,metricsmanagement.metrics.tags.application=mi-appmanagement.metrics.tags.environment=prodTracing con Zipkin
Section titled “Tracing con Zipkin”<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId></dependency><dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId></dependency># application.propertiesmanagement.tracing.sampling.probability=1.0management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spansMétricas personalizadas
Section titled “Métricas personalizadas”@Service@RequiredArgsConstructorpublic class PedidoService {
private final MeterRegistry meterRegistry;
@Timed(value = "pedidos.crear", description = "Tiempo de creación de pedidos") @Counted(value = "pedidos.total", description = "Total de pedidos") public Pedido crear(PedidoDTO dto) { // Lógica }
// Métricas manuales public void procesarPago(Pedido pedido) { Timer.Sample sample = Timer.start(meterRegistry);
try { // Procesar pago meterRegistry.counter("pagos.exitosos").increment(); } catch (Exception e) { meterRegistry.counter("pagos.fallidos").increment(); throw e; } finally { sample.stop(meterRegistry.timer("pagos.tiempo")); } }}
🐝