Why to use ScopedProxy?
Oczywiście, wyjaśnijmy to dokładnie krok po kroku. Proxy dla beanów o węższym zakresie (scoped proxy) to jeden z kluczowych i niezwykle użytecznych mechanizmów w Spring Framework. Służy do rozwiązania fundamentalnego problemu: wstrzykiwania beana o krótszym cyklu życia do beana o dłuższym cyklu życia.
1. Problem: Niezgodność Zakresów (Scope Mismatch)
Zacznijmy od zrozumienia problemu. W Springu każdy bean ma określony zakres (scope), który definiuje jego cykl życia. Najpopularniejsze zakresy to:
singleton(domyślny): Spring tworzy tylko jedną instancję tego beana w całym kontenerze aplikacji. Ta instancja jest tworzona przy starcie aplikacji i niszczona przy jej zamknięciu.prototype: Nowa instancja jest tworzona za każdym razem, gdy bean jest wstrzykiwany lub pobierany z kontenera.request: Nowa instancja jest tworzona dla każdego pojedynczego żądania HTTP. Istnieje tylko w trakcie tego żądania.session: Nowa instancja jest tworzona dla każdej sesji HTTP użytkownika. Istnieje tak długo, jak trwa sesja.application: Jedna instancja na cały cykl życiaServletContext(podobne do singletona, ale w kontekście aplikacji webowej).
Problem pojawia się, gdy próbujesz wstrzyknąć bean o węższym zakresie do beana o szerszym zakresie.
Przykład: Chcesz wstrzyknąć bean o zakresie request (np. DaneUzytkownika) do beana o zakresie singleton (np. KontrolerZamowien).
// Bean o zakresie 'request' - powinien być unikalny dla każdego żądania HTTP
@Component
@Scope("request")
public class DaneUzytkownika {
private String ipAddress;
// ... gettery, settery
}
// Bean o zakresie 'singleton' (domyślnie)
@RestController
public class KontrolerZamowien {
private final DaneUzytkownika daneUzytkownika;
@Autowired
public KontrolerZamowien(DaneUzytkownika daneUzytkownika) {
this.daneUzytkownika = daneUzytkownika;
}
// ... metody kontrolera
}
Dlaczego to nie zadziała (bez proxy)?
KontrolerZamowienjest singletonem, więc Spring tworzy jego instancję tylko raz, podczas uruchamiania aplikacji.- W momencie tworzenia
KontrolerZamowien, Spring musi wstrzyknąć zależność -DaneUzytkownika. - Ale
DaneUzytkownikama zakresrequest. W momencie startu aplikacji nie istnieje żadne żądanie HTTP, więc Spring nie wie, którą instancjęDaneUzytkownikama stworzyć i wstrzyknąć.
W efekcie Spring rzuci wyjątkiem, np. BeanCreationException, informując o niemożności zaspokojenia zależności z powodu niezgodności zakresów. Singleton nie może przechowywać bezpośredniej referencji do beana, który jeszcze nie istnieje i będzie tworzony wielokrotnie w przyszłości.
2. Rozwiązanie: Scoped Proxy
Scoped proxy to "pośrednik" lub "pełnomocnik", który rozwiązuje ten problem w elegancki sposób. Zamiast wstrzykiwać prawdziwy bean o węższym zakresie, Spring wstrzykuje specjalny obiekt proxy.
Jak to działa?
- Do singletona (
KontrolerZamowien) nie jest wstrzykiwana prawdziwa instancjaDaneUzytkownika. - Zamiast tego, Spring tworzy i wstrzykuje obiekt proxy, który wygląda i zachowuje się jak
DaneUzytkownika(implementuje ten sam interfejs lub dziedziczy po tej klasie). - Ten obiekt proxy jest lekki i może bezpiecznie istnieć w singletonie przez cały cykl życia aplikacji.
- Gdy metoda na obiekcie proxy jest wywoływana (np.
daneUzytkownika.getIpAddress()), proxy nie wykonuje logiki samo w sobie. - Zamiast tego, deleguje wywołanie:
- Sprawdza aktualny kontekst (np. bieżące żądanie HTTP).
- Pobiera z kontenera Springa prawdziwą instancję beana (
DaneUzytkownika) przypisaną do tego kontekstu (do tego żądania). - Wywołuje żądaną metodę na tej prawdziwej instancji i zwraca wynik.
Dzięki temu singleton (KontrolerZamowien) może przez cały czas trzymać referencję do jednego, niezmiennego obiektu (proxy), a proxy za każdym razem "załatwia" dostęp do właściwego, aktualnego beana z węższego zakresu.
3. Jak skonfigurować Scoped Proxy?
Konfiguracja jest bardzo prosta i sprowadza się do dodania jednego atrybutu w adnotacji @Scope.
Metoda z adnotacjami (zalecana)
Używamy atrybutu proxyMode w adnotacji @Scope.
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
@Component
// 1. Definiujemy zakres jako 'request'
// 2. Włączamy tworzenie proxy
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class DaneUzytkownika {
// ...
}
Teraz bean KontrolerZamowien (z pierwszego przykładu) zadziała poprawnie.
Dostępne tryby proxyMode:
ScopedProxyMode.DEFAULT: Domyślny tryb. Spring sam próbuje wybrać najlepszą strategię (zazwyczajTARGET_CLASS).ScopedProxyMode.NO: (Wartość domyślna, jeśliproxyModenie jest podany) - proxy nie jest tworzone.ScopedProxyMode.INTERFACES: Spring tworzy proxy dynamiczne oparte na interfejsach Javy (JDK dynamic proxy). Wymaga to, aby bean implementował co najmniej jeden interfejs. Proxy będzie implementowało te same interfejsy co bean, ale nie będzie jego podklasą.ScopedProxyMode.TARGET_CLASS: Spring tworzy proxy za pomocą biblioteki CGLIB. Proxy jest tworzone przez rozszerzenie klasy beana (tworzenie podklasy w locie). Nie wymaga to implementacji interfejsów. Jest to najczęściej używana i najbardziej elastyczna opcja.
Kiedy używać INTERFACES, a kiedy TARGET_CLASS?
- Jeśli Twój bean implementuje interfejs i chcesz, aby zależności opierały się tylko na tym interfejsie, możesz użyć
INTERFACES. - W praktyce,
TARGET_CLASSjest niemal zawsze lepszym i bezpieczniejszym wyborem, ponieważ działa niezależnie od tego, czy bean implementuje interfejsy, czy nie.
Metoda z XML (w starszych projektach)
W konfiguracji opartej na XML, używa się tagu <aop:scoped-proxy/>.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="daneUzytkownika" class="com.example.DaneUzytkownika" scope="request">
<!-- Ta linia tworzy proxy. Domyślnie użyje CGLIB (TARGET_CLASS) jeśli to możliwe -->
<aop:scoped-proxy/>
</bean>
<bean id="kontrolerZamowien" class="com.example.KontrolerZamowien">
<constructor-arg ref="daneUzytkownika"/>
</bean>
</beans>
Można też jawnie określić tryb: <aop:scoped-proxy proxy-target-class="false"/> dla interfejsów.
4. Praktyczny Przykład: Koszyk Zakupowy
Klasyczny przykład to koszyk zakupowy (ShoppingCart), który powinien być unikalny dla każdej sesji użytkownika (session scope). Chcemy wstrzyknąć go do kontrolera produktów, który jest singletonem.
// Koszyk - unikalny dla każdej sesji użytkownika
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {
private final List<String> items = new ArrayList<>();
public void addItem(String item) {
items.add(item);
}
public List<String> getItems() {
return items;
}
}
// Kontroler - singleton, żyje przez cały czas działania aplikacji
@RestController
@RequestMapping("/products")
public class ProductController {
private final ShoppingCart shoppingCart;
@Autowired
public ProductController(ShoppingCart shoppingCart) {
// Wstrzykiwane jest tutaj proxy do ShoppingCart, a nie prawdziwy obiekt!
this.shoppingCart = shoppingCart;
System.out.println("Typ wstrzykniętego obiektu: " + shoppingCart.getClass().getName());
// Wydrukuje coś w stylu: com.example.ShoppingCart$$EnhancerBySpringCGLIB$$...
}
@GetMapping("/add")
public String addProduct(@RequestParam String item) {
// Wywołanie metody na proxy
shoppingCart.addItem(item);
// Proxy znajdzie prawdziwy koszyk dla bieżącej sesji i na nim wykona metodę
return "Dodano: " + item;
}
@GetMapping("/cart")
public List<String> viewCart() {
// Ponownie, proxy deleguje wywołanie do koszyka z aktualnej sesji
return shoppingCart.getItems();
}
}
Jak to zadziała dla dwóch różnych użytkowników?
- Użytkownik A wchodzi na stronę. Spring tworzy dla niego nową sesję i nową instancję
ShoppingCart(Koszyk A). - Użytkownik B wchodzi na stronę. Spring tworzy dla niego nową sesję i nową instancję
ShoppingCart(Koszyk B). - Obaj użytkownicy wysyłają żądania do tego samego, jednego singletona
ProductController. - Gdy Użytkownik A dodaje produkt, jego kontroler wywołuje metodę na proxy. Proxy lokalizuje Koszyk A w sesji A i dodaje produkt.
- Gdy Użytkownik B dodaje produkt, jego kontroler wywołuje metodę na tym samym proxy. Proxy lokalizuje Koszyk B w sesji B i dodaje produkt.
Koszyki obu użytkowników są od siebie w pełni odizolowane, mimo że oba są zarządzane przez ten sam singletonowy kontroler.
Podsumowanie Kluczowych Punktów
- Cel: Umożliwienie wstrzykiwania beanów o krótszym cyklu życia (np.
request,session) do beanów o dłuższym cyklu życia (np.singleton). - Jak: Spring wstrzykuje specjalny obiekt proxy zamiast prawdziwego beana.
- Działanie proxy: Przechwytuje wywołania metod i deleguje je do właściwej, aktualnej instancji beana, którą wyszukuje w odpowiednim zakresie (np. w bieżącym żądaniu lub sesji).
- Konfiguracja:
proxyMode = ScopedProxyMode.TARGET_CLASS(lubINTERFACES) w adnotacji@Scope. - Najczęstszy wybór:
ScopedProxyMode.TARGET_CLASS(używa CGLIB) jest najbardziej uniwersalny, ponieważ nie wymaga, aby bean implementował interfejsy.