19 – Exemples
Version : 4.0.0 Date : 2025-12-09
1. Application minimale
1.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.1</version>
</parent>
<groupId>com.example</groupId>
<artifactId>my-socle-app</artifactId>
<version>1.0.0</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Socle V4 -->
<dependency>
<groupId>eu.lmvi</groupId>
<artifactId>socle-v004</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2 Application.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.example", "eu.lmvi.socle"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1.3 application.yml
socle:
app_name: my-app
env_name: ${ENV_NAME:DEV}
region: ${REGION:local}
server:
port: ${HTTP_PORT:8080}
logging:
config: classpath:log4j2.xml
1.4 Worker simple
package com.example.worker;
import eu.lmvi.socle.worker.Worker;
import org.springframework.stereotype.Component;
@Component
public class HelloWorker implements Worker {
@Override
public String getName() {
return "hello-worker";
}
@Override
public void initialize() {
System.out.println("Hello Worker initialized");
}
@Override
public void start() {
System.out.println("Hello Worker started");
}
@Override
public void doWork() {
System.out.println("Hello from worker!");
}
@Override
public void stop() {
System.out.println("Hello Worker stopped");
}
@Override
public boolean isHealthy() {
return true;
}
@Override
public Map<String, Object> getStats() {
return Map.of("status", "running");
}
}
2. Worker Kafka Consumer
package com.example.worker;
import eu.lmvi.socle.worker.AbstractWorker;
import eu.lmvi.socle.techdb.TechDbManager;
import org.apache.kafka.clients.consumer.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class KafkaConsumerWorker extends AbstractWorker {
@Autowired
private TechDbManager techDb;
private KafkaConsumer<String, String> consumer;
private String topic = "my-topic";
private long lastOffset = 0;
@Override
public String getName() {
return "kafka-consumer";
}
@Override
public int getStartPriority() {
return 10;
}
@Override
protected void doInitialize() {
// Restaurer l'offset
lastOffset = techDb.getOffset("kafka", topic + "-0").orElse(0L);
log.info("Starting from offset: {}", lastOffset);
// Créer le consumer
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "my-group");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("enable.auto.commit", "false");
consumer = new KafkaConsumer<>(props);
consumer.subscribe(List.of(topic));
}
@Override
protected void doProcess() {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
processMessage(record);
lastOffset = record.offset();
incrementProcessed();
}
// Persister périodiquement
if (processedCount.get() % 100 == 0) {
techDb.saveOffset("kafka", topic + "-0", lastOffset, null);
}
}
private void processMessage(ConsumerRecord<String, String> record) {
log.debug("Processing: key={}, value={}", record.key(), record.value());
// Traitement...
}
@Override
protected void doStop() {
// Sauvegarder l'offset final
techDb.saveOffset("kafka", topic + "-0", lastOffset, null);
if (consumer != null) {
consumer.close();
}
}
@Override
public Map<String, Object> getStats() {
Map<String, Object> stats = new HashMap<>(super.getStats());
stats.put("lastOffset", lastOffset);
return stats;
}
}
3. Worker HTTP API
package com.example.worker;
import eu.lmvi.socle.worker.Worker;
import eu.lmvi.socle.kv.KvBus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
@Component
@RestController
@RequestMapping("/api/orders")
public class OrderApiWorker implements Worker {
@Autowired
private KvBus kvBus;
@Autowired
private OrderService orderService;
private volatile boolean running = false;
@Override
public String getName() {
return "order-api";
}
@Override
public boolean isPassive() {
return true; // Pas de doWork cyclique
}
@Override
public void initialize() {}
@Override
public void start() {
running = true;
}
@Override
public void doWork() {
// Passif - traitement via endpoints REST
}
@Override
public void stop() {
running = false;
}
@Override
public boolean isHealthy() {
return running;
}
@Override
public Map<String, Object> getStats() {
return Map.of("running", running);
}
// === REST Endpoints ===
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
Order order = orderService.create(request);
// Cache l'order
kvBus.putJson("order:" + order.getId(), order);
kvBus.setTtl("order:" + order.getId(), Duration.ofHours(1));
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
@GetMapping("/{id}")
public ResponseEntity<Order> getOrder(@PathVariable String id) {
// Vérifier le cache d'abord
Optional<Order> cached = kvBus.getJson("order:" + id, Order.class);
if (cached.isPresent()) {
return ResponseEntity.ok(cached.get());
}
// Sinon charger depuis la DB
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
4. Worker Schedulé (Cron)
package com.example.worker;
import eu.lmvi.socle.worker.AbstractWorker;
import org.springframework.stereotype.Component;
@Component
public class DailyReportWorker extends AbstractWorker {
@Override
public String getName() {
return "daily-report";
}
@Override
public String getSchedule() {
return "0 0 6 * * ?"; // Tous les jours à 6h
}
@Override
public boolean isScheduled() {
return true;
}
@Override
protected void doProcess() {
log.info("Generating daily report...");
// Collecter les données
ReportData data = collectReportData();
// Générer le rapport
Report report = generateReport(data);
// Envoyer par email
sendReportByEmail(report);
log.info("Daily report sent successfully");
incrementProcessed();
}
private ReportData collectReportData() {
// ...
return new ReportData();
}
private Report generateReport(ReportData data) {
// ...
return new Report();
}
private void sendReportByEmail(Report report) {
// ...
}
}
5. Pipeline de traitement
package com.example.pipeline;
import eu.lmvi.socle.pipeline.*;
import org.springframework.stereotype.Component;
@Component
public class OrderProcessingPipeline {
@Autowired
private PipelineEngine engine;
@Autowired
private OrderValidator validator;
@Autowired
private OrderEnricher enricher;
@Autowired
private PaymentProcessor paymentProcessor;
@Autowired
private NotificationService notificationService;
public PipelineResult<ProcessedOrder> process(Order order) {
Pipeline<Order, ProcessedOrder> pipeline = PipelineBuilder
.<Order, ProcessedOrder>create("order-processing")
.addStep(new ValidationStep(validator))
.addStep(new EnrichmentStep(enricher))
.addStep(new PaymentStep(paymentProcessor))
.addStep(new NotificationStep(notificationService))
.build();
return engine.execute(pipeline, order);
}
}
// Étape de validation
class ValidationStep implements PipelineStep<Order, ValidatedOrder> {
private final OrderValidator validator;
public ValidationStep(OrderValidator validator) {
this.validator = validator;
}
@Override
public String getName() {
return "validation";
}
@Override
public StepResult<ValidatedOrder> execute(Order input, PipelineContext context) {
List<String> errors = validator.validate(input);
if (!errors.isEmpty()) {
return StepResult.failure(getName(),
new ValidationException(errors), Duration.ZERO, 1);
}
return StepResult.success(getName(), new ValidatedOrder(input), Duration.ZERO);
}
@Override
public boolean isRetryable() {
return false;
}
}
// Étape de notification (optionnelle)
class NotificationStep implements PipelineStep<ProcessedOrder, ProcessedOrder> {
private final NotificationService notificationService;
@Override
public String getName() {
return "notification";
}
@Override
public StepResult<ProcessedOrder> execute(ProcessedOrder input, PipelineContext context) {
try {
notificationService.sendOrderConfirmation(input);
return StepResult.success(getName(), input, Duration.ZERO);
} catch (Exception e) {
return StepResult.failure(getName(), e, Duration.ZERO, 1);
}
}
@Override
public boolean isOptional() {
return true; // Le pipeline continue même si la notif échoue
}
}
6. Service avec résilience
package com.example.service;
import eu.lmvi.socle.resilience.*;
import org.springframework.stereotype.Service;
@Service
public class ExternalApiService {
@Autowired
private RetryTemplate retryTemplate;
@Autowired
private CircuitBreakerRegistry cbRegistry;
@Autowired
private KvBus kvBus;
private final OkHttpClient httpClient;
public Data fetchData(String id) {
// Circuit breaker + Retry + Cache fallback
CircuitBreaker cb = cbRegistry.getOrCreate("external-api");
return cb.executeWithFallback(
() -> retryTemplate.execute(() -> doFetchData(id)),
() -> getCachedData(id)
);
}
private Data doFetchData(String id) throws IOException {
Request request = new Request.Builder()
.url("https://api.example.com/data/" + id)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("API returned " + response.code());
}
Data data = parseResponse(response.body().string());
// Mettre en cache
kvBus.putJson("cache:data:" + id, data);
kvBus.setTtl("cache:data:" + id, Duration.ofMinutes(5));
return data;
}
}
private Data getCachedData(String id) {
return kvBus.getJson("cache:data:" + id, Data.class)
.orElseThrow(() -> new RuntimeException("No cached data available"));
}
}
7. Configuration multi-environnement
application.yml (base)
socle:
app_name: ${APP_NAME:my-app}
env_name: ${ENV_NAME:DEV}
region: ${REGION:local}
spring:
profiles:
active: ${PROFILE:dev}
application-dev.yml
socle:
kvbus:
mode: in_memory
techdb:
console:
enabled: true
admin:
auth:
enabled: false
logging:
level:
eu.lmvi.socle: DEBUG
application-prod.yml
socle:
kvbus:
mode: redis
redis:
host: ${REDIS_HOST}
password: ${REDIS_PASSWORD}
techdb:
console:
enabled: false
logging:
forwarder:
enabled: true
auth:
enabled: true
admin:
auth:
enabled: true
logging:
level:
eu.lmvi.socle: INFO
8. Dockerfile complet
# Build stage
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN apk add --no-cache maven && \
mvn clean package -DskipTests
# Runtime stage
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Security
RUN addgroup -S app && adduser -S app -G app
USER app
# Copy artifact
COPY --from=build --chown=app:app /app/target/*.jar app.jar
# Config
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom"
# Health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD wget -qO- http://localhost:8080/admin/health/live || exit 1
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
9. Kubernetes deployment complet
Voir 16-KUBERNETES pour l’exemple complet de déploiement K8s.
10. Références
- 03-QUICKSTART – Démarrage rapide
- 05-WORKERS – Workers
- 17-HOWTO – How-To Guides

Laisser un commentaire