Podstawy - Tworzymy własnego Event Publishera
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.
@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ę.
@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.
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.
@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.
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.
@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.
@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.
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ść!