1. @Aspect i włączanie AOP
Jeśli po raz pierwszy piszesz aspekt, prawie na pewno wydarzy się komedia: dodajesz @Aspect, uruchamiasz aplikację, czekasz na piękne logi… i dostajesz ciszę. Nie dlatego, że Spring cię nie szanuje. Po prostu aspekt — to nie magiczne zaklęcie, lecz zwykły bean, który potrzebuje infrastruktury: kontener musi nauczyć się tworzyć proxy, znajdować aspekty i owijać pasujące beany. Bez jawnego włączenia mechaniki AOP aspekt będzie po prostu „kolejną klasą", której nikt nie używa.
Wyobraź sobie, że postawiłeś w pokoju czujnik dymu (klasa z @Aspect), ale zapomniałeś podłączyć go do prądu (@EnableAspectJAutoProxy). Pudełko wisi ładnie, ale pożar ma to w nosie.
Tu ważna jedna myśl: żeby aspekt naprawdę zaczął owijać wywołania, potrzebujemy włączyć proxy-based AOP w czystym Spring-kontekście i napisać around-advice, który poprawnie wywołuje docelową metodę przez proceed().
Zależności dla Spring AOP
Przed pisaniem kodu warto zrobić szybką „weryfikację rzeczywistości": czy projekt ma potrzebne zależności. W ContextFlow powinny być już podłączone, ale nowicjuszowi bardzo przydatne jest zobaczenie, że AOP — to nie „wbudowane w Javę", lecz osobna część stosu.
Potrzebujemy dwóch rzeczy. Po pierwsze, moduł Spring, który umie budować proxy AOP (spring-aop). Po drugie, biblioteka AspectJ, z której brane są klasy takie jak ProceedingJoinPoint i adnotacje @Aspect (aspectjweaver w naszym baseline). Nie robimy compile-time weaving i nie zamieniamy projektu w „kurs AspectJ", ale biblioteka jest potrzebna jako część runtime.
Minimalny kawałek build.gradle.kts (pokazuję tylko ważne, reszta już u ciebie jest):
dependencies {
// Bazowy kontener Spring: konfiguracja, skanowanie komponentów, cykl życia beanów
implementation("org.springframework:spring-context")
// Spring AOP: umie budować proxy i stosować advice według pointcutów
implementation("org.springframework:spring-aop")
// AspectJ runtime: adnotacje i API (ProceedingJoinPoint itp.) dla @Aspect
implementation("org.aspectj:aspectjweaver")
}
Jeśli widzisz u siebie w projekcie właśnie to (lub to samo, ale przez wyrównywanie BOM) — świetnie. Jeśli czegoś brakuje, symptom będzie albo kompilacyjny (klasy org.aspectj.* nie są znajdowane), albo „aspekt jest, ale nie działa" (infrastruktura AOP nie jest aktywowana).
3. Włączanie AOP: @EnableAspectJAutoProxy
Teraz robimy główny krok: mówimy kontenerowi Spring „można tworzyć proxy AOP i stosować aspekty". Do tego służy adnotacja @EnableAspectJAutoProxy. Nie robi magii bezpośrednio; włącza infrastrukturę, która podczas startu kontenera (przez znany ci model BeanPostProcessor) sprawdzi beany i w razie potrzeby zastąpi je obiektami proxy.
Czyli nie dodajemy „kolejnej biblioteki logowania". Włączamy mechanizm, który będzie wbudowywał się w tworzenie beanów. To ważne: AOP w Springu — to część cyklu życia kontenera, a nie static-hack w twoim serwisie.
W wersji edukacyjnej możemy włączyć AOP bezpośrednio w naszym głównym konfigu aplikacji:
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
// Ważne: skanujemy pakiet główny, żeby znalazły się i serwisy, i aspekty
@ComponentScan("com.example.contextflow")
// Włączamy infrastrukturę Spring AOP (automatyczne tworzenie proxy dla pasujących beanów)
@EnableAspectJAutoProxy
public class AppConfig {
// Konfig może być pusty — ważne są adnotacje i włączenie infrastruktury
}
Zwróć uwagę na jedną „metodyczną sztuczkę": @ComponentScan("com.example.contextflow") pokrywa zarówno application.service, jak i support.aop. To znaczy, że aspekt jako klasa zostanie znaleziony przez kontener. Jeśli przypadkowo ograniczysz skanowanie tylko do application, aspekt po prostu się nie zarejestruje, i dalej możesz trzykrotnie włączać AOP — nie będzie czego stosować.
Jeśli twój projekt już urósł do modułowej konfiguracji (co jest normalne dla ContextFlow po lekcjach o @Import), praktyczniej jest trzymać włączenie AOP w osobnym module, np. AopConfig, i importować go w głównym konfigu. Ale zasada ta sama: gdzieś w twojej konfiguracji Java musi być @EnableAspectJAutoProxy.
Oto bardzo uproszczony schemat tego, co właśnie włączyliśmy:
flowchart TD
A[ApplicationContext start] --> B[Tworzenie infrastructure beans]
B --> C["@EnableAspectJAutoProxy rejestruje AutoProxyCreator (BPP)"]
C --> D[Kontener tworzy twoje beany]
D --> E[AutoProxyCreator decyduje: proxy potrzebne czy nie]
E --> F[Bean w kontekście staje się proxy]
F --> G[Wywołania idą przez advice -> target]
Niech ten schemat nie przeraża: nie trzeba pamiętać nazw wewnętrznych klas. Ważne jest rozumienie związku przyczynowo-skutkowego: AOP = infrastruktura kontenera + proxy + przechwytywanie wywołania metody.
4. Pierwszy aspekt @Around i proceed()
Najkrótsza droga do odczucia korzyści AOP — zrobić aspekt, który mierzy czas wykonania metod warstwy serwisowej. Widzieliśmy już „ręczny" wariant w poprzednim wykładzie: start = nanoTime(), try/finally, wydruk. Teraz wyniesiemy to do jednej klasy i podłączymy kontenerem do wszystkich pasujących metod.
Stwórzmy aspekt ServiceTimingAspect (zgodnie ze strukturą projektu — logicznie w com.example.contextflow.support.aop):
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect // Mówimy: to aspekt, wewnątrz są metody advice
@Component // Rejestrujemy aspekt jako Spring bean (inaczej kontener go nie zobaczy)
public class ServiceTimingAspect {
// Pointcut: wszystkie metody wszystkich klas w pakiecie application.service i jego podpakietach
@Around("execution(* com.example.contextflow.application.service..*.*(..))")
public Object timing(ProceedingJoinPoint pjp) throws Throwable {
// Zaznaczamy czas PRZED wywołaniem metody biznesowej
long start = System.nanoTime();
try {
// WAŻNE: proceed() naprawdę uruchamia docelową metodę
return pjp.proceed();
} finally {
// finally gwarantuje log nawet przy wyjątku w metodzie biznesowej
long tookNs = System.nanoTime() - start;
// Sygnatura metody pomaga zrozumieć, co dokładnie zostało wywołane
System.out.println(pjp.getSignature().toShortString()
+ " took " + tookNs + " ns"); // generateDailyReport(..) took 152300 ns
}
}
}
Rozłóżmy ten kod jak dorośli ludzie, którzy nie chcą uczyć się „adnotacji na pamięć", lecz chcą rozumieć, co się dzieje.
@Aspect mówi infrastrukturze Spring: „to aspekt, są w nim metody advice". Ale samo @Aspect nie włącza AOP — to tylko znacznik (dlatego potrzebowaliśmy @EnableAspectJAutoProxy).
@Component czyni aspekt zwykłym Spring beanem. Bez tego kontener go nie zobaczy (jeśli nie zarejestrujesz aspektu przez @Bean ręcznie).
@Around(...) — to around-advice. Jego sens polega na tym, że dosłownie owija wywołanie docelowej metody. Przed proceed() — „przed wywołaniem", po proceed() — „po wywołaniu".
ProceedingJoinPoint — to obiekt, przez który advice uzyskuje dostęp do „bieżącego wywołania". I tak, proceed() — to ten przycisk „kontynuuj wykonanie". Jeśli go nie naciśniesz, docelowa metoda w ogóle się nie wykona. To typowy błąd nowicjusza: napisać aspekt, który „wszystko loguje", i przypadkowo zamienić aplikację w teatr cieni bez prawdziwego działania.
Osobno zaznaczę System.nanoTime(). Do pomiaru czasu trwania jest to lepsze niż currentTimeMillis(), bo nanoTime() jest monotonicznie rosnące i nie zależy od „przestawili zegar na komputerze" ani „synchronizacja czasu". Ważna jest nam różnica, a nie bieżąca data.
W tej konstrukcji zebrana jest już cała podstawa scenariusza AOP: serwis zarządzany przez kontener, włączony @EnableAspectJAutoProxy i jeden ServiceTimingAspect, który owija warstwę serwisową przez @Around. Zmieniać się teraz mogą tylko dwie rzeczy: jak szeroko pointcut wybiera metody i jaką ścieżką wywołanie dociera do obiektu target.
5. Weryfikacja działania przez proxy
AOP — to temat, gdzie bardzo łatwo się oszukać. Dlatego sprawdzamy nie „tak mi się wydaje", lecz „widzę w wydruku". Dla prostoty weźmy dowolny serwisowy bean z application.service. Niech to będzie ReportingService z krótką metodą, żeby wydruk był oczywisty.
Mini-przykład serwisu:
import org.springframework.stereotype.Service;
@Service // Ta klasa musi być Spring beanem, żeby można było ją proksować
public class ReportingService {
public void generateDailyReport() {
// To komunikat „kontrolny": pomaga zrozumieć, że sama metoda naprawdę się wykonała
System.out.println("Generating report..."); // Generating report...
}
}
A teraz prawidłowe uruchomienie — właśnie przez ApplicationContext, żeby kontener mógł podstawić proxy:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApp {
public static void main(String[] args) {
// Kontekst zarządza tworzeniem beanów i właśnie tu włącza się proxy AOP
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Pobieramy bean z kontekstu (najprawdopodobniej to proxy, a nie „czysty" ReportingService)
ReportingService service = context.getBean(ReportingService.class);
// Wywołanie pójdzie przez proxy -> advice -> metoda target
service.generateDailyReport();
}
}
}
W konsoli powinieneś zobaczyć dwie wiadomości. Pierwsza — z samego serwisu, druga — od aspektu:
Generating report...
generateDailyReport(..) took 152300 ns
Jeśli widzisz tylko Generating report..., to AOP się nie włączył. Najczęstsze przyczyny omówimy na końcu wykładu, ale logika jest taka: albo nie włączono @EnableAspectJAutoProxy, albo aspekt się nie zarejestrował jako bean, albo pointcut nie pasował, albo wywołałeś metodę nie przez bean z kontekstu.
I tu warto zrobić „anty-przykład", żeby mózg utrwalił regułę. Jeśli stworzysz obiekt ręcznie przez new, aspekt nie zadziała, bo Spring w ogóle nie uczestniczy w tworzeniu i nie może podłożyć proxy:
public class ManualRunDemo {
public static void main(String[] args) {
// Stworzyli obiekt ręcznie — kontener Spring nie uczestniczy, proxy nie pojawi się
ReportingService service = new ReportingService();
// Wywołanie idzie bezpośrednio, advice nigdy nie zadziała
service.generateDailyReport(); // Generating report...
}
}
W tym przykładzie wszystko uczciwe: sam stworzyłeś obiekt, sam go wywołałeś, Spring stoi z boku i smutno patrzy. AOP — to historia o beanach zarządzanych przez kontener, a nie o dowolnych obiektach JVM.
Dane z ProceedingJoinPoint
Gdy around-advice zadziałał, pojawia się naturalny impuls: „O, teraz widzę wszystko! Teraz wydrukuję argumenty, wyniki, całą Wszechświat!" Tu warto się zatrzymać i przypomnieć, że robimy edukacyjny aspekt do timingu, a nie system totalnej inwigilacji kolegów (chociaż… żart).
ProceedingJoinPoint daje trochę przydatnych danych, i wystarczy ich, żeby wydruk był czytelniejszy. Na przykład zamiast krótkiego generateDailyReport(..) można wydrukować bardziej „architektoniczną" nazwę:
// Pełna sygnatura przydaje się, gdy w logach jest wiele metod o jednakowych nazwach
String method = pjp.getSignature().toLongString();
System.out.println(method);
// public void com.example.contextflow.application.service.ReportingService.generateDailyReport()
Albo można wydrukować nazwę klasy obiektu target (czasem jest to interesujące, bo pjp.getThis() i pjp.getTarget() w świecie proxy mogą się różnić):
// getTarget() — „prawdziwy" obiekt, który jest owijany przez proxy
Object target = pjp.getTarget();
System.out.println("Target class = " + target.getClass().getName());
// Target class = com.example.contextflow.application.service.ReportingService
A z argumentami (pjp.getArgs()) lepiej być ostrożnym. W projekcie edukacyjnym jest to bezpieczne, ale w prawdziwej aplikacji bardzo łatwo przypadkowo zalogować coś nadmiarowego. Na poziomie kursu wystarczy wiedzieć, że taka możliwość istnieje, i pamiętać regułę: techniczny log nie powinien zamieniać się w wyciek danych.
6. Gdzie aspekty mają mieszkać w ContextFlow
Teraz najważniejsze praktycznie: jak wbudować ten mechanizm tak, żeby projekt nie zamienił się w „przypadkowy zestaw aspektów", lecz pozostał czytelny. W ContextFlow mamy dobre miejsce na takie rzeczy — pakiet support.aop. Serwisy mieszkają w application.service, modele domenowe w domain.model. I to nie tylko piękno: tak fizycznie oddzielasz „mechanikę techniczną" od „sensu biznesowego".
Aspekt ServiceTimingAspect — to czysta infrastruktura. Nie powinien wiedzieć o Order, Customer ani „zniżce dla lojalnych klientów". Jest mu wszystko jedno, co robi serwis — mierzy czas i pisze techniczną diagnostykę. Jeśli nagle zechciało ci się napisać w środku aspektu „jeśli zamówienie anulowane, to…" — to sygnał, że mieszasz logikę domenową z techniczną. W tym momencie lepiej odłożyć klawiaturę i cicho zapytać siebie: „Czy na pewno nie próbuję wnieść scenariusza biznesowego do aspektu z lenistwa?"
Technicznie do podłączenia w istniejącym projekcie zazwyczaj wystarczają dwie czynności: dodać @EnableAspectJAutoProxy do głównego konfiga (lub zaimportować moduł konfiguracji AOP) i upewnić się, że support.aop trafia do @ComponentScan. Jeśli pakiet główny aplikacji — com.example.contextflow, skanowanie od niego złapie wszystko, czego trzeba.
W efekcie warstwa serwisowa pozostaje czysta: w metodach nie ma nanoTime(), nie ma try/finally, nie ma powtarzających się linii. A przy tym dostajesz obserwowalność zachowania aplikacji, co jest szczególnie miłe, gdy serwisów przybywa, a scenariusze są dłuższe.
7. Typowe błędy przy pracy ze Spring AOP
W tym temacie błędy zazwyczaj nie są „subtelne", lecz bardzo bezpośrednie: albo zapomniałeś włączyć infrastrukturę, albo napisałeś advice tak, że nie wywołuje docelowej metody, albo w ogóle nie trafiłeś w pointcut. Dobra wiadomość: wszystko to leczy się nie szamanizmem, lecz sprawdzeniem kilku konkretnych miejsc.
Błąd nr 1: zapomniano o @EnableAspectJAutoProxy.
Symptom wygląda szczególnie podstępnie: aspekt jako klasa istnieje, jako bean może nawet wstać (jeśli jest @Component), aplikacja startuje, ale w wydruku nie ma żadnych śladów advice. Przyczyna prosta: kontener nie dostał „pozwolenia" na tworzenie proxy AOP, i żadnego auto-proxying nie ma.
Błąd nr 2: aspekt nie stał się Spring beanem.
@Aspect — to nie rejestracja. Jeśli usunąłeś @Component i nie dodałeś @Bean w konfiguracji, kontener nie zobaczy aspektu. W efekcie AOP jest włączone, ale nie ma czego stosować. Ważne: aspekt w Springu — to zwykły bean.
Błąd nr 3: w @Around zapomniano wywołać proceed().
To klasyka gatunku. Niby wszystko działa, advice drukuje komunikaty, ale metoda biznesowa się nie wykonuje. W around-advice musisz wywołać pjp.proceed() (lub świadomie zastąpić wykonanie czymś innym — ale to zdecydowanie nie jest nasz edukacyjny scenariusz).
Błąd nr 4: advice nie zwraca wyniku proceed().
Jeśli docelowa metoda zwraca wartość, a ty w advice zwracasz null albo w ogóle nic, łamiesz kontrakt metody. W najlepszym razie dostaniesz NullPointerException w kodzie wywołującym, w najgorszym — ciche błędy logiczne. Domyślnie ucz się: return pjp.proceed(); i dopiero potem komplikuj.
Błąd nr 5: wywołujesz serwis nie z kontekstu, lecz przez new.
AOP działa tylko na beanach zarządzanych przez kontener. Jeśli stworzyłeś obiekt ręcznie, Spring nie podłożył proxy, advice nie zadziała. Symptom zazwyczaj brzmi jako „no dlaczego aspekt nie działa, przecież napisałem adnotacje?!", a odpowiedź nudna: bo adnotacje — to nie magia, lecz metadane dla kontenera.
GO TO FULL VERSION