TP13.1: Eureka Server y Clientes
🎯 Objetivos del TP
- Crear un Eureka Server (Service Registry)
- Crear dos servicios que se registren como Eureka Clients
- Probar el load balancing entre varias instancias
- Observar el comportamiento durante fallos
Duración estimada: 1h30
📋 Arquitectura Final
📁 Estructura de los Proyectos
microservices-demo/
├── eureka-server/
│ ├── pom.xml
│ └── src/main/...
├── user-service/
│ ├── pom.xml
│ └── src/main/...
└── product-service/
├── pom.xml
└── src/main/...
Parte 1: Eureka Server (20 min)
1.1 Crear el Proyecto
Utilice Spring Initializr (https://start.spring.io) o cree manualmente:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>eureka-server</artifactId>
<version>1.0.0</version>
<name>eureka-server</name>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2 Aplicación Principal
package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
1.3 Configuración
# src/main/resources/application.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 5000
management:
endpoints:
web:
exposure:
include: "*"
1.4 Probar
# Iniciar Eureka Server
cd eureka-server
mvn spring-boot:run
Abra: http://localhost:8761
Debería ver el dashboard de Eureka sin ningún servicio registrado.
Parte 2: User Service (30 min)
2.1 Crear el Proyecto
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 Aplicación Principal
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
2.3 Modelo
package com.example.userservice.model;
public class User {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
public User() {}
public User(Long id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
2.4 Controlador
package com.example.userservice.controller;
import com.example.userservice.model.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.PostConstruct;
import java.util.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Value("${server.port}")
private String serverPort;
private final Map<Long, User> users = new HashMap<>();
@PostConstruct
public void init() {
users.put(1L, new User(1L, "john", "john@example.com", "John", "Doe"));
users.put(2L, new User(2L, "jane", "jane@example.com", "Jane", "Smith"));
users.put(3L, new User(3L, "bob", "bob@example.com", "Bob", "Wilson"));
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(new ArrayList<>(users.values()));
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = users.get(id);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}
@GetMapping("/{id}/exists")
public ResponseEntity<Boolean> userExists(@PathVariable Long id) {
return ResponseEntity.ok(users.containsKey(id));
}
// Endpoint para identificar qué instancia responde
@GetMapping("/info")
public ResponseEntity<Map<String, String>> getInfo() {
return ResponseEntity.ok(Map.of(
"service", "user-service",
"port", serverPort,
"timestamp", new Date().toString()
));
}
}
2.5 Configuración
# src/main/resources/application.yml
server:
port: 8081
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}
management:
endpoints:
web:
exposure:
include: health,info
2.6 Probar
# Iniciar User Service (Instancia 1)
cd user-service
mvn spring-boot:run
# Verificar en el Dashboard de Eureka
# http://localhost:8761 -> Debería aparecer USER-SERVICE
# Probar los endpoints
curl http://localhost:8081/api/users
curl http://localhost:8081/api/users/1
curl http://localhost:8081/api/users/info
2.7 Lanzar una Segunda Instancia
# En otra terminal
cd user-service
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8082
Verifique en Eureka: USER-SERVICE debería tener 2 instancias.
Parte 3: Product Service con Load Balancing (30 min)
3.1 Crear el Proyecto
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>product-service</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2 Configuración de RestTemplate con Load Balancing
package com.example.productservice.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
3.3 DTO para User
package com.example.productservice.dto;
public class UserDto {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
3.4 Servicio para Llamar a User Service
package com.example.productservice.service;
import com.example.productservice.dto.UserDto;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Service
public class UserServiceClient {
private final RestTemplate restTemplate;
public UserServiceClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public UserDto getUserById(Long userId) {
// Utiliza el nombre del servicio (¡no la URL!)
String url = "http://user-service/api/users/" + userId;
return restTemplate.getForObject(url, UserDto.class);
}
@SuppressWarnings("unchecked")
public Map<String, String> getUserServiceInfo() {
String url = "http://user-service/api/users/info";
return restTemplate.getForObject(url, Map.class);
}
}
3.5 Modelo Product
package com.example.productservice.model;
public class Product {
private Long id;
private String name;
private Double price;
private Long ownerId;
public Product() {}
public Product(Long id, String name, Double price, Long ownerId) {
this.id = id;
this.name = name;
this.price = price;
this.ownerId = ownerId;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public Long getOwnerId() { return ownerId; }
public void setOwnerId(Long ownerId) { this.ownerId = ownerId; }
}
3.6 DTO ProductWithOwner
package com.example.productservice.dto;
import com.example.productservice.model.Product;
public class ProductWithOwner {
private Product product;
private UserDto owner;
private String servedBy; // Para ver qué instancia de User Service respondió
public ProductWithOwner(Product product, UserDto owner, String servedBy) {
this.product = product;
this.owner = owner;
this.servedBy = servedBy;
}
// Getters et Setters
public Product getProduct() { return product; }
public void setProduct(Product product) { this.product = product; }
public UserDto getOwner() { return owner; }
public void setOwner(UserDto owner) { this.owner = owner; }
public String getServedBy() { return servedBy; }
public void setServedBy(String servedBy) { this.servedBy = servedBy; }
}
3.7 Controlador
package com.example.productservice.controller;
import com.example.productservice.dto.ProductWithOwner;
import com.example.productservice.dto.UserDto;
import com.example.productservice.model.Product;
import com.example.productservice.service.UserServiceClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.PostConstruct;
import java.util.*;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final UserServiceClient userServiceClient;
private final Map<Long, Product> products = new HashMap<>();
public ProductController(UserServiceClient userServiceClient) {
this.userServiceClient = userServiceClient;
}
@PostConstruct
public void init() {
products.put(1L, new Product(1L, "Laptop", 999.99, 1L));
products.put(2L, new Product(2L, "Phone", 699.99, 2L));
products.put(3L, new Product(3L, "Tablet", 499.99, 1L));
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(new ArrayList<>(products.values()));
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = products.get(id);
if (product == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(product);
}
// Endpoint que llama a User Service con load balancing
@GetMapping("/{id}/with-owner")
public ResponseEntity<ProductWithOwner> getProductWithOwner(@PathVariable Long id) {
Product product = products.get(id);
if (product == null) {
return ResponseEntity.notFound().build();
}
// Llamada a User Service (¡con load balancing!)
UserDto owner = userServiceClient.getUserById(product.getOwnerId());
// Ver qué instancia respondió
Map<String, String> serviceInfo = userServiceClient.getUserServiceInfo();
return ResponseEntity.ok(new ProductWithOwner(
product,
owner,
"user-service:" + serviceInfo.get("port")
));
}
// Probar load balancing - llama a User Service varias veces
@GetMapping("/test-load-balancing")
public ResponseEntity<List<String>> testLoadBalancing() {
List<String> responses = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Map<String, String> info = userServiceClient.getUserServiceInfo();
responses.add("Call " + (i+1) + ": Served by port " + info.get("port"));
}
return ResponseEntity.ok(responses);
}
}
3.8 Configuración
# src/main/resources/application.yml
server:
port: 8083
spring:
application:
name: product-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}
3.9 Probar el Load Balancing
# Iniciar Product Service
cd product-service
mvn spring-boot:run
# Probar el load balancing
curl http://localhost:8083/api/products/test-load-balancing
Resultado esperado (Round Robin):
[
"Call 1: Served by port 8081",
"Call 2: Served by port 8082",
"Call 3: Served by port 8081",
"Call 4: Served by port 8082",
"Call 5: Served by port 8081",
"Call 6: Served by port 8082",
"Call 7: Served by port 8081",
"Call 8: Served by port 8082",
"Call 9: Served by port 8081",
"Call 10: Served by port 8082"
]
Parte 4: Prueba de Resiliencia (10 min)
4.1 Detener una Instancia
- Detenga la instancia de User Service en el puerto 8082 (Ctrl+C)
- Espere 30 segundos (tiempo de expulsión de Eureka)
- Verifique el dashboard de Eureka: solo debería quedar una instancia
- Vuelva a probar el load balancing:
curl http://localhost:8083/api/products/test-load-balancing
Resultado esperado:
[
"Call 1: Served by port 8081",
"Call 2: Served by port 8081",
"Call 3: Served by port 8081",
...
]
4.2 Reiniciar la Instancia
cd user-service
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8082
¡El load balancing se reanuda automáticamente en ambas instancias!
✅ Checklist de Validación
- Eureka Server inicia en http://localhost:8761
- User Service (instancia 1) se registra en el puerto 8081
- User Service (instancia 2) se registra en el puerto 8082
- Product Service se registra en el puerto 8083
- El dashboard de Eureka muestra 3 servicios (USER-SERVICE x2, PRODUCT-SERVICE x1)
- El load balancing funciona (alternancia 8081/8082)
- Al detener una instancia → el tráfico se redirige a la otra
- Al reiniciar → el load balancing se reanuda
🎯 Ejercicios Bonus
Ejercicio 1: Agregar un Health Check Personalizado
@Component
public class UserServiceHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Su lógica de verificación
return Health.up()
.withDetail("users", "All users healthy")
.build();
}
}
Ejercicio 2: Agregar Metadatos Eureka
eureka:
instance:
metadata-map:
version: "1.0.0"
region: "us-east-1"
Ejercicio 3: Configurar un Timeout Personalizado
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(3000);
factory.setReadTimeout(5000);
return new RestTemplate(factory);
}