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ść!