JMS / IBM MQ · Prio 9 · Vorher/Nachher

JMS Consumer: doppelte Verarbeitung verhindern

Ein Message Driven Bean verarbeitet Rechnungsereignisse nicht idempotent. Nach dem Refactoring gibt es Idempotency-Key, Retry-Klassifikation und Outbox/Inbox-Nachweis.

← Refactoring Übersicht
JMS Consumer: doppelte Verarbeitung verhindern Diagramm
Fachlich-technischer Schnitt vom Legacy-Problem zur refactorbaren Zielstruktur.
VorherMDB verarbeitet direkt
Schritt 1eventId erzwingen
Schritt 2Inbox-Tabelle
NachherIdempotent Consumer
Problemverständnis
AspektBeschreibung
SymptomBei MQ-Retry wird Versand mehrfach ausgelöst; technische Exceptions und fachliche Ablehnungen landen im gleichen Fehlerpfad.
RisikoDoppelte Briefe, doppelte Buchungen oder unklare DLQ-Klärung.
Refactoring-ZielFachlichen Ereignisschlüssel speichern, Verarbeitung klassifizieren und technische Wiederholung von fachlicher Ablehnung trennen.
Idempotent ConsumerInbox PatternRetry PolicyDead Letter ChannelAdapterCircuit Breaker
Vorher: MDB mit direktem Seiteneffekt
Warum kritisch: Dieser Code mischt Verantwortlichkeiten. Er ist nicht automatisch schlecht, aber Änderungen sind teuer, weil Fachlogik, Infrastruktur und Fehlerbehandlung verklebt sind.
Vorher: MDB mit direktem Seiteneffekt
@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/InvoiceEvents"),
    @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
})
public class LegacyInvoiceEventMdb implements MessageListener {
    @EJB private ShippingDocumentService shipping;
    @EJB private ArchiveService archive;

    public void onMessage(Message message) {
        try {
            String payload = message.getBody(String.class);
            InvoiceCreatedEvent event = InvoiceCreatedEvent.fromJson(payload);

            // Problem: bei Rollback/Retry wird Dokument evtl. mehrfach erzeugt.
            shipping.createAndSendLetter(event.invoiceNo(), event.customerNo());
            archive.storeBusinessDocument(event.invoiceNo(), payload);
        } catch (Exception ex) {
            throw new EJBException(ex); // alles wird Retry/DLQ, auch fachliche Fehler
        }
    }
}
Inbox-Tabelle als fachlicher Verarbeitungsnachweis
Refactoring-Regel: Erst Verhalten sichern, dann Struktur ändern. Ohne Golden Master oder Characterization Test kann ein schönes Design fachlich falsch sein.
Inbox-Tabelle als fachlicher Verarbeitungsnachweis
CREATE TABLE INBOX_EVENT_PROCESSING (
  EVENT_ID        VARCHAR2(120) NOT NULL,
  CONSUMER_NAME   VARCHAR2(80)  NOT NULL,
  STATUS          VARCHAR2(20)  NOT NULL,
  CORRELATION_ID  VARCHAR2(120),
  REASON_CODE     VARCHAR2(80),
  PROCESSED_AT    TIMESTAMP,
  CREATED_AT      TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
  CONSTRAINT PK_INBOX_EVENT PRIMARY KEY (EVENT_ID, CONSUMER_NAME)
);
Nachher: Idempotenter Consumer mit Inbox und Fehlerklassifikation
Warum besser: Der fachliche Ablauf ist erkennbar. Technische Details sitzen hinter Ports/Adaptern, Regeln sind benannt und Tests können ohne kompletten Legacy-Stack laufen.
Nachher: Idempotenter Consumer mit Inbox und Fehlerklassifikation
@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/InvoiceEvents"),
    @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
})
public class InvoiceEventConsumer implements MessageListener {
    @EJB private InboxRepository inboxRepository;          // Pattern: Inbox
    @EJB private InvoiceEventHandler handler;              // Pattern: Application Service
    @EJB private EventFailureClassifier failureClassifier; // Pattern: Policy

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void onMessage(Message message) {
        String eventId = requiredProperty(message, "eventId");
        String correlationId = optionalProperty(message, "correlationId");
        String payload = body(message);

        if (inboxRepository.alreadyProcessed(eventId, "shipping-documents")) {
            return; // Pattern: Idempotent Consumer
        }

        try {
            InvoiceCreatedEvent event = InvoiceCreatedEvent.fromJson(payload);
            handler.handle(event, correlationId);
            inboxRepository.markProcessed(eventId, "shipping-documents", correlationId);
        } catch (BusinessRejectException ex) {
            inboxRepository.markRejected(eventId, ex.reasonCode(), correlationId);
            // Keine technische Wiederholung: Fachliche Ablehnung wird geklärt.
        } catch (Exception ex) {
            RetryDecision decision = failureClassifier.classify(ex);
            inboxRepository.markTechnicalFailure(eventId, decision.reason(), correlationId);
            throw new EJBException(ex); // Retry/DLQ bleibt technisch korrekt.
        }
    }
}
Wirkung und Trade-offs

Deduplizierung

MQ-Retry erzeugt keine doppelten Dokumente.

Diagnose

Inbox zeigt verarbeitet, fachlich abgelehnt oder technisch fehlgeschlagen.

DLQ-Qualität

Nur echte technische Fehler landen im Wiederanlauf.

Audit

Correlation-ID und Event-ID sind nachvollziehbar.

Wichtig: Refactoring heißt nicht Big-Bang-Neuschreiben. Die alte Schnittstelle darf stabil bleiben, während innen schrittweise sauberere Strukturen entstehen.
⌂ Cockpit