Dzisiaj będziemy kontynuować historię dotyczącą ApplicationEventPublisher z poprzedniego wpisu. Jednak teraz zmienią się wymagania. Załóżmy, że potrzebujemy jakiejś dodatkowej logiki podczas publikowania zdarzenia. W przypadku ApplicationEventPublisher, który jest dostępny w Springu, jest to trochę utrudnione. Nie mamy do tego bezpośredniego dostępu, jest to abstrakcja zarządzana przez Springa. Chciałbym Ci pokazać, jak w łatwy sposób możemy dodawać dowolną logikę do publikacji zdarzeń, bez wchodzenia w większe detale, w jaki sposób działa Spring.

Zdefiniowanie problemu

Na potrzeby rozważań skorzystamy z kodu napisanego w poprzednim artykule. Dla przypomnienia, mamy MainService publikujący zdarzenie typy SampleEvent. Na niego nasłuchują serwisy takie jak ServiceA.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Component
@RequiredArgsConstructor
public class MainService {

    private final ApplicationEventPublisher publisher;

    public void doSomething() {
        System.out.println("calling other services");
    
        publisher.publishEvent(new SampleEvent());
    }

}

public record SampleEvent() {
}

@Component
public class ServiceA {

    @EventListener
    public void handle(SampleEvent event) {
        doSomething();
    }
    
    public void doSomething() {
        System.out.println("doing something in ServiceA");
    }

}

Skorzystamy też z wcześniej napisanego testu, który uruchomi nasz przypadek. W ten sposób będziemy weryfikować nowopowstałą logikę.

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class MainServiceIntTest {

    @Autowired
    MainService mainService;
    
    @Test
    void test() {
        mainService.doSomething();
    }

}

Wprowadzenie nowej warstwy abstrakcji

Teraz przyszedł czas na utworzenie nowej abstrakcji reprezentującej nasz publisher. Będzie on nosił nazwę EventPublisher. Dodatkowo wprowadzimy także interfejs będący znacznikiem dla publikowanych zdarzeń - Event. W ten sposób będziemy chociaż trochę kontrolować, co faktycznie będzie przechodziło przez nasz publisher.

1
2
3
4
5
6
7
8
9
10
11
12
public interface EventPublisher {

    void publish(Event event);

    default void publish(Collection<Event> events) {
        events.forEach(this::publish);
    }

}

public interface Event {
}

Kolejnym krokiem będzie dodanie implementacji dla naszej abstrakcji. Nazwiemy ją JustForwardingEventPublisher, ponieważ jej zadaniem będzie delegowanie publikowania zdarzeń do publishera dostępnego w Spring.

1
2
3
4
5
6
7
8
9
10
11
@Component
@RequiredArgsConstructor
public class JustForwardingEventPublisher implements EventPublisher {

    private final ApplicationEventPublisher publisher;

    @Override
    public void publish(Event event) {
        publisher.publishEvent(event);
    }
}

Jeśli teraz skorzystamy z tak przygotowanej implementacji w MainService, to aplikacja powinna się zachować w ten sam sposób, jak to miało miejsce przed tymi zmianami. Istotne jest, aby dodać jeszcze do SampleEvent informację o tym, żeby implementował nowy interfejs Event.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public record SampleEvent() implements Event {
}

@Component
@RequiredArgsConstructor
public class MainService {

    private final EventPublisher publisher;

    public void doSomething() {
        System.out.println("calling other services");

        publisher.publish(new SampleEvent());
    }

}

Tym oto sposobem daliśmy sobie pole manewru do dalszych usprawnień.

Dodanie nowej logiki

Załóżmy, że chcielibyśmy dodać logikę, której zadaniem jest logowanie zdarzeń przechodzących przez EventPublisher. W aktualnej sytuacji jest to naprawdę proste. Musimy dodać nową implementację naszej abstrakcji.

1
2
3
4
5
6
7
8
9
10
11
@RequiredArgsConstructor
public class LoggingEventPublisher implements EventPublisher {

    private final EventPublisher publisher;

    @Override
    public void publish(Event event) {
        System.out.println("just logging");
        publisher.publish(event);
    }
}

Skorzystaliśmy tutaj ze wzorca projektowego o nazwie dekorator. Opakowujemy dowolną implementację EventPublisher dodając dodatkową logikę w postaci logowania dodatkowych informacji. Można by się tutaj pokusić o zaimplementowanie nawet warstwy persystencji każdego ze zdarzeń. Ogranicza nas tylko wyobraźnia. Sprawdźmy, jak to wygląda w praktyce.

Pewnie nie uszło Twojej uwadze, że nad nazwą klasy LoggingEventPublisher brakuje adnotacji @Component. Jest to spowodowane tym, że w tym przypadku lepiej samemu poskładać gotowe klocki niż oddać tę odpowiedzialność frameworkowi. Jest to po prostu prostsze i wygodniejsze.

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class EventPublisherConfig {

    @Bean
    EventPublisher eventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        JustForwardingEventPublisher justForwardingEventPublisher = new JustForwardingEventPublisher(
                applicationEventPublisher
        );
        return new LoggingEventPublisher(justForwardingEventPublisher);
    }

}

I tyle! Pozostałe klasy zostały nietknięte, a my możemy się cieszyć nową logiką. Wystarczy uruchomić “test” i zweryfikować działanie naszego rozwiązania.

1
2
3
4
5
6
calling other services
just logging
doing something in ServiceA
doing something in ServiceB
doing something in ServiceC
doing something in ServiceD

Dodawanie kolejnych wrapperów nie powinno stanowić w tym momencie większego problemu.

Podsumownie

Takie podejście daje nam wiele możliwości. Minusem tego rozwiązania jest fakt, że IDE, w tym przypadku IntelliJ, ma duże wsparcie dla mechanizmów Springa. W łatwy sposób pozwala nam się przenosić pomiędzy miejsce publikacji zdarzenia, a jego odbiorcami. Tutaj tego nie mamy. Oczywiście, aby uzyskać takie rozwiązanie, musieliśmy również na starcie poświęcić więcej pracy, niż ma to miejsce w przypadku zastosowania gotowego rozwiązania. Coś za coś.

Warto pamiętać o tym, że naprawdę przydatne jest korzystanie z gotowych mechanizmów dostarczonych przez framework. Nawet w kontekście budowania własnych narzędzi. Mam nadzieję, że zainspirowałem Cię do przeprowadzenia takich eksperymentów w swojej aplikacji. Tylko pamiętaj, ważne jest, aby rozwiązywały one konkretny problem, który napotkałeś. A tymczasem, na razie i cześć!