Systeme d'alarme IoT evenementiel : hexagonal, CQRS, Kafka
Backend d'un systeme de video-surveillance dont le capteur est un iPhone recycle. Le probleme est simple, la structure est le sujet : - domain/ sans aucun import de framework (verifie par 7 regles ArchUnit qui tournent aussi pendant le docker build) - CQRS strict : write et read ne partagent ni modele ni port - AlarmDecision en sealed interface -> switch exhaustif verifie a la compilation, plutot qu'une chaine de if - Kafka en KRaft (Zookeeper est supprime depuis Kafka 4.0), cle de partition = deviceId pour garantir l'ordre par capteur - ErrorHandlingDeserializer + DLQ : un message malforme ne bloque pas le flux des evenements suivants - notification WebSocket differee apres commit, pour ne jamais afficher une intrusion qui partirait en rollback 31 tests au vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
63ead8a966
81 changed files with 5051 additions and 0 deletions
|
|
@ -0,0 +1,79 @@
|
|||
package me.belkacemi.iotalarm.presentation.advice;
|
||||
|
||||
import me.belkacemi.iotalarm.domain.exception.DomainException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Traduit les exceptions en reponses HTTP, au format RFC 7807 (ProblemDetail).
|
||||
*
|
||||
* <p>C'est ici, et nulle part ailleurs, que le domaine rencontre HTTP. Une
|
||||
* {@link DomainException} ne sait pas ce qu'est un code 409 - et c'est
|
||||
* precisement ce qui permet de la lever depuis l'agregat sans le coupler au web.</p>
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
/**
|
||||
* Regle metier violee : armer une alarme deja armee, acquitter deux fois.
|
||||
* 409 et non 400 - la requete est bien formee, c'est l'etat qui s'y oppose.
|
||||
*/
|
||||
@ExceptionHandler(DomainException.class)
|
||||
public ProblemDetail onDomainException(DomainException exception) {
|
||||
log.info("Regle metier refusee : {}", exception.getMessage());
|
||||
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.CONFLICT, exception.getMessage());
|
||||
problem.setTitle("Regle metier violee");
|
||||
problem.setType(URI.create("urn:iot-alarm:domain-rule-violation"));
|
||||
return problem;
|
||||
}
|
||||
|
||||
/** Donnee invalide a la frontiere : type de capteur inconnu, deviceId malforme. */
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ProblemDetail onIllegalArgument(IllegalArgumentException exception) {
|
||||
log.info("Requete rejetee : {}", exception.getMessage());
|
||||
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.BAD_REQUEST, exception.getMessage());
|
||||
problem.setTitle("Requete invalide");
|
||||
problem.setType(URI.create("urn:iot-alarm:invalid-request"));
|
||||
return problem;
|
||||
}
|
||||
|
||||
/** Echec de Bean Validation : champs manquants ou hors bornes. */
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ProblemDetail onValidationError(MethodArgumentNotValidException exception) {
|
||||
String details = exception.getBindingResult().getFieldErrors().stream()
|
||||
.map(error -> error.getField() + " : " + error.getDefaultMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.BAD_REQUEST, details);
|
||||
problem.setTitle("Validation echouee");
|
||||
problem.setType(URI.create("urn:iot-alarm:validation-failed"));
|
||||
return problem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filet de securite. On journalise la stacktrace mais on ne la renvoie pas :
|
||||
* l'endpoint d'ingestion est expose publiquement.
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ProblemDetail onUnexpectedError(Exception exception) {
|
||||
log.error("Erreur inattendue", exception);
|
||||
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"Une erreur interne est survenue.");
|
||||
problem.setTitle("Erreur interne");
|
||||
return problem;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue