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é
| Composant | Rôle |
|---|---|
| Gateway | Routage, filtrage, sécurité |
| Eureka | Discovery, registration |
| Config Server | Configuration centralisée |
| Resilience4j | CircuitBreaker, Retry, RateLimiter |
| Micrometer Tracing | Distributed tracing |
| Spring Cloud Stream | Messaging (Kafka) |
| Feign | HTTP client déclaratif |
| WebClient | HTTP client réactif |
| Docker | Conteneurisation |
| Kubernetes | Orchestration |