15. Comunicación entre Microservicios
🏗️ 15.1 Arquitectura de Microservicios
Section titled “🏗️ 15.1 Arquitectura de Microservicios”Monolito vs Microservicios
Section titled “Monolito vs Microservicios”| Aspecto | Monolito | Microservicios |
|---|---|---|
| Despliegue | Todo junto | Independiente |
| Escalado | Vertical | Horizontal |
| Tecnología | Una sola | Múltiples |
| Equipo | Grande | Pequeños |
| Complejidad | Baja inicial | Alta inicial |
Arquitectura típica
Section titled “Arquitectura típica”┌─────────────────────────────────────────────────────────┐│ API Gateway │└─────────────────────────────────────────────────────────┘ │ ┌─────────────────────┼─────────────────────┐ │ │ │ ▼ ▼ ▼┌─────────┐ ┌─────────┐ ┌─────────┐│ Usuario │ │ Pedido │ │Producto ││ Service │ │ Service │ │ Service │└────┬────┘ └────┬────┘ └────┬────┘ │ │ │ ▼ ▼ ▼┌─────────┐ ┌─────────┐ ┌─────────┐│ DB │ │ DB │ │ DB │└─────────┘ └─────────┘ └─────────┘🔄 15.2 Comunicación Síncrona (REST)
Section titled “🔄 15.2 Comunicación Síncrona (REST)”RestTemplate (legacy)
Section titled “RestTemplate (legacy)”@Servicepublic class ProductoClientService {
private final RestTemplate restTemplate;
public ProductoClientService(RestTemplateBuilder builder) { this.restTemplate = builder .rootUri("http://producto-service:8080") .setConnectTimeout(Duration.ofSeconds(5)) .setReadTimeout(Duration.ofSeconds(5)) .build(); }
public Producto obtenerProducto(Long id) { return restTemplate.getForObject("/api/productos/{id}", Producto.class, id); }
public List<Producto> listarProductos() { ResponseEntity<List<Producto>> response = restTemplate.exchange( "/api/productos", HttpMethod.GET, null, new ParameterizedTypeReference<List<Producto>>() {} ); return response.getBody(); }
public Producto crearProducto(ProductoDTO dto) { return restTemplate.postForObject("/api/productos", dto, Producto.class); }}WebClient (reactivo, recomendado)
Section titled “WebClient (reactivo, recomendado)”@Servicepublic class ProductoClientService {
private final WebClient webClient;
public ProductoClientService(WebClient.Builder builder) { this.webClient = builder .baseUrl("http://producto-service:8080") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); }
public Mono<Producto> obtenerProducto(Long id) { return webClient.get() .uri("/api/productos/{id}", id) .retrieve() .bodyToMono(Producto.class); }
public Flux<Producto> listarProductos() { return webClient.get() .uri("/api/productos") .retrieve() .bodyToFlux(Producto.class); }
// Versión bloqueante (para código no reactivo) public Producto obtenerProductoSync(Long id) { return webClient.get() .uri("/api/productos/{id}", id) .retrieve() .bodyToMono(Producto.class) .block(); // Bloquea hasta obtener respuesta }
public Producto crearProducto(ProductoDTO dto) { return webClient.post() .uri("/api/productos") .bodyValue(dto) .retrieve() .bodyToMono(Producto.class) .block(); }}🪶 15.3 Feign Client
Section titled “🪶 15.3 Feign Client”Dependencia
Section titled “Dependencia”<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>Habilitar Feign
Section titled “Habilitar Feign”@SpringBootApplication@EnableFeignClientspublic class PedidoServiceApplication { public static void main(String[] args) { SpringApplication.run(PedidoServiceApplication.class, args); }}Definir cliente
Section titled “Definir cliente”@FeignClient(name = "producto-service", url = "${producto.service.url}")public interface ProductoClient {
@GetMapping("/api/productos/{id}") Producto obtenerPorId(@PathVariable("id") Long id);
@GetMapping("/api/productos") List<Producto> listar();
@GetMapping("/api/productos") List<Producto> buscar(@RequestParam("categoria") String categoria);
@PostMapping("/api/productos") Producto crear(@RequestBody ProductoDTO dto);
@PutMapping("/api/productos/{id}") Producto actualizar(@PathVariable("id") Long id, @RequestBody ProductoDTO dto);
@DeleteMapping("/api/productos/{id}") void eliminar(@PathVariable("id") Long id);}Uso en servicio
Section titled “Uso en servicio”@Servicepublic class PedidoService {
private final ProductoClient productoClient; private final PedidoRepository pedidoRepository;
public Pedido crear(PedidoDTO dto) { // Llamar al servicio de productos List<Producto> productos = dto.getProductoIds().stream() .map(productoClient::obtenerPorId) .toList();
BigDecimal total = productos.stream() .map(Producto::getPrecio) .reduce(BigDecimal.ZERO, BigDecimal::add);
Pedido pedido = Pedido.builder() .productos(productos) .total(total) .build();
return pedidoRepository.save(pedido); }}Configuración avanzada
Section titled “Configuración avanzada”# application.propertiesproducto.service.url=http://localhost:8081
# Timeoutsfeign.client.config.default.connect-timeout=5000feign.client.config.default.read-timeout=5000
# Loggingfeign.client.config.default.logger-level=fulllogging.level.com.miapp.client=DEBUG⚖️ 15.4 Balanceo de Carga
Section titled “⚖️ 15.4 Balanceo de Carga”Spring Cloud LoadBalancer
Section titled “Spring Cloud LoadBalancer”<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>Feign con LoadBalancer
Section titled “Feign con LoadBalancer”// El nombre del servicio se resuelve via Service Discovery@FeignClient(name = "producto-service") // Sin URL, usa Eurekapublic interface ProductoClient {
@GetMapping("/api/productos/{id}") Producto obtenerPorId(@PathVariable("id") Long id);}WebClient con LoadBalancer
Section titled “WebClient con LoadBalancer”@Configurationpublic class WebClientConfig {
@Bean @LoadBalanced // Habilita balanceo de carga public WebClient.Builder webClientBuilder() { return WebClient.builder(); }}
@Servicepublic class ProductoClientService {
private final WebClient webClient;
public ProductoClientService(WebClient.Builder builder) { this.webClient = builder .baseUrl("http://producto-service") // Nombre del servicio .build(); }}🔍 15.5 Service Discovery
Section titled “🔍 15.5 Service Discovery”Eureka Server
Section titled “Eureka Server”<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }}# application.properties (Eureka Server)server.port=8761eureka.client.register-with-eureka=falseeureka.client.fetch-registry=falseEureka Client
Section titled “Eureka Client”<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency># application.properties (Microservicio)spring.application.name=producto-serviceeureka.client.service-url.defaultZone=http://localhost:8761/eureka/eureka.instance.prefer-ip-address=true🔌 15.6 Circuit Breaker
Section titled “🔌 15.6 Circuit Breaker”Resilience4j
Section titled “Resilience4j”<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId></dependency>Configuración
Section titled “Configuración”# application.propertiesresilience4j.circuitbreaker.instances.productoService.failure-rate-threshold=50resilience4j.circuitbreaker.instances.productoService.wait-duration-in-open-state=10sresilience4j.circuitbreaker.instances.productoService.sliding-window-size=10resilience4j.circuitbreaker.instances.productoService.minimum-number-of-calls=5
resilience4j.retry.instances.productoService.max-attempts=3resilience4j.retry.instances.productoService.wait-duration=1s
resilience4j.timelimiter.instances.productoService.timeout-duration=3sUso con anotaciones
Section titled “Uso con anotaciones”@Servicepublic class ProductoClientService {
private final ProductoClient productoClient;
@CircuitBreaker(name = "productoService", fallbackMethod = "obtenerProductoFallback") @Retry(name = "productoService") @TimeLimiter(name = "productoService") public CompletableFuture<Producto> obtenerProducto(Long id) { return CompletableFuture.supplyAsync(() -> productoClient.obtenerPorId(id)); }
// Fallback cuando el circuito está abierto o hay error public CompletableFuture<Producto> obtenerProductoFallback(Long id, Throwable t) { log.warn("Fallback para producto {}: {}", id, t.getMessage()); return CompletableFuture.completedFuture( Producto.builder() .id(id) .nombre("Producto no disponible") .precio(BigDecimal.ZERO) .build() ); }}Feign con Circuit Breaker
Section titled “Feign con Circuit Breaker”@FeignClient( name = "producto-service", fallback = ProductoClientFallback.class)public interface ProductoClient { @GetMapping("/api/productos/{id}") Producto obtenerPorId(@PathVariable("id") Long id);}
@Componentpublic class ProductoClientFallback implements ProductoClient {
@Override public Producto obtenerPorId(Long id) { return Producto.builder() .id(id) .nombre("Producto no disponible") .build(); }}📨 15.7 Comunicación Asíncrona
Section titled “📨 15.7 Comunicación Asíncrona”RabbitMQ
Section titled “RabbitMQ”<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId></dependency># application.propertiesspring.rabbitmq.host=localhostspring.rabbitmq.port=5672spring.rabbitmq.username=guestspring.rabbitmq.password=guestProductor
Section titled “Productor”@Configurationpublic class RabbitConfig {
@Bean public Queue pedidosQueue() { return new Queue("pedidos.creados", true); }
@Bean public TopicExchange exchange() { return new TopicExchange("pedidos.exchange"); }
@Bean public Binding binding(Queue queue, TopicExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("pedido.creado"); }}
@Servicepublic class PedidoService {
private final RabbitTemplate rabbitTemplate;
public Pedido crear(PedidoDTO dto) { Pedido pedido = pedidoRepository.save(new Pedido(dto));
// Publicar evento PedidoCreadoEvent evento = new PedidoCreadoEvent(pedido.getId(), pedido.getTotal()); rabbitTemplate.convertAndSend("pedidos.exchange", "pedido.creado", evento);
return pedido; }}Consumidor
Section titled “Consumidor”@Servicepublic class NotificacionService {
@RabbitListener(queues = "pedidos.creados") public void procesarPedidoCreado(PedidoCreadoEvent evento) { log.info("Pedido creado: {}", evento.getPedidoId());
// Enviar notificación, email, etc. enviarNotificacion(evento); }}📊 15.8 Patrones de Comunicación
Section titled “📊 15.8 Patrones de Comunicación”Saga Pattern
Section titled “Saga Pattern”@Servicepublic class PedidoSagaOrchestrator {
private final PedidoService pedidoService; private final InventarioClient inventarioClient; private final PagoClient pagoClient;
@Transactional public Pedido procesarPedido(PedidoDTO dto) { Pedido pedido = null; String reservaId = null; String pagoId = null;
try { // Paso 1: Crear pedido pedido = pedidoService.crear(dto);
// Paso 2: Reservar inventario reservaId = inventarioClient.reservar(dto.getProductos());
// Paso 3: Procesar pago pagoId = pagoClient.procesar(dto.getPago());
// Paso 4: Confirmar pedido pedido = pedidoService.confirmar(pedido.getId());
return pedido;
} catch (Exception e) { // Compensaciones if (pagoId != null) { pagoClient.revertir(pagoId); } if (reservaId != null) { inventarioClient.liberarReserva(reservaId); } if (pedido != null) { pedidoService.cancelar(pedido.getId()); } throw new SagaException("Error en saga de pedido", e); } }}
🐝