MFormations
Modern Java Engineering

Chapitre 8

Chapitre 08 : Spring Microservices

Chapitre 08 : Spring Microservices

Cours : Spring Microservices

1. Architecture Microservices

1.1 Principes

                    ┌──────────┐
                    │  Gateway  │
                    │ (Routeur) │
                    └────┬─────┘
                         │
          ┌──────────────┼──────────────┐
          ↓              ↓              ↓
    ┌──────────┐   ┌──────────┐   ┌──────────┐
    │ Service  │   │ Service  │   │ Service  │
    │   User   │   │  Order   │   │ Payment  │
    └────┬─────┘   └────┬─────┘   └────┬─────┘
         │              │              │
         └──────────────┼──────────────┘
                        ↓
                  ┌──────────┐
                  │  Eureka  │
                  │ (Discovery)│
                  └──────────┘

1.2 Dépendances

<!-- Spring Cloud BOM -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2023.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2. Spring Cloud Gateway

2.1 Configuration

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1
            - name: CircuitBreaker
              args:
                name: userServiceCB
                fallbackUri: forward:/fallback/users
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100
                redis-rate-limiter.burstCapacity: 200

        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1

        - id: discovery-service
          uri: http://localhost:8761
          predicates:
            - Path=/eureka/web
          filters:
            - SetPath=/

      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin

2.2 Gateway Java Config

@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator customRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .circuitBreaker(config -> config
                        .setName("userServiceCB")
                        .setFallbackUri("forward:/fallback/users")))
                .uri("lb://user-service"))
            .route("order-service", r -> r
                .path("/api/orders/**")
                .filters(f -> f.stripPrefix(1))
                .uri("lb://order-service"))
            .route("auth-service", r -> r
                .path("/auth/**")
                .filters(f -> f
                    .addRequestHeader("X-Gateway", "true"))
                .uri("lb://auth-service"))
            .build();
    }
    
    // Rate Limiter
    @Bean
    public RedisRateLimiter redisRateLimiter() {
        return new RedisRateLimiter(100, 200);
    }
}

2.3 Filters Personnalisés

@Component
public class RequestLoggingFilter implements GlobalFilter, Ordered {
    private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        log.info("Requête: {} {} de {}", 
            request.getMethod(), request.getPath(), 
            request.getRemoteAddress());
        
        return chain.filter(exchange)
            .then(Mono.fromRunnable(() -> {
                log.info("Réponse: {}", exchange.getResponse().getStatusCode());
            }));
    }
    
    @Override
    public int getOrder() {
        return -1; // Premier filtre
    }
}

3. Service Discovery (Eureka)

3.1 Eureka Server

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServiceApplication.class, args);
    }
}

// application.yml
server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false

3.2 Eureka Client

@SpringBootApplication
@EnableDiscoveryClient // Optionnel avec Spring Boot 3.x (auto-détecté)
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

// application.yml
spring:
  application:
    name: user-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 10
    lease-expiration-duration-in-seconds: 30

4. Config Server

4.1 Config Server

# config-server application.yml
server:
  port: 8888

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/company/config-repo
          default-label: main
          search-paths: '{application}'

4.2 Config Client

# bootstrap.yml (ou application.yml avec Spring Boot 3.x)
spring:
  application:
    name: user-service
  config:
    import: configserver:http://localhost:8888

# Configuration distante (dans git)
# user-service.yml
app:
  feature:
    caching: true
    analytics: false

# user-service-dev.yml (profil)
app:
  feature:
    debugging: true

4.3 Refresh Scope

@RestController
@RequestMapping("/api/config")
@RefreshScope // Permet le rafraîchissement sans redémarrage
public class ConfigController {
    
    @Value("${app.feature.caching:false}")
    private boolean cachingEnabled;
    
    @GetMapping("/features")
    public Map<String, Object> getFeatures() {
        return Map.of("caching", cachingEnabled);
    }
}

// POST /actuator/refresh pour recharger la configuration

5. Resilience4j

5.1 CircuitBreaker

@Service
public class UserServiceClient {
    
    @CircuitBreaker(name = "userService", fallbackMethod = "getDefaultUser")
    @RateLimiter(name = "userService")
    @Retry(name = "userService", fallbackMethod = "getDefaultUser")
    public UserDTO getUser(Long id) {
        return webClient.get()
            .uri("/api/users/{id}", id)
            .retrieve()
            .bodyToMono(UserDTO.class)
            .block();
    }
    
    public UserDTO getDefaultUser(Long id, Throwable t) {
        log.warn("Fallback pour user {}: {}", id, t.getMessage());
        return new UserDTO(0L, "Utilisateur temporaire", "fallback@email.com");
    }
}

// application.yml
resilience4j:
  circuitbreaker:
    instances:
      userService:
        sliding-window-size: 10
        minimum-number-of-calls: 5
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 3
        automatic-transition-from-open-to-half-open-enabled: true
  retry:
    instances:
      userService:
        max-attempts: 3
        wait-duration: 500ms
        retry-exceptions:
          - org.springframework.web.client.HttpServerErrorException
  ratelimiter:
    instances:
      userService:
        limit-for-period: 100
        limit-refresh-period: 1s
        timeout-duration: 500ms
  bulkhead:
    instances:
      userService:
        max-concurrent-calls: 10
        max-wait-duration: 100ms

5.2 TimeLimiter

@TimedLimiter(name = "userService", fallbackMethod = "getDefaultUser")
public CompletableFuture<UserDTO> getUserAsync(Long id) {
    return CompletableFuture.supplyAsync(() ->
        userServiceClient.getUser(id));
}

5.3 Actuator Endpoints

management:
  endpoints:
    web:
      exposure:
        include: health,info,circuitbreakers,ratelimiters,retries

6. Distributed Tracing

6.1 Micrometer Tracing

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>
spring:
  application:
    name: user-service
  sleuth: # Obsolète depuis Spring Boot 3.x
    # Utiliser Micrometer Tracing à la place

management:
  tracing:
    sampling:
      probability: 1.0 # 100% des traces (0.1 en prod)
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

6.2 Tags Personnalisés

@Component
public class TracingConfiguration {
    
    @Bean
    public ObservationHandler<Observation.Context> customHandler() {
        return new ObservationHandler<>() {
            @Override
            public void onStart(Observation.Context context) {
                context.addLowCardinalityKeyValue("service", "user-service");
                context.addLowCardinalityKeyValue("environment", 
                    System.getenv("ENV"));
            }
            
            @Override
            public boolean supportsContext(Observation.Context context) {
                return true;
            }
        };
    }
}

7. Spring Cloud Stream (Kafka)

7.1 Producteur

@Configuration
public class KafkaBindings {
    @Bean
    public Supplier<UserEvent> userEventSupplier() {
        return () -> {
            // Simulation d'événements
            return new UserEvent(
                System.currentTimeMillis(),
                "USER_CREATED",
                Map.of("userId", 1L, "email", "test@test.com")
            );
        };
    }
}

// application.yml
spring:
  cloud:
    stream:
      bindings:
        userEventSupplier-out-0:
          destination: user-events
          content-type: application/json
      kafka:
        binder:
          brokers: localhost:9092
          auto-create-topics: true

7.2 Consommateur

@Component
public class UserEventConsumer {
    private static final Logger log = LoggerFactory.getLogger(UserEventConsumer.class);
    
    @Bean
    public Consumer<UserEvent> handleUserEvents() {
        return event -> {
            log.info("Événement reçu: {} - {}", event.type(), event.data());
            
            switch (event.type()) {
                case "USER_CREATED" -> handleUserCreated(event);
                case "USER_UPDATED" -> handleUserUpdated(event);
                case "USER_DELETED" -> handleUserDeleted(event);
            }
        };
    }
    
    // Spring Cloud Stream Function
    @Bean
    public Function<UserEvent, NotificationEvent> processUserEvent() {
        return event -> {
            log.info("Transformation: {} → Notification", event.type());
            return new NotificationEvent(event.timestamp(), 
                "notification-" + event.type(), 
                event.data());
        };
    }
}

// application.yml
spring:
  cloud:
    stream:
      bindings:
        handleUserEvents-in-0:
          destination: user-events
          group: user-service-group
        processUserEvent-in-0:
          destination: user-events
        processUserEvent-out-0:
          destination: notifications

8. Feign / WebClient

8.1 Feign Client

// Dépendance
// spring-cloud-starter-openfeign

@FeignClient(name = "user-service", path = "/api/users",
             fallback = UserServiceFallback.class,
             configuration = UserFeignConfig.class)
public interface UserServiceClient {
    
    @GetMapping("/{id}")
    UserDTO getUser(@PathVariable Long id);
    
    @GetMapping
    List<UserDTO> getAllUsers(@RequestParam int page, @RequestParam int size);
    
    @PostMapping
    UserDTO createUser(@RequestBody CreateUserRequest request);
}

// Fallback
@Component
public class UserServiceFallback implements UserServiceClient {
    @Override
    public UserDTO getUser(Long id) {
        return new UserDTO(0L, "Fallback", "fallback@test.com");
    }
    
    @Override
    public List<UserDTO> getAllUsers(int page, int size) {
        return List.of();
    }
    
    @Override
    public UserDTO createUser(CreateUserRequest request) {
        throw new ServiceUnavailableException("User service unavailable");
    }
}

// Configuration Feign
public class UserFeignConfig {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            requestTemplate.header("X-Source", "order-service");
            requestTemplate.header("Authorization", "Bearer " + getToken());
        };
    }
    
    @Bean
    public Retryer retryer() {
        return new Retryer.Default(100, 1000, 3);
    }
}

8.2 WebClient (Reactive)

@Service
public class OrderServiceClient {
    private final WebClient webClient;
    
    public OrderServiceClient(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder
            .baseUrl("http://order-service")
            .defaultHeader("X-Source", "payment-service")
            .filter(ExchangeFilterFunctions
                .statusError(HttpStatus::is4xxClientError, 
                    resp -> new ClientException(resp.statusCode().value())))
            .build();
    }
    
    public Mono<OrderDTO> getOrder(Long id) {
        return webClient.get()
            .uri("/api/orders/{id}", id)
            .retrieve()
            .bodyToMono(OrderDTO.class)
            .timeout(Duration.ofSeconds(5))
            .retryWhen(Retry.backoff(3, Duration.ofMillis(500))
                .filter(throwable -> throwable instanceof TimeoutException));
    }
    
    public Flux<OrderDTO> getUserOrders(Long userId) {
        return webClient.get()
            .uri("/api/orders?userId={id}", userId)
            .retrieve()
            .bodyToFlux(OrderDTO.class);
    }
    
    public Mono<OrderDTO> createOrder(CreateOrderRequest request) {
        return webClient.post()
            .uri("/api/orders")
            .body(Mono.just(request), CreateOrderRequest.class)
            .retrieve()
            .bodyToMono(OrderDTO.class)
            .onErrorResume(e -> {
                log.error("Erreur création commande: {}", e.getMessage());
                return Mono.empty();
            });
    }
}

// Configuration WebClient
@Configuration
public class WebClientConfig {
    
    @Bean
    public WebClient.Builder webClientBuilder(
            DiscoveryClient discoveryClient,
            LoadBalancerClient loadBalancerClient) {
        return WebClient.builder()
            .filter(new LoadBalancerExchangeFilterFunction(loadBalancerClient))
            .filter(logRequest())
            .filter(retryFilter());
    }
}

9. Docker + Kubernetes

9.1 Dockerfile

# Multi-stage build
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline
COPY src src
RUN ./mvnw package -DskipTests

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]

9.2 docker-compose.yml

version: '3.8'
services:
  discovery-service:
    build: ./discovery-service
    ports:
      - "8761:8761"
  
  config-service:
    build: ./config-service
    ports:
      - "8888:8888"
    depends_on:
      - discovery-service
  
  gateway-service:
    build: ./gateway-service
    ports:
      - "8080:8080"
    depends_on:
      - discovery-service
      - config-service
  
  user-service:
    build: ./user-service
    depends_on:
      - discovery-service
      - config-service
      - postgres-user
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    deploy:
      replicas: 2
  
  order-service:
    build: ./order-service
    depends_on:
      - discovery-service
      - config-service
      - postgres-order
  
  postgres-user:
    image: postgres:16
    environment:
      POSTGRES_DB: userdb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
  
  postgres-order:
    image: postgres:16
    environment:
      POSTGRES_DB: orderdb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
  
  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
  
  zookeeper:
    image: confluentinc/cp-zookeeper:latest
  
  zipkin:
    image: openzipkin/zipkin:latest
    ports:
      - "9411:9411"
  
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
  
  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"

9.3 Kubernetes Deployment

# user-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
        - name: user-service
          image: registry.example.com/user-service:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "k8s"
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: password
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 20
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
    - port: 8080
  type: ClusterIP
---
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  application.yml: |
    spring:
      cloud:
        config:
          enabled: false
    eureka:
      client:
        service-url:
          defaultZone: http://discovery-service:8761/eureka

10. Bonnes Pratiques

// 1. Un bounded context par service
// 2. Base de données dédiée par service
// 3. Communication async (Kafka) pour les événements
// 4. Communication sync (Feign/WebClient) pour les requêtes
// 5. CircuitBreaker + Retry + TimeLimiter
// 6. Distributed tracing obligatoire
// 7. Health + liveness + readiness probes
// 8. Configuration externalisée (Config Server / K8s ConfigMap)
// 9. API Gateway pour la sécurité, rate limiting
// 10. Déploiement conteneurisé (Docker + K8s)

11. Résumé

ComposantRôle
GatewayRoutage, filtrage, sécurité
EurekaDiscovery, registration
Config ServerConfiguration centralisée
Resilience4jCircuitBreaker, Retry, RateLimiter
Micrometer TracingDistributed tracing
Spring Cloud StreamMessaging (Kafka)
FeignHTTP client déclaratif
WebClientHTTP client réactif
DockerConteneurisation
KubernetesOrchestration