انتقل إلى المحتوى الرئيسي

TP13.1: خادم يوريكا والعملاء

🎯 أهداف التمرين​

  • إنشاء خادم يوريكا (سجل الخدمة)
  • إنشاء خدمتين تسجلان كـ عملاء يوريكا
  • اختبار التوازن التحميلي بين عدة مثيلات
  • مراقبة السلوك أثناء الأعطال

المدة المقدرة: 1 ساعة و30 دقيقة


📋 البنية النهائية​


📁 هيكل المشاريع​

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

الجزء 1: خادم يوريكا (20 دقيقة)​

1.1 إنشاء المشروع​

استخدم Spring Initializr (https://start.spring.io) أو أنشئ يدويًا:

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 التطبيق الرئيسي​

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 التكوين​

# 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 اختبار​

# بدء خادم يوريكا
cd eureka-server
mvn spring-boot:run

افتح: http://localhost:8761

يجب أن ترى لوحة معلومات يوريكا بدون أي خدمة مسجلة.


الجزء 2: خدمة المستخدم (30 دقيقة)​

2.1 إنشاء المشروع​

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 التطبيق الرئيسي​

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 النموذج​

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 وحدة التحكم​

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 التكوين​

# 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 اختبار​

# بدء خدمة المستخدم (المثيل 1)
cd user-service
mvn spring-boot:run

# تحقق في لوحة معلومات يوريكا
# http://localhost:8761 -> يجب أن يظهر USER-SERVICE

# اختبار النقاط النهائية
curl http://localhost:8081/api/users
curl http://localhost:8081/api/users/1
curl http://localhost:8081/api/users/info

2.7 تشغيل مثيل ثاني​

# في نافذة طرفية أخرى
cd user-service
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8082

تحقق في يوريكا: يجب أن يكون لـ USER-SERVICE مثيلان.


الجزء 3: خدمة المنتج مع التوازن التحميلي (30 دقيقة)​

3.1 إنشاء المشروع​

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 تكوين RestTemplate مع التوازن التحميلي​

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 للمستخدم​

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 خدمة لاستدعاء خدمة المستخدم​

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 نموذج المنتج​

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 وحدة التحكم​

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 التكوين​

# 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 اختبار التوازن التحميلي​

# بدء خدمة المنتج
cd product-service
mvn spring-boot:run

# اختبار التوازن التحميلي
curl http://localhost:8083/api/products/test-load-balancing

النتيجة المتوقعة (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"
]

الجزء 4: اختبار المرونة (10 دقائق)​

4.1 إيقاف مثيل​

  1. أوقف مثيل خدمة المستخدم على المنفذ 8082 (Ctrl+C)
  2. انتظر 30 ثانية (وقت الإخلاء في يوريكا)
  3. تحقق من لوحة معلومات يوريكا: يجب أن يبقى مثيل واحد فقط
  4. أعد اختبار التوازن التحميلي:
curl http://localhost:8083/api/products/test-load-balancing

النتيجة المتوقعة:

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

4.2 إعادة تشغيل المثيل​

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

يستأنف التوازن التحميلي تلقائيًا على المثيلين!


✅ قائمة التحقق من التحقق​

  • خادم يوريكا يبدأ على http://localhost:8761
  • خدمة المستخدم (المثيل 1) تسجل على المنفذ 8081
  • خدمة المستخدم (المثيل 2) تسجل على المنفذ 8082
  • خدمة المنتج تسجل على المنفذ 8083
  • لوحة معلومات يوريكا تعرض 3 خدمات (USER-SERVICE x2, PRODUCT-SERVICE x1)
  • التوازن التحميلي يعمل (تناوب 8081/8082)
  • إيقاف مثيل → إعادة توجيه الحركة إلى الآخر
  • إعادة التشغيل → يستأنف التوازن التحميلي

🎯 تمارين إضافية​

تمرين 1: إضافة فحص صحة مخصص​

@Component
public class UserServiceHealthIndicator implements HealthIndicator {

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

تمرين 2: إضافة بيانات وصفية ليوريكا​

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

تمرين 3: تكوين مهلة مخصصة​

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

📚 الموارد​


التمرين التالي: خادم التكوين →

العودة إلى الوحدة 13