1. @Aspect und Aktivierung von AOP
Wenn Sie zum ersten Mal einen Aspekt schreiben, wird es fast garantiert eine Komödie: Sie fügen @Aspect hinzu, starten die Anwendung, warten auf schöne Logs … und erhalten Stille. Das liegt nicht daran, dass Spring Sie nicht respektiert. Ein Aspekt ist einfach kein Zauberspruch, sondern eine gewöhnliche Bean, die eine Infrastruktur benötigt: Der Container muss lernen, Proxy zu erstellen, Aspekte zu finden und passende Beans zu umhüllen. Ohne explizite Aktivierung der AOP-Mechanik bleibt der Aspekt nur „eine weitere Klasse“, die niemand verwendet.
Stellen Sie sich vor, Sie haben in einem Raum einen Rauchmelder installiert (eine Klasse mit @Aspect), aber vergessen, ihn an die Stromversorgung anzuschließen (@EnableAspectJAutoProxy). Das Kästchen hängt zwar schön, aber ein Brand ist ihm völlig egal.
Hier ist ein wichtiger Gedanke: Damit der Aspekt tatsächlich Aufrufe umhüllt, müssen wir proxy-basiertes AOP in einem reinen Spring-Kontext aktivieren und einen Around-Advice schreiben, der die Zielmethode korrekt über proceed() aufruft.
Abhängigkeiten für Spring AOP
Bevor Sie Code schreiben, ist es nützlich, einen schnellen „Realitätscheck“ durchzuführen: Hat das Projekt die erforderlichen Abhängigkeiten? In ContextFlow sollten sie bereits eingebunden sein, aber für Anfänger ist es sehr hilfreich zu sehen, dass AOP nicht „in Java eingebaut“, sondern ein separater Teil des Stacks ist.
Wir brauchen zwei Dinge. Erstens ein Spring-Modul, das AOP-Proxys erstellen kann (spring-aop). Zweitens die AspectJ-Bibliothek, von der Klassen wie ProceedingJoinPoint und Annotationen wie @Aspect stammen (aspectjweaver in unserem Baseline). Wir machen kein Compile-Time-Weaving und verwandeln das Projekt nicht in einen „AspectJ-Kurs“, aber die Bibliothek wird als Laufzeitkomponente benötigt.
Minimales Stück build.gradle.kts (zeige nur das Wichtige, der Rest ist bereits vorhanden):
dependencies {
// Basis-Spring-Container: Konfiguration, Komponenten-Scan, Lebenszyklus von Beans
implementation("org.springframework:spring-context")
// Spring AOP: kann Proxy erstellen und Advice basierend auf Pointcuts anwenden
implementation("org.springframework:spring-aop")
// AspectJ Runtime: Annotationen und API (ProceedingJoinPoint usw.) für @Aspect
implementation("org.aspectj:aspectjweaver")
}
Wenn Sie genau das in Ihrem Projekt sehen (oder dasselbe, aber mit BOM-Anpassung) – großartig. Wenn etwas fehlt, wird das Symptom entweder kompilatorisch sein (Klassen org.aspectj.* werden nicht gefunden) oder „Aspekt vorhanden, funktioniert aber nicht“ (AOP-Infrastruktur nicht aktiviert).
3. Aktivierung von AOP: @EnableAspectJAutoProxy
Jetzt machen wir den wichtigsten Schritt: Wir sagen dem Spring-Container „AOP-Proxy darf erstellt und Aspekte angewendet werden“. Dafür gibt es die Annotation @EnableAspectJAutoProxy. Sie wirkt nicht direkt Zauber, sondern aktiviert die Infrastruktur, die während des Container-Starts (über das Ihnen bekannte Modell BeanPostProcessor) Beans prüft und bei Bedarf durch Proxy-Objekte ersetzt.
Das heißt, wir fügen keine „weitere Logging-Bibliothek“ hinzu. Wir aktivieren einen Mechanismus, der sich in die Bean-Erstellung einfügt. Das ist wichtig: AOP in Spring ist Teil des Container-Lebenszyklus, kein static-Hack in Ihrem Service.
Im Lehrkontext können wir AOP direkt in unserer obersten Anwendungskonfiguration aktivieren:
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
// Wichtig: Scannen des Root-Pakets, damit sowohl Services als auch Aspekte gefunden werden
@ComponentScan("com.example.contextflow")
// Aktiviert Spring AOP-Infrastruktur (automatische Proxy-Erstellung für passende Beans)
@EnableAspectJAutoProxy
public class AppConfig {
// Konfiguration kann leer sein – wichtig sind die Annotationen und die Aktivierung der Infrastruktur
}
Beachten Sie einen „methodischen Trick“: @ComponentScan("com.example.contextflow") deckt sowohl application.service als auch support.aop ab. Das bedeutet, dass der Aspekt als Klasse vom Container gefunden wird. Wenn Sie das Scannen versehentlich nur auf application beschränken, wird der Aspekt einfach nicht registriert, und Sie können AOP noch dreimal aktivieren – es gibt nichts, worauf es angewendet werden könnte.
Wenn Ihr Projekt bereits zu einer modularen Konfiguration gewachsen ist (was für ContextFlow nach den Tagen zu @Import normal ist), ist es praktischer, die AOP-Aktivierung in einem separaten Modul, z. B. AopConfig, zu halten und in die Hauptkonfiguration zu importieren. Das Prinzip bleibt jedoch dasselbe: Irgendwo in Ihrer Java-Konfiguration muss @EnableAspectJAutoProxy vorhanden sein.
Hier ist ein stark vereinfachtes Diagramm dessen, was wir jetzt aktiviert haben:
flowchart TD
A[ApplicationContext start] --> B[Erstellung von Infrastructure Beans]
B --> C["@EnableAspectJAutoProxy registriert AutoProxyCreator (BPP)"]
C --> D[Container erstellt Ihre Beans]
D --> E[AutoProxyCreator entscheidet: Proxy benötigt oder nicht]
E --> F[Bean im Kontext wird zu Proxy]
F --> G[Aufrufe gehen über Advice -> Target]
Lassen Sie sich von diesem Diagramm nicht erschrecken: Sie müssen sich nicht die Namen der internen Klassen merken. Wichtig ist, den kausalen Zusammenhang zu verstehen: AOP = Container-Infrastruktur + Proxy + Methodenaufruf-Interception.
4. Erster @Around-Aspekt und proceed()
Der kürzeste Weg, den Nutzen von AOP zu spüren, ist ein Aspekt, der die Ausführungszeit von Methoden der Serviceschicht misst. Wir haben bereits die „manuelle“ Variante in der letzten Vorlesung gesehen: start = nanoTime(), try/finally, Ausgabe. Jetzt extrahieren wir dies in eine Klasse und binden es über den Container an alle passenden Methoden.
Erstellen wir den Aspekt ServiceTimingAspect (projektstrukturell logisch in 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 // Sagt: Dies ist ein Aspekt, enth√§lt Advice-Methoden
@Component // Registriert den Aspekt als Spring-Bean (sonst sieht der Container ihn nicht)
public class ServiceTimingAspect {
// Pointcut: Alle Methoden aller Klassen im Paket application.service und dessen Unterpaketen
@Around("execution(* com.example.contextflow.application.service..*.*(..))")
public Object timing(ProceedingJoinPoint pjp) throws Throwable {
// Zeit VOR dem Aufruf der Geschäftsmethode messen
long start = System.nanoTime();
try {
// WICHTIG: proceed() startet tats√§chlich die Zielmethode
return pjp.proceed();
} finally {
// finally garantiert Log sogar bei Exception in der Geschäftsmethode
long tookNs = System.nanoTime() - start;
// Die Methodensignatur hilft zu verstehen, was genau aufgerufen wurde
System.out.println(pjp.getSignature().toShortString()
+ " took " + tookNs + " ns"); // generateDailyReport(..) took 152300 ns
}
}
}
Lassen Sie uns diesen Code wie Erwachsene analysieren, die nicht „Annotationen auswendig lernen“ wollen, sondern verstehen möchten, was passiert.
@Aspect sagt der Spring-Infrastruktur: „Dies ist ein Aspekt, er enthält Advice-Methoden“. Aber @Aspect allein aktiviert kein AOP – es ist nur ein Marker (deshalb brauchten wir @EnableAspectJAutoProxy).
@Component macht den Aspekt zu einer gewöhnlichen Spring-Bean. Ohne dies sieht der Container ihn nicht (es sei denn, Sie registrieren den Aspekt manuell über @Bean).
@Around(...) ist ein Around-Advice. Sein Sinn ist, dass er buchstäblich den Aufruf der Zielmethode umhüllt. Vor proceed() – „vor dem Aufruf“, nach proceed() – „nach dem Aufruf“.
ProceedingJoinPoint ist das Objekt, über das der Advice Zugriff auf den „aktuellen Aufruf“ erhält. Und ja, proceed() ist der berühmte Knopf „Ausführung fortsetzen“. Wenn man ihn nicht drückt, wird die Zielmethode überhaupt nicht ausgeführt. Das ist ein typischer Anfängerfehler: einen Aspekt zu schreiben, der „alles loggt“, und versehentlich die Anwendung in ein Schattentheater ohne reale Aktion zu verwandeln.
Abschließend möchte ich System.nanoTime() erwähnen. Für die Messung der Dauer ist dies korrekter als currentTimeMillis(), da nanoTime() monoton ist und nicht von „Uhr umgestellt“ oder „Zeitsynchronisation“ abhängt. Uns interessiert nicht das aktuelle Datum, sondern die Differenz.
In dieser Konstruktion ist bereits die gesamte Grundlage des AOP-Szenarios enthalten: container-verwalteter Service, aktiviertes @EnableAspectJAutoProxy und ein ServiceTimingAspect, der die Serviceschicht über @Around umhüllt. Ändern können sich jetzt nur zwei Dinge: Wie breit der Pointcut Methoden auswählt und auf welchem Weg der Aufruf zum Target-Objekt gelangt.
5. Überprüfung der Funktionsweise über Proxy
AOP ist ein Thema, bei dem man sich sehr leicht selbst täuschen kann. Daher prüfen wir nicht „ich denke so“, sondern „ich sehe es in der Ausgabe“. Der Einfachheit halber nehmen wir eine beliebige Service-Bean aus application.service. Nehmen wir ReportingService mit einer kurzen Methode, damit die Ausgabe offensichtlich ist.
Mini-Beispiel des Services:
import org.springframework.stereotype.Service;
@Service // Diese Klasse muss eine Spring-Bean sein, damit sie proxied werden kann
public class ReportingService {
public void generateDailyReport() {
// Diese Nachricht ist eine "Kontrollmeldung": Sie hilft zu verstehen, dass die Methode tatsächlich ausgeführt wurde
System.out.println("Generating report..."); // Generating report...
}
}
Und nun der korrekte Start – genau über ApplicationContext, damit der Container den Proxy einsetzen kann:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApp {
public static void main(String[] args) {
// Der Container steuert die Bean-Erstellung und genau hier wird AOP-Proxying aktiviert
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Bean aus dem Kontext holen (höchstwahrscheinlich ein Proxy, kein "reiner" ReportingService)
ReportingService service = context.getBean(ReportingService.class);
// Der Aufruf erfolgt über Proxy -> Advice -> Target-Methode
service.generateDailyReport();
}
}
}
In der Konsole sollten Sie zwei Nachrichten sehen. Die erste – aus dem Service selbst, die zweite – vom Aspekt:
Generating report...
generateDailyReport(..) took 152300 ns
Wenn Sie nur Generating report... sehen, ist AOP nicht aktiviert. Die häufigsten Ursachen werden wir am Ende der Vorlesung besprechen, aber die Logik ist: Entweder wurde @EnableAspectJAutoProxy nicht gesetzt, oder der Aspekt wurde nicht als Bean registriert, oder der Pointcut hat nicht gepasst, oder Sie haben die Methode nicht über eine Bean aus dem Kontext aufgerufen.
Und hier ist es nützlich, ein „Gegenbeispiel“ zu machen, damit das Gehirn die Regel verinnerlicht. Wenn Sie ein Objekt manuell mit new erstellen, funktioniert der Aspekt nicht, weil Spring gar nicht an der Erstellung beteiligt ist und keinen Proxy unterjubeln kann:
public class ManualRunDemo {
public static void main(String[] args) {
// Objekt manuell erstellt – Spring-Container nicht beteiligt, kein Proxy wird erstellt
ReportingService service = new ReportingService();
// Aufruf erfolgt direkt, Advice wird niemals ausgeführt
service.generateDailyReport(); // Generating report...
}
}
In diesem Beispiel ist alles fair: Sie haben das Objekt selbst erstellt, Sie selbst aufgerufen, Spring steht daneben und schaut traurig zu. AOP ist eine Geschichte über container-managed Beans, nicht über beliebige JVM-Objekte.
Daten aus ProceedingJoinPoint
Wenn der Around-Advice funktioniert, entsteht die natürliche Versuchung: „Oh, ich sehe jetzt alles! Ich werde Argumente, Ergebnisse, das ganze Universum ausdrucken!“ Hier sollte man innehalten und sich daran erinnern, dass wir einen Lehr-Aspekt für Timing machen, kein System der totalen Überwachung von Kollegen (obwohl ... Scherz).
ProceedingJoinPoint liefert einige nützliche Daten, die ausreichen, um die Ausgabe verständlicher zu machen. Anstelle des kurzen generateDailyReport(..) kann man einen „architektonischeren“ Namen ausgeben:
// Vollständige Signatur nützlich, wenn viele Methoden mit gleichen Namen in Logs
String method = pjp.getSignature().toLongString();
System.out.println(method);
// public void com.example.contextflow.application.service.ReportingService.generateDailyReport()
Oder man kann den Klassennamen des Target-Objekts ausgeben (manchmal interessant, weil pjp.getThis() und pjp.getTarget() in der Proxy-Welt unterschiedlich sein können):
// getTarget() – das "echte" Objekt, das vom Proxy umh√ºllt wird
Object target = pjp.getTarget();
System.out.println("Target class = " + target.getClass().getName());
// Target class = com.example.contextflow.application.service.ReportingService
Bei Argumenten (pjp.getArgs()) ist jedoch Vorsicht geboten. In einem Studienprojekt ist das sicher, aber in einer realen Anwendung kann man leicht versehentlich etwas Überflüssiges protokollieren. Auf Kursniveau reicht es zu wissen, dass diese Möglichkeit besteht, und die Regel zu beachten: Ein technisches Log sollte nicht zu einem Datenleck werden.
6. Wo Aspekte in ContextFlow leben
Nun zum Praktischsten: Wie integriert man diesen Mechanismus so, dass das Projekt nicht zu einer „zufälligen Sammlung von Aspekten“ wird, sondern lesbar bleibt. In ContextFlow haben wir einen guten Platz für solche Dinge – das Paket support.aop. Services leben in application.service, Domain-Modelle in domain.model. Und das ist nicht nur schön: So trennen Sie physisch die „technische Mechanik“ vom „Geschäftssinn“.
Der Aspekt ServiceTimingAspect ist reine Infrastruktur. Er sollte nichts über Order, Customer oder „Rabatt für treue Kunden“ wissen. Ihm ist egal, was genau der Service tut – er misst die Zeit und schreibt technische Diagnosen. Wenn Sie plötzlich innerhalb des Aspekts schreiben möchten „wenn die Bestellung storniert wurde, dann …“ – das ist ein Signal, dass Sie Domänenlogik mit technischer vermischt haben. An diesem Punkt ist es besser, die Tastatur wegzulegen und sich leise zu fragen: „Versuche ich nicht aus Faulheit ein Geschäftsszenario in den Aspekt zu stopfen?“
Technisch reichen für die Einbindung in ein bestehendes Projekt normalerweise zwei Aktionen: Fügen Sie @EnableAspectJAutoProxy zur Hauptkonfiguration hinzu (oder importieren Sie das AOP-Konfigurationsmodul) und stellen Sie sicher, dass support.aop in den @ComponentScan fällt. Wenn das Root-Paket der Anwendung com.example.contextflow ist, erfasst der Scan von dort alles, was benötigt wird.
Infolgedessen bleibt die Serviceschicht sauber: In den Methoden gibt es kein nanoTime(), kein try/finally, keine sich wiederholenden Zeilen. Und dennoch erhalten Sie Beobachtbarkeit des Anwendungsverhaltens, was besonders erfreulich ist, wenn es mehr Services gibt und die Szenarien länger werden.
7. Typische Fehler bei der Arbeit mit Spring AOP
Bei diesem Thema sind die Fehler normalerweise nicht „subtil“, sondern sehr direkt: Entweder haben Sie vergessen, die Infrastruktur zu aktivieren, oder Sie haben den Advice so geschrieben, dass er die Zielmethode nicht aufruft, oder Sie haben den Pointcut überhaupt nicht getroffen. Die gute Nachricht: All dies wird nicht durch Schamanentum behoben, sondern durch Überprüfung einiger bestimmter Stellen.
Fehler Nr. 1: @EnableAspectJAutoProxy vergessen.
Das Symptom ist besonders tückisch: Der Aspekt existiert als Klasse, kann sogar als Bean hochkommen (wenn er @Component ist), die Anwendung startet, aber in der Ausgabe gibt es keine Spur des Advices. Der Grund ist einfach: Der Container hat keine „Erlaubnis“ erhalten, AOP-Proxys zu erstellen, und es findet kein Auto-Proxying statt.
Fehler Nr. 2: Der Aspekt wurde keine Spring-Bean.
@Aspect ist keine Registrierung. Wenn Sie @Component entfernt und kein @Bean in der Konfiguration hinzugefügt haben, sieht der Container den Aspekt nicht. Infolgedessen ist AOP aktiviert, aber es gibt nichts anzuwenden. Wichtig zu merken: Ein Aspekt in Spring ist eine gewöhnliche Bean.
Fehler Nr. 3: Im @Around wurde vergessen, proceed() aufzurufen.
Das ist ein Klassiker. Es scheint alles zu funktionieren, der Advice druckt Nachrichten, aber die Geschäftsmethode wird nicht ausgeführt. Im Around-Advice müssen Sie pjp.proceed() aufrufen (oder bewusst die Ausführung durch etwas anderes ersetzen – aber das ist definitiv nicht unser Lehrszenario).
Fehler Nr. 4: Der Advice gibt das Ergebnis von proceed() nicht zurück.
Wenn die Zielmethode einen Wert zurückgibt, Sie aber im Advice null oder gar nichts zurückgeben, brechen Sie den Methodenvertrag. Im besten Fall erhalten Sie eine NullPointerException im aufrufenden Code, im schlimmsten Fall stille logische Fehler. Teaching default: return pjp.proceed(); und erst dann komplizierter machen.
Fehler Nr. 5: Sie rufen den Service nicht aus dem Kontext auf, sondern mit new.
AOP funktioniert nur bei container-managed Beans. Wenn Sie ein Objekt manuell erstellt haben, hat Spring keinen Proxy untergeschoben, der Advice wird nicht ausgeführt. Das Symptom klingt meistens: „Warum funktioniert der Aspekt nicht, ich habe doch die Annotationen geschrieben?!“, und die Antwort ist langweilig: Weil Annotationen keine Magie sind, sondern Metadaten für den Container.
GO TO FULL VERSION