Proxy pattern w Spring AOP – Jak działa magia aspektów

TL;DR: Spring AOP używa wzorca Proxy do „owijania” obiektów i dodawania dodatkowej funkcjonalności (logowanie, security, transakcje) bez modyfikacji oryginalnego kodu. Spring automatycznie wybiera między JDK Dynamic Proxy (dla interfejsów) a CGLIB Proxy (dla klas).

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ć
Wymagania wstępne: Podstawowa znajomość Spring Framework, dependency injection, oraz programowania obiektowego w Java.

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.

Spring AOP Proxy – obiekt wygenerowany automatycznie przez Spring, który „opakowuje” Twoje beany i wykonuje dodatkową logikę (advice) w określonych punktach (pointcuts).

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);
    }
}
JDK Dynamic Proxy działa tylko z interfejsami. Spring tworzy proxy implementujące ten sam interfejs co oryginalny bean.

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
    }
}
CGLIB tworzy podklasę oryginalnej klasy i nadpisuje jej metody, dodając funkcjonalność AOP.
AspektJDK Dynamic ProxyCGLIB Proxy
WymaganiaKlasa musi implementować interfejsDziała z każdą klasą
WydajnośćSzybsze tworzenieSzybsze wykonanie
OgraniczeniaTylko publiczne metody z interfejsuNie działa z final/private metodami
DependencyWbudowane w JDKWymaga 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
}
Pro tip: Od Spring 5.0 domyślnie używany jest CGLIB proxy nawet dla interfejsów, chyba że wyraźnie ustawisz proxyTargetClass=false.

**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;
        }
    }
}
Uwaga: Proxy działa tylko dla wywołań przez Spring Context. Wywołania wewnętrzne (metoda A wywołuje metodę B w tej samej klasie) nie przejdą przez proxy!

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

Pułapka: Self-invocation problem – wywołania wewnętrzne nie przechodzą przez proxy.
@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!
    }
}
Typowy błąd: Próba użycia @Async lub @Transactional na private/final metodach – proxy nie może ich nadpisać.

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

Pro tip: W aplikacjach high-performance rozważ używanie AspectJ compile-time weaving zamiast Spring AOP dla krytycznych ścieżek.
Czy mogę wyłączyć tworzenie proxy dla konkretnego beana?

Tak, użyj adnotacji @Scope(proxyMode = ScopedProxyMode.NO) lub skonfiguruj bean ręcznie bez aspektów.

Dlaczego moja @Transactional metoda nie działa?

Prawdopodobnie wywołujesz ją wewnątrz tej samej klasy. Wywołania wewnętrzne nie przechodzą przez proxy, więc aspecty nie działają.

Jak sprawdzić jaki typ proxy używa mój bean?

Użyj AopUtils.isJdkDynamicProxy() i AopUtils.isCglibProxy() do sprawdzenia typu proxy.

Czy proxy wpływa na wydajność aplikacji?

Tak, ale minimalnie (1-5% overhead). W większości aplikacji jest to pomijalny koszt w porównaniu z korzyściami z AOP.

Mogę mieszać JDK i CGLIB proxy w jednej aplikacji?

Tak, Spring automatycznie wybiera odpowiedni typ dla każdego beana zgodnie z jego charakterystyką.

Co się stanie jeśli klasa ma final metody?

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!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry