Socle V004 – Exemples de Code

Socle V004 - Exemples de Code

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

Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *