Spring Boot 4 – komplett guide · Lektion

Interne applikationshändelser och lyssnare

Frikoppla moduler genom domänhändelser som publiceras och konsumeras i applikationskontexten.

Lektion 2 av 413 steg

Interne applikationshändelser och lyssnare är en gratis lektion i Spring Boot 4 – komplett guide på CoddyKit. Detta är lektion 2 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Spring Boot 4 – komplett guide, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Spring Boot 4 – komplett guide innehåller totalt 4 lektioner.

Varför frikoppla moduler med händelser?

I en modulär monolit behöver en Order-modul ofta utlösa arbete i andra moduler: skicka ett bekräftelsemeddelande, uppdatera lagersaldot eller tilldela lojalitetspoäng. Det naiva tillvägagångssättet är att låta OrderService anropa EmailService, InventoryService och LoyaltyService direkt.

  • Det skapar tät koppling — Order blir beroende av varje nedströmsmodul.
  • Om en ny reaktion ska läggas till måste OrderService ändras igen.
  • Modulerna kan inte längre utvecklas eller testas oberoende av varandra.

Springs interna applikationshändelser vänder på detta: Order-modulen publicerar helt enkelt en domänhändelse och intresserade moduler lyssnar efter den. Publiceraren vet aldrig vem som lyssnar.

Domänhändelseposten

En intern händelse är bara ett vanligt objekt — i Spring Boot 4 (Java 21+) är en record det idiomatiska valet. Den bör vara oföränderlig och endast innehålla de data som lyssnarna behöver.

  • Namnge den i perfekt particip (OrderCompleted) — den beskriver något som redan har hänt.
  • Placera den i den ägande modulens paket.
  • Händelsen behöver inga Spring-annoteringar.
package com.shop.order;

import java.math.BigDecimal;
import java.util.UUID;

public record OrderCompleted(
        UUID orderId,
        String customerEmail,
        BigDecimal total) {
}

Publicera en händelse

Den ägande modulen publicerar händelsen via Springs ApplicationEventPublisher. Injicera den via konstruktorn och anropa publishEvent() efter att affärsoperationen har lyckats.

  • Publiceraren har ingen kompileringsreferens till någon lyssnare.
  • Publicering är synkron som standard — lyssnarna körs i samma tråd och i samma transaktion.
package com.shop.order;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final ApplicationEventPublisher events;
    private final OrderRepository orders;

    public OrderService(ApplicationEventPublisher events, OrderRepository orders) {
        this.events = events;
        this.orders = orders;
    }

    @Transactional
    public void complete(Order order) {
        order.markCompleted();
        orders.save(order);
        events.publishEvent(
            new OrderCompleted(order.getId(), order.getCustomerEmail(), order.getTotal()));
    }
}

Lyssna med @EventListener

Vilken Spring-bean som helst i en annan modul kan reagera genom att annotera en metod med @EventListener. Den enda metodparametern deklarerar den händelsetyp som lyssnaren bryr sig om.

  • Metoden kan ha vilket namn som helst — Spring matchar utifrån parametertypen.
  • Lyssnaren finns i sin egen modul (com.shop.notification) och refererar aldrig till Order-tjänsten.
package com.shop.notification;

import com.shop.order.OrderCompleted;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class OrderEmailListener {

    private final MailSender mail;

    public OrderEmailListener(MailSender mail) {
        this.mail = mail;
    }

    @EventListener
    void on(OrderCompleted event) {
        mail.send(event.customerEmail(),
                  "Your order " + event.orderId() + " is complete!");
    }
}

Synkron som standard — och riskerna med det

Som standard anropar publishEvent() alla matchande @EventListener-lyssnare synkront, i ordning och i den anropande tråden. Eftersom lyssnarna körs i publicerarens transaktion gäller följande:

  • En lyssnare som kastar ett undantag skickar undantaget tillbaka till publiceraren och rullar tillbaka hela transaktionen.
  • En långsam lyssnare (till exempel en som skickar ett e-postmeddelande) blockerar affärsanropet tills den är klar.

Detta är sällan vad ni vill för sidoeffekter som e-post. I nästa avsnitt visas hur båda problemen kan lösas med transaktionsbundna och asynkrona lyssnare.

Reagera först efter commit

Vanligtvis vill ni att sidoeffekter ska utföras endast om transaktionen faktiskt committas — ni bör inte skicka ett e-postmeddelande om att beställningen är klar om sparandet av beställningen senare rullas tillbaka. Använd @TransactionalEventListener, som som standard binds till fasen AFTER_COMMIT.

  • Lyssnaren körs efter att publicerarens transaktion har committats utan fel.
  • Om transaktionen rullas tillbaka hoppar man över lyssnaren.
  • Ni kan välja andra faser via phase = BEFORE_COMMIT, AFTER_ROLLBACK eller AFTER_COMPLETION.
package com.shop.notification;

import com.shop.order.OrderCompleted;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class OrderEmailListener {

    private final MailSender mail;

    public OrderEmailListener(MailSender mail) {
        this.mail = mail;
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    void on(OrderCompleted event) {
        mail.send(event.customerEmail(),
                  "Your order " + event.orderId() + " is complete!");
    }
}

Gör lyssnare asynkrona

För att förhindra att en långsam lyssnare blockerar anroparen lägger du till @Async. Lyssnaren körs då i en separat tråd från en task executor, så publishEvent() returnerar omedelbart.

  • Aktivera asynkront stöd en gång genom att lägga till @EnableAsync på en konfigurationsklass.
  • Viktigt: en @Async-lyssnare körs i sin egen tråd utan någon transaktion och utan åtkomst till publicerarens kontext. Kombinera den med @TransactionalEventListener(AFTER_COMMIT) när du vill ha "efter commit, i en bakgrundstråd".
  • Asynkrona lyssnare som kastar undantag loggas men påverkar inte publiceraren — hantera fel själv.
package com.shop.notification;

import com.shop.order.OrderCompleted;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class OrderEmailListener {

    private final MailSender mail;

    public OrderEmailListener(MailSender mail) {
        this.mail = mail;
    }

    @Async
    @TransactionalEventListener
    void on(OrderCompleted event) {
        mail.send(event.customerEmail(),
                  "Your order " + event.orderId() + " is complete!");
    }
}

Externalisering av händelser med Spring Modulith

Vanliga Spring-händelser finns endast i minnet: om applikationen kraschar mellan commit och att lyssnaren körs går händelsen förlorad. Spring Modulith tillför varaktighet genom sitt Event Publication Registry.

  • Lägg till startpaketet spring-modulith-events-jpa (eller JDBC/MongoDB).
  • Varje anrop till @TransactionalEventListener registreras i en event_publication-tabell i samma transaktion.
  • När lyssnaren är klar markeras publiceringen som slutförd. Ofullständiga publiceringar kan publiceras igen vid omstart, vilket garanterar leverans minst en gång.

Konfigurera publiceringsregistret

När händelsestartpaketet finns på classpath skapar Modulith automatiskt registers schemat och kopplar in interceptorn. En liten YAML-konfiguration styr återställningsbeteendet.

  • republish-outstanding-events-on-restart skickar in alla publiceringar som aldrig slutfördes före den senaste avstängningen på nytt.
  • Modulith exponerar också registret så att du kan övervaka eller manuellt skicka fastnade händelser på nytt.
spring:
  modulith:
    events:
      jdbc:
        schema-initialization:
          enabled: true
      republish-outstanding-events-on-restart: true
      completion-mode: delete

Filtrera händelser efter villkor

Ibland ska en lyssnare bara reagera på vissa händelser av en viss typ. Attributet condition tar ett SpEL-uttryck som utvärderas mot händelsen; lyssnaren körs bara när resultatet är true.

  • Referera till händelsens nyttolast med #root.event eller direkt till egenskapen via #-roten.
  • Detta håller filtreringen deklarativ i stället för att lägga ett if överst i metoden.
package com.shop.loyalty;

import com.shop.order.OrderCompleted;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class HighValueOrderListener {

    // Only fires for orders over 100
    @EventListener(condition = "#event.total().compareTo(new java.math.BigDecimal('100')) > 0")
    void onBigOrder(OrderCompleted event) {
        // award bonus loyalty points
    }
}

En fristående mental modell för pub/sub

Om Spring tas bort är mönstret bara ett register med lyssnare, indexerade efter händelsetyp. Kodexemplet nedan är ett vanligt Java-program som efterliknar det synkrona publicerings-/prenumerationsbeteendet — användbart för att förstå vad ApplicationEventPublisher gör under huven.

import java.util.*;
import java.util.function.Consumer;

public class MiniEventBus {
    private final Map<Class<?>, List<Consumer<Object>>> listeners = new HashMap<>();

    <T> void subscribe(Class<T> type, Consumer<T> listener) {
        listeners.computeIfAbsent(type, k -> new ArrayList<>())
                 .add(e -> listener.accept(type.cast(e)));
    }

    void publish(Object event) {
        listeners.getOrDefault(event.getClass(), List.of())
                 .forEach(l -> l.accept(event));
    }

    record OrderCompleted(String id, double total) {}

    public static void main(String[] args) {
        MiniEventBus bus = new MiniEventBus();
        bus.subscribe(OrderCompleted.class,
                e -> System.out.println("Email: order " + e.id() + " done"));
        bus.subscribe(OrderCompleted.class,
                e -> System.out.println("Loyalty: +" + (int) e.total() + " points"));
        bus.publish(new OrderCompleted("A-42", 150.0));
    }
}

Snabbkontroll: välj rätt lyssnare

Du publicerar en OrderCompleted-händelse i en @Transactional-metod som sparar en order. E-postlyssnaren måste köras endast om ordern faktiskt har sparats, och den får varken fördröja eller avbryta anropet som sparar ordern. Vilken lyssnarkonfiguration är korrekt?

Sammanfattning: frikoppla moduler med interna händelser

Du har lärt dig att frikoppla moduler med domänhändelser i applikationen:

  • Modellera händelser som oföränderliga records i dåtidsform, som ägs av den publicerande modulen.
  • Publicera via ApplicationEventPublisher.publishEvent() — publiceraren refererar aldrig till lyssnare.
  • Reagera med @EventListener (synkront, i transaktionen) eller @TransactionalEventListener (körs i en vald transaktionsfas, som standard AFTER_COMMIT).
  • Lägg till @Async + @EnableAsync för att köra sidoeffekter utanför anroparens tråd.
  • Filtrera deklarativt med SpEL-attributet condition.
  • Använd Spring Modulith's Event Publication Registry för att spara publiceringar och publicera ofullständiga publiceringar igen vid omstart, så att leverans minst en gång kan garanteras.

Resultatet blir moduler som kommunicerar genom händelser och därför kan testas och ersättas oberoende av varandra, utan direkta beroenden.

Gratis att börja

Lär dig Java med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
21
Lektioner
84

Vanliga frågor

Är lektionen ”Interne applikationshändelser och lyssnare” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Spring Boot 4 – komplett guide, inklusive ”Interne applikationshändelser och lyssnare”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Spring Boot 4 – komplett guide innehåller totalt 4 lektioner.

Vad lär jag mig i ”Interne applikationshändelser och lyssnare”?

Frikoppla moduler genom domänhändelser som publiceras och konsumeras i applikationskontexten. Ni övar på Spring Boot 4 – komplett guide med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Spring Boot 4 – komplett guide?

Du behöver inga förkunskaper. Utbildningen i Spring Boot 4 – komplett guide på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 2 av 4.

Hur lång tid tar lektionen ”Interne applikationshändelser och lyssnare”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Spring Boot 4 – komplett guide-lektionen?

Ja. Varje Spring Boot 4 – komplett guide-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Applikationsmoduler och gränsverifiering
  2. Interne applikationshändelser och lyssnare
  3. Transaktionell händelsepublicering och Outbox
  4. Integrationstestning av moduler och scenarier
← Tillbaka till Spring Boot 4 – komplett guide