Dzisiaj chciałbym Ci pokazać, w jaki sposób możemy wprowadzić publikowanie zdarzeń do naszej aplikacji. Głównie mam tu na myśli eventy w obrębie jednej aplikacji. Zobaczymy, jaki problem rozwiązuje ich wprowadzenie. Jednak należy też pamiętać, że nie zawsze chcielibyśmy ten “problem” rozwiązywać. Czasami chcielibyśmy, explicite zobaczyć, jak dany proces wygląda. Mieć go po prostu w jednym miejscu. Przejdźmy zatem do kodu.
Zdefiniowanie problemu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@RequiredArgsConstructor
public class MainService {
private final ServiceA serviceA;
private final ServiceB serviceB;
public void doSomething() {
System.out.println("calling other services");
serviceA.doSomething();
serviceB.doSomething();
}
}
Mamy MainService
który do wykonania swojego zadania korzysta z ServiceA
i ServiceB
. Nic nadzwyczajnego. Aby mieć “siatkę bezpieczeństwa” do weryfikacji działania naszej aplikacji, napiszmy sobie test. Tak naprawdę nie będzie to test, bo nie ma asercji. Traktujmy to jako po prostu środowisko uruchomieniowe.
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class MainServiceIntTest {
@Autowired
MainService mainService;
@Test
void test() {
mainService.doSomething();
}
}
Jeśli uruchomimy powyższą metodę, to na konsoli pojawią się nam następujące zapisy:
1
2
3
calling other services
doing something in ServiceA
doing something in ServiceB
Czyli wszystko działa, jak należy.
Ujawnienie problemu
Załóżmy, że do wykonania naszego zadania niezbędny jest teraz serwis ServiceC
, który, na potrzeby przykładu, nie różni się niczym od pozostałych serwisów.
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
@Component
@RequiredArgsConstructor
public class MainService {
private final ServiceA serviceA;
private final ServiceB serviceB;
private final ServiceC serviceC;
public void doSomething() {
System.out.println("calling other services");
serviceA.doSomething();
serviceB.doSomething();
serviceC.doSomething();
}
}
@Component
public class ServiceC {
public void doSomething() {
System.out.println("doing something in ServiceC");
}
}
Aby go wprowadzić, musieliśmy wykonać dwa kroki. Pierwszym z nich jest dodanie nowego serwisu. Drugim, dodanie go jako zależności w MainService
. Uruchamiając test, zobaczymy, że faktycznie nowa linijka pojawia się w konsoli.
1
2
3
4
calling other services
doing something in ServiceA
doing something in ServiceB
doing something in ServiceC
Wprowadzamy ApplicationEventPublisher
Jak możemy zmienić ten stan rzeczy, aby nie uzależniać MainService
od kolejnych klas? Możemy skorzystać z dostępnego w Springu ApplicationEventPublisher
. Jak to zrobić. Wystarczy, że dodamy go jako zależność do MainService
zamiast wszystkich widocznych tam serwisów. Następnie opublikujemy zdarzenie, na które każdy z serwisów będzie nasłuchiwał. Zdarzeniem może być dowolna klasa Javowa. W naszym przypadku będzie to pusty rekord o nazwie SampleEvent
.
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");
}
}
Tak samo robimy dla pozostałych serwisów, jak zostało to zaprezentowane w ServiceA
. Należy pamiętać, że metoda oznaczona przez adnotację @EventListener
musi być publiczna oraz znajdować się w beanie Springa. Dodatkowo w sygnaturze tej metody jako parametr musi znaleźć się typ zdarzenia, na które nasłuchujemy. Całą resztą konfiguracji i połączeń zajmie się za nas Spring. To on powiąże ze sobą te wszystkie zależności. Jeśli uruchomimy nasz “test” to powinien działać w ten sam sposób, jak miało to miejsce przed tymi zmianami.
Weryfikacja nowego rozwiązania
W sytuacji, gdy dojdzie nowy serwis, to tylko w nim należy dokonać zmian. MainService
pozostanie nienaruszony.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class ServiceD {
@EventListener
public void handle(SampleEvent event) {
doSomething();
}
public void doSomething() {
System.out.println("doing something in ServiceD");
}
}
I to tyle. Weryfikując dodanie nowego serwisu, dostaniemy w konsoli następujące wpisy:
1
2
3
4
5
calling other services
doing something in ServiceA
doing something in ServiceB
doing something in ServiceC
doing something in ServiceD
Podsumowanie
Publikowanie zdarzeń pozwala nam odwrócić zależność. Komponent nadrzędny nie musi być świadomy komponentów podrzędnych, aby wykonać dany proces. Wystarczy, że opublikuje zdarzenie, a reszta “wykona się sama”. Daje nam to naprawdę wiele możliwości. Jednak jak wspomniałem na początku. W takim podejściu proces biznesowy jest porozrzucany po aplikacji. Nie mamy go przedstawionego w jednym miejscu, a czasem chcielibyśmy, aby tak było. Dlatego odpowiednie techniki oraz narzędzie zawsze dostosowujemy do rozwiązywanego problemu. To by było na tyle, dziękuję Ci za Twój czas. Na razie i cześć!