Tworząc kolejne aplikacje oparte o Springa nie zastanawiamy się nad tym jak ten słynny framework działa pod spodem. Po prostu nie ma na to czasu, ponieważ trzeba dostarczać “wartość biznesową”. Dopiero kiedy jego zawiłości sprawią nam kłopot na produkcji to zaczynamy zagłębiać się w jego czeluści.

Gdyby tak jednak odwrócić tą sytuację i zacząć dmuchać na zimne? W wolnej chwili, z własnej nieprzymuszonej woli, zajrzeć do kodu Springa? Ostatnio taki cel zaczął mi przyświecać. Zacząłem się zastanawiać co dzieje się pod maską narzędzia, z którego korzystam każdego dnia w pracy. I z tego właśnie powodu chciałbym Ci dzisiaj przedstawić jak działa ApplicationEventPublisher, który pozwala na publikowanie zdarzeń wewnątrz aplikacji.

ApplicationEventPublisher to nic innego jak interfejs z dwoma metodami. Jedna z nich przyjmuje jako argument abstrakcyjną klasę ApplicationEvent, natomiast druga ogólny typ Object. Skąd takie rozdzielnie? ApplicationEvent jest rozwiązaniem historycznym. Kiedyś aby umożliwić opublikowanie zdarzenia w aplikacji należało w klasie je reprezentującej rozszerzyć klasę o nazwie ApplicationEvent. Natomiast klasy nasłuchujące na takie zdarzenie musiały zaimplementować intefejs ApplicationListener o wcześniej podanym typie. Takie podejście sprawiało, że aplikacja była mocno sprzężona z frameworkiem. Z tego powodu postanowiono zmienić podejście i rozluźniono obostrzenia.

As of Spring 4.2, the event infrastructure has been significantly improved and offers an annotation-based model as well as the ability to publish any arbitrary event (that is, an object that does not necessarily extend from ApplicationEvent). When such an object is published, we wrap it in an event for you.

Teraz zdarzenia mogą być czymkolwiek. Nie muszą korzystać z jakiegoś dodatkowego znacznika. Wystarczy je zaimplementować jako zwykłą klasę Javową, a Spring poradzi sobie z resztą. Postaje jednak pytanie, co z listenerami zainteresowanymi tymi zdarzeniami? Rozwiązaniem na ten problem jest adnotacja @EventListener, którą wykorzystujemy do oznaczenia metody potencjalnego listenera. Metoda taka musi jako parametr przyjmować typ zdarzenia jakiego się w tym miejscu spodziewamy. I to wszystko!

Tylko tak wygląda rzeczywistość po stronie użytkownika tego narzędzia. Jak jest to ze sobą pospajane wewnątrz? Zaraz do tego przejdziemy. Na początek zobaczmy implementacje wykorzystującą ten kawałek Springa.

Działanie rozwiązania w praktyce

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.ApplicationEvent;

public class ExampleEvent extends ApplicationEvent {

    public final String message;

    public ExampleEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ExamplePublisher {

    private final ApplicationEventPublisher eventPublisher;

    public void call() {
        eventPublisher.publishEvent(new ExampleEvent(this, "this is message"));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ExampleListener implements ApplicationListener<ExampleEvent> {

    @Override
    public void onApplicationEvent(ExampleEvent event) {
        log.debug("message: {}", event.message);
    }

}

Powyżej zostało przedstawione rozwiązanie oparte o pierwotny mechanizm. Każda klasa posiada zależność w importach do Springa, co mocno wiąże implementacje z frameworkiem. Sprawdźmy natomiast jak wygląda drugie rozwiązanie.

1
2
public record ExampleEvent(String message) {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ExamplePublisher {

    private final ApplicationEventPublisher eventPublisher;

    public void call() {
        eventPublisher.publishEvent(new ExampleEvent("this is message"));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ExampleListener {

    @EventListener
    public void handle(ExampleEvent event) {
        log.debug("message: {}", event.message());
    }

}

Pod kątem rozluźnienia couplingu od frameworka jest to lepsze rozwiązanie. Łatwiej jest usunąć adnotację niż zaimplemenowany interfejs. W tym miejscu można by się zastanawiać czy gra jest warta świeczki, aby dokonywać takich zmian. Na potrzeby tego artykułu załóżmy, że jest wiele korzyści przemawiających za tym podejściem.

Chętni mogliby pójść jeszcze dalej i zamiast korzystać z ApplicationEventPublisher to dla nich lepszym rozwiązaniem byłoby zastąpienie go własnym interfejsem o przykładowej nazwie EventPublisher. Jedna z implementacji takiego interfejsu mogłaby pod spodem korzystać z gotowego rozwiązania Springa. Dałoby to nam większą elastyczność i łatwiejsze wprowadzanie zmian w przyszłości, mniejszym kosztem. Dobra, ale odbiegam od sedna. Wróćmy do tego jak faktycznie działa publikowanie zdarzeń w Springu pod maską.

Skoro już wiemy, że ApplicationEventPublisher jest interfejsem to oznacza, że musi mieć jakąś implementację. Tym tropem natrafiamy na fakt, że interfejs ApplicationContext, serce kontenera Springa, rozszerza właśnie ApplicationEventPublisher. W ten oto sposób każda implementacja kontekstu aplikacyjnego musi zaimplementować metodę publishEvent. Taki tok myślowy w łatwy sposób naprowadza nas na dalsze kroki.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ExamplePublisher {

    private final ApplicationContext applicationContext;

    public void call() {
        applicationContext.publishEvent(new ExampleEvent("this is message"));
    }

}

Implementacje ApplicationContext mają dostęp do każdego beana znajdującego się w aplikacji. Podczas publikacji zdarzenia, za pośrednictwem ApplicationEventPublisher, mogą je przewertować, aby znaleźć te metody beanów, które obsłużą dane zdarzenie reprezentowane przez zwykłą klasę. Jednak sprawa jest prostsza, z racji istnienia klasy o nazwie EventListenerMethodProcessor. Jej zadaniem jest zarejestrowanie metod oznaczonych przez @EventListener jako historycznych ApplicationListener. W ten sposób ApplicationContext nie skanuje wszystkich beanów, tylko zarejestrowane listenery.

W telegraficznym skrócie, podczas tworzenia kontekstu Springa poszukiwane są metody oznaczone przez @EventListener i rejestrowywane jako ApplicationListener. Kiedy przychodzi do opublikowania zdarzenia przez ApplicationEventPublisher wykorzystywany jest ApplicationContext, który pozwala przeszukać odpowiednie listenery w swoim kontenerze. Gdy znajdzie odpowiednie to uruchamiana jest logika kryjąca się w wcześniej wspomnianych metodach oznaczonych przez @EventListener.

Mam nadzieję, że tym wpisem uchyliłem Tobie chociaż trochę rąbka tajemnicy jak działa jeden z mechanizmów Springa pod spodem. Warto jest znać lepiej narzędzia, z których na co dzień korzystamy niż tylko w sposób jaki został przedstawiony na jednym z kolejnych kursów w Internecie.