W rozdziale
Wstrzykiwanie zależności: DI & IoC
opisaliśmy w jaki sposób kontener Springa tworzy i udostępnia obiekty w ramach swojego kontenera. Wspomnieliśmy tam, że obiekty
te nazywamy beanami. Teraz chcemy rozszerzyć wiedzę na temat beanów o dodatkowe zagadnienie. Pomówimy o zakresach ich działania.
Adnotacja @Scope
Domyślnie każdy bean w Springu jest inicjowany tylko raz co skutkuje tym, że istnieje tylko jedna instancja tego beana w ramach kontenera.
Innymi słowy, wstrzykując bean
ItemService w różnych miejscach w aplikacji zawsze używamy tego samego obiektu
(obiekty te są singletonami).
Może się jednak zdarzyć, że takie rozwiązanie nie będzie nam wystarczało i będziemy chcieli zmienić to domyślne ustawienie.
W tym celu możemy skorzystać z adnotacji
@Scope, która pozwala nam użyć innego zakresu działania.
Za jej pomocą możemy ustawić kilka różnych rozdzajów zakresów:
prototype,
request,
session,
application,
websocket, a także...
singleton
(wówczas nasz bean będzie dostępny wszędzie w ramach kontenera, identycznie jakbyśmy nie ustawiali żadnego scope'u).
Zobaczmy teraz czym różnią się powyższe zakresy. Wyjaśnienia rozpocznijmy od dokładniejszego omówienia zakresu
singleton.
- Singleton
Tak jak już wspominaliśmy, w ramach całego kontenera Springa istnieje tylko jedna instancja takiego obiektu.
Co to oznacza w praktyce? To znaczy, że wstrzykując obiekt, w każdym miejscu wstrzyknięcia otrzymujemy dokładnie tą samą instancję obiektu.
Zatem jeśli na przykład odczytamy adres obiektu w jednej klasie, to będzie on dokładnie taki sam w każdej innej klasie.
Wywołując metodę toString na obiekcie ItemService, otrzymamy wskazanie na to samo miejsce w pamięci
(zarówno w klasie ItemController jak i ReportController).
W naszym przypadku na konsoli zostało wypisane:
- ItemService@3a6609f2
- ItemService@3a6609f2
Tak samo będzie to wyglądało w każdej kolejnej klasie, do której wstrzykniemy ItemService. Oznacza to, że zmieniając obiekt w jednej klasie
będziemy mogli tą zmianę odczytać w każdej innej klasie gdzie ten obiekt jest wstrzyknięty.
- Prototype
Prototype oznacza, że w każdym kolejnym miejscu wstrzyknięcia będzie tworzona nowa instancja obiektu.
Oczywiście w przypadku serwisów (klas oznaczonych adnotacją @Service) bezstanowych (stateless) nie ma to większego sensu,
gdyż jedna instancja (jak w przypadku singletona) w zupełności wystarcza do obsługi operacji przetwarzania logiki biznesowej.
Natomiast takie rozwiązanie staje się bardzo przydatne w przypadku klas przechowujących stan, gdy wymagane jest
kolekcjonowanie danych niezależnie, w kolejnych miejscach wstrzyknięcia.
Zobaczmy jak to będzie wyglądało na przykładzie klasy implementującej stworzony przez nas interfejs ItemCollector:
Wywołując metodę toString na obiektach ItemCollector, otrzymamy wskazania na dwa różne miejsca w pamięci
(inne w klasie ItemController, a inne w klasie ReportController).
W naszym przypadku na konsoli zostało wypisane:
- ItemCollector@20d8599c
- ItemCollector@15849e84
Widzimy więc, że ItemController i ReportController
przechowują dwie różne instancje obiektu ItemCollector.
Zmiana danych w jednym obiekcie
ItemCollector nie będzie miała wpływu na dane przechowywane w drugiej instancji.
Innymi słowy, zmieniając obiekt w jednej klasie nie będziemy mogli tej zmiany odczytać w innej klasie.
-
Request
Request oznacza, że nowa instancja obiektu będzie tworzona za każdym razem, gdy odbierzemy
nowe żądanie (request) HTTP. W ten sposób możemy niezależnie przetwarzać dane tak, aby nie
"mieszać" ich między kolejnymi żądaniami. Trzeba przy tym pamiętać, że takie rozwiązanie będzie generowało znaczną ilość obiektów,
więc najlepiej będzie nie przechowywać w nich dużych kolekcji danych (konsumpcja pamięci wzrośnie wtedy ekstremalnie).
Wywołując metodę toString na obiekcie ItemRequestComponent, otrzymamy wskazania na różne miejsca w pamięci
dla każdego kolejnego żądania (requestu) wysłanego na ścieżkę "/api/items/request", na przykład:
- ItemRequestComponent@aa51976d
- ItemRequestComponent@41849f84
- ItemRequestComponent@e5149e32
...
-
Session
Session oznacza, że nowa instancja obiektu będzie tworzona za każdym razem, gdy zostanie stworzona nowa sesja, a konkretnie nowy obiekt
HttpSession. Umożliwia to na przykład stworzenie dedykowanej klasy,
która będzie przechowywała podstawowe informacje o zalogowanym użytkowniku.
W tym przypadku, zarówno w klasie UserServiceImpl jak i ItemServiceImpl (a także w dowolnej innej klasie ze wstrzykniętym obiektem CurrentUser)
otrzymamy tą samą instancję obiektu CurrentUser. Wykonanie metody toString zawsze wskaże na to samo miejsce pamięci (w ramach tej samej sesji).
Jeśli mamy wielu użytkowników zalogowanych do aplikacji, wtedy dla każdego z nich będzie przypisana odrębna instancja obiektu CurrentUser.
-
Application
Application oznacza, że nowa instancja obiektu będzie tworzona raz i będzie istniała w ramach cyklu życia całego kontekstu serwletu (ServletContext).
Zatem jeśli mamy więcej aplikacji działających w ramach tego samego ServletContext, to wszystkie te aplikacje będą współdzieliły jedną instancję
obiektu.
Możemy powiedzieć, że zakres application rozszerza właściwości zakresu singleton,
który jak pamiętamy umożliwia tworzenie jednego obiektu w ramach pojedynczego kontenera aplikacji. W przypadku kilku aplikacji uruchomionych w ramach jednego
ServletContext, singleton - w przeciwieństwie do application -
utworzy kilka instancji obiektu.
-
Websocket
Websocket to ostatni rodzaj zakresu, który jest udostępniony przez obecną wersję Springa (Spring 5).
W tym przypadku będzie istniała jedna instancja obiektu per sesja WebSocket-u:
Gdzie się podział zakres globalSession?
W tym miejscu należy wspomnieć jeszcze o pewnym problemie, który się pojawia gdy szukamy informacji o zakresach w internecie.
Do wersji 4.3 Spring umożliwiał ustawienie jeszcze jednego zakresu - o nazwie
globalSession.
Opcja ta została usunięta ze Springa 5, ponieważ dotyczyła ona portletów, a ta technologia przestała być przez Springa wspierana.
Niemniej wiele stron ciągle zawiera tą informację, a to skądinąd powoduje, że podczas czytania tych materiałów możemy czuć się nieco zagubieni.
Niepotrzebnie. W Springu 5 nie ma już scope'u
globalSession, co możecie zweryfikować sami zaglądając na strony
pod linkami udostępnionymi na końcu tego rozdziału.
ScopedProxyMode - TARGET_CLASS
W przykładach przytoczonych powyżej w kilku miejscach użyliśmy dodatkowego atrybutu adnotacji
@Scope -
proxyMode.
Atrybut ten mówi Springowi, że w momencie tworzenia kontekstu aplikacji ma on stworzyć i wstrzyknąć - zamiast obiektu docelowego - obiekt proxy.
Następnie, gdy tylko zaistnieją odpowiednie warunki (zostanie stworzona sesja, bądź serwer otrzyma request HTTP w zależności do rodzaju zakresu)
zostanie dopiero stworzona instancja docelowa obiektu. Dlaczego tak jest? Wszystko przez to, że w momencie tworzenia kontekstu aplikacji
nie istnieje jeszcze żadne aktywne żądanie lub sesja, więc nie można stworzyć w pełni reprezentatywnego obiektowego odpowiednika sesji czy też żądania.
Co się zatem stanie jeśli nie podamy tego atrybutu? Wtedy już podczas startu aplikacji otrzymamy bardzo "sympatyczny" wyjątek:
Wisienka na torcie
Na koniec wisienka na torcie (taka mała, ale jednak).
Jeśli nie podoba się nam ustawianie dodatkowego atrybutu i chcemy aby nasz kod wyglądał jeszcze bardziej czytelnie, to możemy użyć specjalnych adnotacji (zamiast
@Scope),
które mają domyślnie ustawiony odpowiedni
proxyMode. Dostępne są następujące adnotacje:
@RequestScope,
@SessionScope i
@ApplicationScope.
Rekomendacja
W zasadzie wszystko co chcielibyśmy tu napisać zostało już gdzieś "przemycone" w treści tego rozdziału.
Możemy ewentualnie jeszcze dodać, że najczęściej używanym scopem w trakcie naszej przygody ze Springiem był i jest scope domyślny
czyli
singleton. Natomiast prawdą jest też, że każdy z tych zakresów jest bardzo potrzebny, a i nieraz ich specyficzne właściwości
stają się wręcz niezastąpione, gdy potrzebujemy wykonać zadanie, do którego jedna instancja nam nie wystarczy.
Praktyka
W zdecydowanej większości przypadków używamy zakresu singleton, jednak zdarzają się przypadki użycia zakresu request oraz session.
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Czy informacje, które otrzymałeś, były pomocne?
Jeśli tak, zapraszam Cię do podarowania mi kawy.
Topowe Materiały
Spring IO: Bean Scopes
Baeldung: Quick Guide to Spring Bean Scopes
Udemy: [NEW] Spring Boot 3, Spring 6 & Hibernate for Beginners — polskie napisy