Dlaczego wzorzec Proxy w Spring AOP jest kluczowy
Wyobraź sobie, że musisz dodać logowanie do każdej metody w 50 klasach serwisowych. Bez Spring AOP oznaczałoby to modyfikację setek metod. Wzorzec Proxy w Spring AOP rozwiązuje ten problem elegancko – „opakowuje” Twoje obiekty w proxy, które automatycznie wykonuje dodatkową logikę przed i po wywołaniu oryginalnych metod.
Co się nauczysz:
- Jak Spring implementuje wzorzec Proxy w AOP
- Różnice między JDK Dynamic Proxy a CGLIB Proxy
- Kiedy Spring wybiera który typ proxy
- Praktyczne przykłady użycia z kodem
- Najczęstsze pułapki i jak ich unikać
Czym jest wzorzec Proxy w kontekście Spring AOP
Wzorzec Proxy to strukturalny wzorzec projektowy, który tworzy „reprezentanta” lub „zastępcę” dla innego obiektu. W Spring AOP proxy działa jak pośrednik między Twoim kodem a rzeczywistym obiektem.
Gdy Spring IoC Container tworzy bean, który ma advice (np. @Transactional, @Cacheable), automatycznie generuje proxy dla tego beana zamiast zwracać oryginalny obiekt.
JDK Dynamic Proxy vs CGLIB Proxy
Spring AOP może używać dwóch mechanizmów tworzenia proxy:
JDK Dynamic Proxy
// Przykład interfejsu public interface UserService { User findById(Long id); void save(User user); } // Implementacja @Service @Transactional public class UserServiceImpl implements UserService { @Override public User findById(Long id) { // logika biznesowa return userRepository.findById(id); } @Override public void save(User user) { // logika biznesowa userRepository.save(user); } }
CGLIB Proxy
// Klasa bez interfejsu @Service @Transactional public class OrderService { public void processOrder(Order order) { // logika biznesowa validateOrder(order); saveOrder(order); sendConfirmation(order); } private void validateOrder(Order order) { // walidacja } }
Aspekt | JDK Dynamic Proxy | CGLIB Proxy |
---|---|---|
Wymagania | Klasa musi implementować interfejs | Działa z każdą klasą |
Wydajność | Szybsze tworzenie | Szybsze wykonanie |
Ograniczenia | Tylko publiczne metody z interfejsu | Nie działa z final/private metodami |
Dependency | Wbudowane w JDK | Wymaga biblioteki CGLIB |
Jak Spring wybiera typ proxy
Spring AOP automatycznie decyduje o typie proxy według następujących reguł:
@Configuration @EnableAspectJAutoProxy(proxyTargetClass = false) // domyślnie false public class AopConfig { // konfiguracja }
**Algorytm wyboru proxy:**
1. Jeśli `proxyTargetClass=true` → zawsze CGLIB
2. Jeśli bean implementuje interfejsy → JDK Dynamic Proxy
3. Jeśli bean nie implementuje interfejsów → CGLIB Proxy
Praktyczny przykład implementacji
Stwórzmy aspekt do logowania wywołań metod:
@Aspect @Component public class LoggingAspect { private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class); @Around("execution(* com.example.service.*.*(..))") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); String className = joinPoint.getTarget().getClass().getSimpleName(); String methodName = joinPoint.getSignature().getName(); logger.info("Executing {}.{}()", className, methodName); try { Object result = joinPoint.proceed(); // wywołanie oryginalnej metody long executionTime = System.currentTimeMillis() - start; logger.info("{}.{}() executed in {} ms", className, methodName, executionTime); return result; } catch (Exception e) { logger.error("Exception in {}.{}(): {}", className, methodName, e.getMessage()); throw e; } } }
Testowanie proxy behavior
@RunWith(SpringRunner.class) @SpringBootTest public class ProxyTest { @Autowired private UserService userService; @Test public void shouldUseProxy() { // Sprawdzenie czy obiekt to proxy boolean isProxy = AopUtils.isAopProxy(userService); assertTrue("UserService should be proxied", isProxy); // Typ proxy boolean isJdkProxy = AopUtils.isJdkDynamicProxy(userService); boolean isCglibProxy = AopUtils.isCglibProxy(userService); System.out.println("JDK Proxy: " + isJdkProxy); System.out.println("CGLIB Proxy: " + isCglibProxy); } }
Najczęstsze pułapki i problemy
@Service public class PaymentService { @Transactional public void processPayment(Payment payment) { validatePayment(payment); // to NIE przejdzie przez proxy! // ... } @Transactional(propagation = Propagation.REQUIRES_NEW) public void validatePayment(Payment payment) { // Ta metoda nie będzie miała nowej transakcji! } }
**Rozwiązanie:**
@Service public class PaymentService { @Autowired private PaymentService self; // injection samego siebie @Transactional public void processPayment(Payment payment) { self.validatePayment(payment); // teraz przejdzie przez proxy! } }
Performance considerations
Proxy wprowadza niewielki overhead:
– **JDK Dynamic Proxy:** ~2-5% narzutu
– **CGLIB Proxy:** ~1-3% narzutu
– **Reflection overhead:** głównie przy pierwszym wywołaniu
Tak, użyj adnotacji @Scope(proxyMode = ScopedProxyMode.NO) lub skonfiguruj bean ręcznie bez aspektów.
Prawdopodobnie wywołujesz ją wewnątrz tej samej klasy. Wywołania wewnętrzne nie przechodzą przez proxy, więc aspecty nie działają.
Użyj AopUtils.isJdkDynamicProxy() i AopUtils.isCglibProxy() do sprawdzenia typu proxy.
Tak, ale minimalnie (1-5% overhead). W większości aplikacji jest to pomijalny koszt w porównaniu z korzyściami z AOP.
Tak, Spring automatycznie wybiera odpowiedni typ dla każdego beana zgodnie z jego charakterystyką.
CGLIB nie może ich nadpisać, więc aspecty nie będą działać na final metodach. JDK proxy nie ma tego problemu jeśli metoda jest w interfejsie.
Przydatne zasoby
🚀 Zadanie dla Ciebie
Stwórz aspekt, który będzie mierzył czas wykonania metod i logował ostrzeżenie jeśli metoda wykonuje się dłużej niż 1 sekunda. Przetestuj go na klasie z interfejsem i bez interfejsu, sprawdzając jaki typ proxy został użyty w każdym przypadku.
Czy znasz inne przypadki użycia wzorca Proxy w Spring? Podziel się swoimi doświadczeniami w komentarzach!