Aller au contenu principal

TP13.1: Eureka Server et Clients

🎯 Objectifs du TP​

  • CrĂ©er un Eureka Server (Service Registry)
  • CrĂ©er deux services qui s'enregistrent comme Eureka Clients
  • Tester le load balancing entre plusieurs instances
  • Observer le comportement lors de pannes

Durée estimée: 1h30


📋 Architecture Finale​


📁 Structure des Projets​

microservices-demo/
├── eureka-server/
│ ├── pom.xml
│ └── src/main/...
├── user-service/
│ ├── pom.xml
│ └── src/main/...
└── product-service/
├── pom.xml
└── src/main/...

Partie 1: Eureka Server (20 min)​

1.1 CrĂ©er le Projet​

Utilisez Spring Initializr (https://start.spring.io) ou créez manuellement:

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 Application Principale​

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 Configuration​

# 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 Tester​

# Démarrer Eureka Server
cd eureka-server
mvn spring-boot:run

Ouvrez: http://localhost:8761

Vous devriez voir le dashboard Eureka avec aucun service enregistré.


Partie 2: User Service (30 min)​

2.1 CrĂ©er le Projet​

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 Application Principale​

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 Model​

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 Controller​

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 pour identifier quelle instance répond
@GetMapping("/info")
public ResponseEntity<Map<String, String>> getInfo() {
return ResponseEntity.ok(Map.of(
"service", "user-service",
"port", serverPort,
"timestamp", new Date().toString()
));
}
}

2.5 Configuration​

# 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 Tester​

# Démarrer User Service (Instance 1)
cd user-service
mvn spring-boot:run

# Vérifier dans Eureka Dashboard
# http://localhost:8761 -> USER-SERVICE devrait apparaĂźtre

# Tester les endpoints
curl http://localhost:8081/api/users
curl http://localhost:8081/api/users/1
curl http://localhost:8081/api/users/info

2.7 Lancer une Deuxiùme Instance​

# Dans un autre terminal
cd user-service
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8082

Vérifiez dans Eureka: USER-SERVICE devrait avoir 2 instances.


Partie 3: Product Service avec Load Balancing (30 min)​

3.1 CrĂ©er le Projet​

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 Configuration RestTemplate avec 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 pour 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 Service pour Appeler 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) {
// Utilise le nom du service (pas l'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 Model 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; // Pour voir quelle instance User Service a répondu

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 Controller​

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 qui appelle User Service avec 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();
}

// Appel Ă  User Service (load balanced!)
UserDto owner = userServiceClient.getUserById(product.getOwnerId());

// Voir quelle instance a répondu
Map<String, String> serviceInfo = userServiceClient.getUserServiceInfo();

return ResponseEntity.ok(new ProductWithOwner(
product,
owner,
"user-service:" + serviceInfo.get("port")
));
}

// Test load balancing - appelle User Service plusieurs fois
@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 Configuration​

# 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 Tester le Load Balancing​

# Démarrer Product Service
cd product-service
mvn spring-boot:run

# Tester le load balancing
curl http://localhost:8083/api/products/test-load-balancing

Résultat attendu (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"
]

Partie 4: Test de RĂ©silience (10 min)​

4.1 ArrĂȘter une Instance​

  1. ArrĂȘtez l'instance User Service sur le port 8082 (Ctrl+C)
  2. Attendez 30 secondes (temps d'éviction Eureka)
  3. Vérifiez le dashboard Eureka: une seule instance devrait rester
  4. Retestez le load balancing:
curl http://localhost:8083/api/products/test-load-balancing

Résultat attendu:

[
"Call 1: Served by port 8081",
"Call 2: Served by port 8081",
"Call 3: Served by port 8081",
...
]

4.2 RedĂ©marrer l'Instance​

cd user-service
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8082

Le load balancing reprend automatiquement sur les deux instances!


✅ Checklist de Validation​

  • Eureka Server dĂ©marre sur http://localhost:8761
  • User Service (instance 1) s'enregistre sur le port 8081
  • User Service (instance 2) s'enregistre sur le port 8082
  • Product Service s'enregistre sur le port 8083
  • Dashboard Eureka affiche 3 services (USER-SERVICE x2, PRODUCT-SERVICE x1)
  • Load balancing fonctionne (alternance 8081/8082)
  • ArrĂȘt d'une instance → trafic redirigĂ© vers l'autre
  • RedĂ©marrage → load balancing reprend

🎯 Exercices Bonus​

Exercice 1: Ajouter un Health Check Custom​

@Component
public class UserServiceHealthIndicator implements HealthIndicator {

@Override
public Health health() {
// Votre logique de vérification
return Health.up()
.withDetail("users", "All users healthy")
.build();
}
}

Exercice 2: Ajouter des MĂ©tadonnĂ©es Eureka​

eureka:
instance:
metadata-map:
version: "1.0.0"
region: "us-east-1"

Exercice 3: Configurer un Timeout Personnalisé​

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(3000);
factory.setReadTimeout(5000);
return new RestTemplate(factory);
}

📚 Ressources​


TP Suivant: Config Server →

Retour au Module 13