Zakres Beana (Scope)

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.
    @Component
    @Scope("singleton")
    public class ItemServiceImpl implements ItemService {
    
        ...
    }
    
    @RestController
    public class ItemController {
    
        final Logger LOG = ...
    
        @Autowired
        private ItemService itemService;    
        
        ...
        
        public void showServiceInfo() {
            LOG.info(itemService.toString());
        }    
    }
    
    @RestController
    public class ReportController {
    
        final Logger LOG = ...
    
        @Autowired
        private ItemService itemService;    
        
        ...
        
        public void showServiceInfo() {
            LOG.info(itemService.toString());
        }
    }
    
    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:
    @Component
    @Scope("prototype")
    public class DefaultItemCollector implements ItemCollector {
    
        ...
    }
    
    @RestController
    public class ItemController {
    
        final Logger LOG = ...
    
        @Autowired
        private ItemCollector itemCollector;    
        
        ...
        
        public void showCollectorInfo() {
            LOG.info(itemCollector.toString());
        }    
    }
    
    @RestController
    public class ReportController {
    
        final Logger LOG = ...
    
        @Autowired
        private ItemCollector itemCollector;    
        
        ...
        
        public void showCollectorInfo() {
            LOG.info(itemCollector.toString());
        }
    }
    
    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).
    @Component
    @Scope("request", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class ItemRequestComponent {
    
        ...
    }
    
    @RequestMapping("/api")
    @RestController
    public class ItemController {
    
        final Logger LOG = ...
    
        @Autowired
        private ItemRequestComponent itemRequestComponent;    
        
        ...
        
        @PostMapping(value = "/items/request")
        public String processRequest() {
            LOG.info(itemRequestComponent.toString());
        }    
        
    }
    
    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.
    @Component
    @Scope("session", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class CurrentUser {
    
        ...
    }
    
    @Service
    public class UserServiceImpl implements UserService {
    
        final Logger LOG = ...
    
        @Autowired
        private CurrentUser currentUser;    
        
        ...
        
        public void processUser() {
            
            LOG.info(currentUser.toString());
            
            ...
        }
    }
    
    @Service
    public class ItemServiceImpl implements ItemService {
    
        final Logger LOG = ...
    
        @Autowired
        private CurrentUser currentUser;    
        
        ...
        
        public void processUserItems() {
            
            LOG.info(currentUser.toString());
            
            ...
        }    
    }
    
    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.
    @Component
    @Scope("application", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class ApplicationSharedComponent {
    
        ...
    }
    
    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:
    @Component
    @Scope("websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class WebsocketSessionComponent {
    
        ...
    }
    

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:
java.lang.IllegalStateException: 
No thread-bound request found: Are you referring to request attributes outside of an actual web request, 
or processing a request outside of the originally receiving thread?

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.
Zdjęcie autora
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
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

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE