Spring Boot Basic – Tag 6 von 10
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting


Aspektorientierte

📍 Deine Position im Kurs

TagThemaStatus
✅ 1Erste REST APIAbgeschlossen
✅ 2Spring Container & DIAbgeschlossen
✅ 3@Controller & Thymeleaf BasicsAbgeschlossen
✅ 4Thymeleaf Forms & MVC-PatternAbgeschlossen
✅ 5Konfiguration & LoggingAbgeschlossen
→ 6DI & AOP im Detail👉 DU BIST HIER!
7Scopes in SpringNoch nicht freigeschaltet
8WebSocketsNoch nicht freigeschaltet
9JAX-RS in Spring BootNoch nicht freigeschaltet
10Integration & AbschlussNoch nicht freigeschaltet

Modul: Spring Boot Basic (10 Arbeitstage)
Dein Ziel: Dependency Injection komplett verstehen & AOP-Grundlagen beherrschen!


📋 Voraussetzungen

Du brauchst:

  • ✅ Tag 1-5 abgeschlossen
  • ✅ Grundverständnis von @Component, @Service, @Controller
  • ✅ PersonService und PersonViewController funktionieren

Tag 5 verpasst? → Hier geht’s zum Blogbeitrag Tag 5


⚡ Was dich heute erwartet

Bisher: Du hast DI schon benutzt (PersonService im Controller), aber wie funktioniert das wirklich?

Heute: Du verstehst die Magie hinter Spring und lernst AOP (Aspektorientierte Programmierung)!

Das Problem:

  • Wie funktioniert @Autowired wirklich?
  • Was ist der Unterschied zwischen Constructor und Field Injection?
  • Was passiert, wenn Spring mehrere Beans findet?
  • Wie kannst du Cross-Cutting Concerns (Logging, Security) elegant lösen?

Die Lösung:

  • Dependency Injection im Detail verstehen
  • @Autowired, @Qualifier, @Primary richtig nutzen
  • AOP mit @Aspect für wiederkehrende Aufgaben
  • Custom Annotations erstellen

🎯 Dein Lernpfad heute

Du arbeitest heute in mehreren aufbauenden Schwierigkeitsstufen. Arbeite in deinem eigenen Tempo durch die Schritte:

🟢 Grundlagen (Schritte 1-5)

Was du lernst:

  • Dependency Injection wirklich verstehen
  • Constructor vs Field vs Setter Injection
  • @Qualifier und @Primary bei mehreren Beans
  • Bean Lifecycle und @PostConstruct/@PreDestroy
  • AOP-Grundkonzepte verstehen

Ziel: Du verstehst, wie Spring Beans verwaltet und injiziert


🟡 Professional (Schritte 6-7)

Was du lernst:

  • Deinen ersten Aspect erstellen (Logging)
  • @Before, @After, @Around Advice nutzen
  • Performance-Messung mit AOP
  • Pointcut-Expressions schreiben

Ziel: Production-Ready AOP-Logging implementiert


🔵 Bonus: Enterprise Features (Schritte 8-9)

Was du lernst:

  • Custom Annotations erstellen (@Timed)
  • Selektive AOP-Anwendung
  • Weitere Custom Annotations (@Logged, @ValidateNotNull)
  • AOP Best Practices

Ziel: Enterprise-Level Cross-Cutting Concerns elegant lösen


💻 Los geht’s!

🟢 GRUNDLAGEN (Schritte 1-5)

Schritt 1: Dependency Injection – Was ist das wirklich?

1.1 Das Problem ohne DI

Stell dir vor, du hättest KEINE Dependency Injection:

// ❌ OHNE DI - Hart verdrahtet!
public class PersonViewController {
    
    // Controller ERSTELLT selbst den Service
    private PersonService personService = new PersonService();
    
    public String showPersons(Model model) {
        List<Person> persons = personService.getAllPersons();
        model.addAttribute("persons", persons);
        return "persons-list";
    }
}

Was ist das Problem?

  1. Tight Coupling (Enge Kopplung)
    • Controller ist direkt abhängig von PersonService
    • Du kannst PersonService nicht austauschen
  2. Schwer testbar
    • Wie testest du den Controller ohne echten PersonService?
    • Mock-Objekte unmöglich
  3. Keine Flexibilität
    • Was, wenn PersonService verschiedene Implementierungen hat?
    • PersonServiceImpl, PersonServiceMock, PersonServiceDatabase?
  4. Singleton-Problem
    • Jeder Controller würde eigenen PersonService erstellen
    • Kein Singleton-Pattern möglich

1.2 Die Lösung: Dependency Injection

Mit DI:

// ✅ MIT DI - Spring injiziert!
@Controller
@RequiredArgsConstructor  // Lombok: Constructor für final fields
public class PersonViewController {
    
    // Controller BEKOMMT den Service injiziert
    private final PersonService personService;
    
    // Spring ruft automatisch auf:
    // public PersonViewController(PersonService personService) {
    //     this.personService = personService;
    // }
    
    public String showPersons(Model model) {
        List<Person> persons = personService.getAllPersons();
        model.addAttribute("persons", persons);
        return "persons-list";
    }
}

Was macht Spring?

  1. Spring Container startet
  2. Scannt alle Klassen mit @Component, @Service, @Controller
  3. Erstellt Beans (Singleton-Instanzen)
  4. Injiziert Abhängigkeiten automatisch
┌──────────────────────────────────┐
│     Spring IoC Container         │
│                                   │
│  ┌─────────────────────────┐    │
│  │ PersonService Bean      │    │
│  │ (Singleton)             │    │
│  └─────────────────────────┘    │
│              ↓ injiziert         │
│  ┌─────────────────────────┐    │
│  │ PersonViewController    │    │
│  │ Bean (Singleton)        │    │
│  └─────────────────────────┘    │
│                                   │
└──────────────────────────────────┘

Vorteile:

  • ✅ Loose Coupling – Controller kennt nur das Interface
  • ✅ Testbar – Mock-Objekte einfach injizierbar
  • ✅ Flexibel – Implementierung austauschbar
  • ✅ Singleton – Spring verwaltet Bean-Lifecycle

Schritt 2: Die 3 Arten von Dependency Injection

Spring unterstützt 3 Arten, Abhängigkeiten zu injizieren:

2.1 Constructor Injection (BEST PRACTICE!)

@Controller
public class PersonViewController {
    
    private final PersonService personService;
    
    // Constructor Injection
    @Autowired  // Optional ab Spring 4.3!
    public PersonViewController(PersonService personService) {
        this.personService = personService;
    }
}

Mit Lombok noch einfacher:

@Controller
@RequiredArgsConstructor  // Generiert Constructor automatisch!
public class PersonViewController {
    
    private final PersonService personService;
    
    // Constructor wird von Lombok generiert!
}

Vorteile:

  • ✅ Immutable (final möglich)
  • ✅ Testbar (Constructor kannst du manuell aufrufen)
  • ✅ Null-safe (Spring garantiert: Bean existiert)
  • ✅ Best Practice!

Wann nutzen? Immer! (außer du hast gute Gründe dagegen)

2.2 Field Injection (NICHT EMPFOHLEN!)

@Controller
public class PersonViewController {
    
    @Autowired  // Field Injection
    private PersonService personService;  // Nicht final!
    
    // Kein Constructor nötig
}

Vorteile:

  • ✅ Weniger Code
  • ✅ Kein Constructor nötig

Nachteile:

  • ❌ Nicht testbar (wie setzt du personService im Test?)
  • ❌ Nicht immutable (kein final möglich)
  • ❌ Hidden Dependencies (siehst du nicht auf den ersten Blick)
  • ❌ Circular Dependencies schwerer zu erkennen

Wann nutzen? Fast nie! Nur in sehr speziellen Fällen.

2.3 Setter Injection (SELTEN GENUTZT)

@Controller
public class PersonViewController {
    
    private PersonService personService;  // Nicht final!
    
    @Autowired
    public void setPersonService(PersonService personService) {
        this.personService = personService;
    }
}

Vorteile:

  • ✅ Optional Dependencies möglich
  • ✅ Dependency kann später geändert werden

Nachteile:

  • ❌ Nicht immutable
  • ❌ Bean könnte temporär in inkonsistentem Zustand sein

Wann nutzen? Nur für optionale Abhängigkeiten!

2.4 Vergleichstabelle

FeatureConstructorFieldSetter
Immutable (final)
Testbar⚠️
Null-safe
Wenig Code⚠️
Best Practice
Optional Dependencies

Faustregel: Nutze IMMER Constructor Injection!

2.5 Praxis-Beispiel: Refactoring zu Constructor Injection

Vorher (Field Injection – SCHLECHT):

@Controller
public class PersonViewController {
    
    @Autowired
    private PersonService personService;
    
    @Autowired
    private AppProperties appProperties;
    
    @Autowired
    private EmailService emailService;
    
    // 3 versteckte Dependencies!
}

Nachher (Constructor Injection – GUT):

@Controller
@RequiredArgsConstructor
public class PersonViewController {
    
    private final PersonService personService;
    private final AppProperties appProperties;
    private final EmailService emailService;
    
    // Constructor von Lombok generiert
    // 3 Dependencies sofort sichtbar!
}

Was ist besser?

  • ✅ Dependencies sind sofort erkennbar
  • ✅ Alle final (immutable)
  • ✅ Leicht testbar
  • ✅ Weniger Code (dank Lombok)

Schritt 3: @Qualifier & @Primary – Mehrere Beans (1 Stunde)

3.1 Das Problem: Mehrere Implementierungen

Stell dir vor, du hast verschiedene PersonService-Implementierungen:

// Interface
public interface PersonService {
    List<Person> getAllPersons();
    Person getPersonById(Long id);
    Person createPerson(Person person);
}

// Implementierung 1: In-Memory
@Service
public class InMemoryPersonService implements PersonService {
    private final List<Person> persons = new ArrayList<>();
    // ...
}

// Implementierung 2: Mit Caching
@Service
public class CachedPersonService implements PersonService {
    private final Map<Long, Person> cache = new HashMap<>();
    // ...
}

Was passiert jetzt?

@Controller
@RequiredArgsConstructor
public class PersonViewController {
    
    private final PersonService personService;  // Welche Implementierung???
}

Spring weiß nicht, welche Bean es injizieren soll!

Error: No qualifying bean of type 'PersonService' available:
expected single matching bean but found 2: inMemoryPersonService, cachedPersonService

3.2 Lösung 1: @Qualifier

@Controller
@RequiredArgsConstructor
public class PersonViewController {
    
    @Qualifier("inMemoryPersonService")  // Bean-Name angeben!
    private final PersonService personService;
    
    // Mit Lombok-Workaround:
    public PersonViewController(@Qualifier("inMemoryPersonService") PersonService personService) {
        this.personService = personService;
    }
}

Bean-Namen:

  • Spring nutzt Class-Name mit kleinem Anfangsbuchstaben
  • InMemoryPersonService → inMemoryPersonService
  • CachedPersonService → cachedPersonService

Eigene Bean-Namen vergeben:

@Service("memoryService")  // Custom Bean-Name
public class InMemoryPersonService implements PersonService {
    // ...
}

// Verwendung:
@Qualifier("memoryService")
private final PersonService personService;

3.3 Lösung 2: @Primary

@Service
@Primary  // Diese Implementierung ist die Standard-Wahl!
public class InMemoryPersonService implements PersonService {
    // ...
}

@Service
public class CachedPersonService implements PersonService {
    // ...
}

Jetzt:

@Controller
@RequiredArgsConstructor
public class PersonViewController {
    
    // Spring nimmt automatisch InMemoryPersonService (@Primary)
    private final PersonService personService;
}

@Primary vs @Qualifier:

@Primary@Qualifier
VerwendungAuf Bean-KlasseAuf Injection-Point
Standard-Bean✅ Ja❌ Nein
Spezifische Bean❌ Nein✅ Ja
Wann nutzen?Eine Implementierung ist StandardVerschiedene Stellen brauchen verschiedene Implementierungen

3.4 Praxis-Beispiel: Dev vs Prod Service

Szenario: In Dev nutzt du InMemory, in Prod Datenbank (später)

@Service
@Profile("dev")  // Nur in DEV!
@Primary
public class InMemoryPersonService implements PersonService {
    private final List<Person> persons = new ArrayList<>();
    // ...
}

@Service
@Profile("prod")  // Nur in PROD!
@Primary
public class DatabasePersonService implements PersonService {
    // Würde mit Datenbank arbeiten (kommt später)
    // ...
}

Controller bleibt gleich:

@Controller
@RequiredArgsConstructor
public class PersonViewController {
    
    // Spring wählt automatisch die richtige Implementierung
    // je nach aktivem Profil!
    private final PersonService personService;
}

Schritt 4: Bean Lifecycle verstehen (45 Min)

4.1 Wie Spring Beans erstellt

Bean Lifecycle:

1. Spring Container startet
      ↓
2. Component Scanning (@ComponentScan)
      ↓
3. Bean Definitions erstellen
      ↓
4. Bean Instantiation (Constructor)
      ↓
5. Dependency Injection
      ↓
6. @PostConstruct ausführen
      ↓
7. Bean ist bereit!
      ↓
8. Application läuft...
      ↓
9. @PreDestroy ausführen
      ↓
10. Spring Container stoppt

4.2 @PostConstruct und @PreDestroy

@Service
public class PersonService {
    
    private static final Logger log = LoggerFactory.getLogger(PersonService.class);
    
    private final List<Person> persons = new ArrayList<>();
    
    // 1. Constructor wird aufgerufen
    public PersonService() {
        log.info("PersonService Constructor called");
    }
    
    // 2. @PostConstruct wird NACH Dependency Injection aufgerufen
    @PostConstruct
    public void init() {
        log.info("PersonService @PostConstruct - Initialization");
        // Hier kannst du Setup-Logic machen
        // z.B. Cache laden, Verbindungen aufbauen, etc.
    }
    
    // 3. @PreDestroy wird VOR dem Shutdown aufgerufen
    @PreDestroy
    public void cleanup() {
        log.info("PersonService @PreDestroy - Cleanup");
        // Hier kannst du aufräumen
        // z.B. Verbindungen schließen, Cache speichern, etc.
    }
    
    public List<Person> getAllPersons() {
        return persons;
    }
}

Console-Output beim Start:

10:30:45.123  INFO PersonService : PersonService Constructor called
10:30:45.124  INFO PersonService : PersonService @PostConstruct - Initialization
10:30:45.125  INFO Application  : Started Application in 2.5 seconds

Console-Output beim Shutdown:

10:35:00.001  INFO PersonService : PersonService @PreDestroy - Cleanup

Wann nutzen?

AnnotationWann nutzen?
@PostConstructSetup-Logic NACH Dependency Injection (Verbindungen aufbauen, Cache laden)
@PreDestroyCleanup VOR Shutdown (Verbindungen schließen, Daten speichern)

4.3 Praxis-Beispiel: Cache-Service

@Service
public class CacheService {
    
    private static final Logger log = LoggerFactory.getLogger(CacheService.class);
    
    private Map<String, Object> cache;
    
    @PostConstruct
    public void loadCache() {
        log.info("Loading cache from disk...");
        cache = new HashMap<>();
        // Cache von Datei laden
        log.info("Cache loaded with {} entries", cache.size());
    }
    
    @PreDestroy
    public void saveCache() {
        log.info("Saving cache to disk...");
        // Cache in Datei speichern
        log.info("Cache saved successfully");
    }
    
    public void put(String key, Object value) {
        cache.put(key, value);
    }
    
    public Object get(String key) {
        return cache.get(key);
    }
}

Schritt 5: AOP (Aspect-Oriented Programming) Einführung (1.5 Stunden)

5.1 Was ist AOP?

Problem: Du hast wiederkehrende Aufgaben (Cross-Cutting Concerns):

  • Logging (jede Methode loggen)
  • Performance-Messung (wie lange dauert jede Methode?)
  • Security-Checks (ist User berechtigt?)
  • Transaction-Management
  • Error-Handling

Ohne AOP:

@Service
public class PersonService {
    
    private static final Logger log = LoggerFactory.getLogger(PersonService.class);
    
    public Person createPerson(Person person) {
        // Logging (wiederholt sich!)
        log.info("Method createPerson called");
        long start = System.currentTimeMillis();
        
        try {
            // Eigentliche Business-Logic
            person.setId(idCounter.getAndIncrement());
            persons.add(person);
            
            // Logging (wiederholt sich!)
            long duration = System.currentTimeMillis() - start;
            log.info("Method createPerson finished in {}ms", duration);
            
            return person;
        } catch (Exception e) {
            // Error-Handling (wiederholt sich!)
            log.error("Error in createPerson", e);
            throw e;
        }
    }
    
    public Person updatePerson(Person person) {
        // Wieder dasselbe Logging!
        log.info("Method updatePerson called");
        long start = System.currentTimeMillis();
        
        try {
            // Business-Logic
            // ...
            
            long duration = System.currentTimeMillis() - start;
            log.info("Method updatePerson finished in {}ms", duration);
            
            return person;
        } catch (Exception e) {
            log.error("Error in updatePerson", e);
            throw e;
        }
    }
    
    // JEDE Methode hat dasselbe Logging/Performance-Measuring!
}

Das ist DRY-Violation (Don’t Repeat Yourself)!

Mit AOP:

@Service
public class PersonService {
    
    @Logged  // Custom Annotation!
    @Timed   // Custom Annotation!
    public Person createPerson(Person person) {
        // Nur Business-Logic!
        person.setId(idCounter.getAndIncrement());
        persons.add(person);
        return person;
    }
    
    @Logged
    @Timed
    public Person updatePerson(Person person) {
        // Nur Business-Logic!
        // ...
        return person;
    }
}

AOP kümmert sich automatisch um:

  • ✅ Logging
  • ✅ Performance-Messung
  • ✅ Error-Handling
  • ✅ Alles, was du willst!

5.2 AOP-Konzepte

BegriffBedeutungBeispiel
AspectDer „Aspekt“ (z.B. Logging)@Aspect class LoggingAspect
Join PointPunkt im Code, wo Aspect wirken kannMethoden-Aufruf
AdviceWas soll ausgeführt werden?Code, der vor/nach Methode läuft
PointcutWO soll Aspect wirken?@Before("execution(* com.example..*.*(..))"))

Arten von Advice:

AdviceWann?Verwendung
@BeforeVOR MethodeLogging, Validation, Security-Check
@AfterNACH Methode (immer)Cleanup, Logging
@AfterReturningNACH erfolgreicher MethodeSuccess-Logging
@AfterThrowingNACH ExceptionError-Handling
@AroundVOR UND NACH MethodePerformance-Messung, Transaction

5.3 Dependency hinzufügen

pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Nach dem Hinzufügen:

mvn clean install

✅ Checkpoint Grundlagen

Kontrolliere:

  • [ ] Du verstehst, was Dependency Injection ist
  • [ ] Du kennst die 3 Injection-Arten und ihre Vor-/Nachteile
  • [ ] Du nutzt Constructor Injection mit @RequiredArgsConstructor
  • [ ] Du verstehst @Qualifier und @Primary
  • [ ] Du kennst den Bean Lifecycle
  • [ ] Du hast @PostConstruct und @PreDestroy verstanden
  • [ ] Du weißt, was AOP ist und wofür es gut ist
  • [ ] Du kennst die verschiedenen Advice-Typen
  • [ ] spring-boot-starter-aop ist in deiner pom.xml

Alles ✅? Weiter zu 🟡 Professional!


🟡 PROFESSIONAL (Schritte 6-7)

Schritt 6: Dein erster Aspect – Logging (1 Stunde)

6.1 Logging-Aspect erstellen

Erstelle: src/main/java/.../aspect/LoggingAspect.java

package com.example.helloworldapi.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
    
    // Pointcut: Alle Methoden in com.example.helloworldapi.service
    @Before("execution(* com.example.helloworldapi.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        log.info("→ Calling {}.{}", className, methodName);
    }
    
    @After("execution(* com.example.helloworldapi.service.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        log.info("← Finished {}.{}", className, methodName);
    }
}

Was macht das?

  • @Aspect → Das ist ein Aspect
  • @Component → Spring verwaltet diese Klasse
  • @Before → Wird VOR der Methode ausgeführt
  • @After → Wird NACH der Methode ausgeführt
  • execution(* com.example.helloworldapi.service.*.*(..)) → Pointcut-Expression

Pointcut-Expression erklärt:

execution(* com.example.helloworldapi.service.*.*(..))
          ↑              ↑                      ↑ ↑  ↑
      Rückgabe        Package              Klasse↓  ↓
       (egal)                                   Methode
                                                 (alle)
                                                Parameter
                                                (egal)

6.2 Testen

Starte die App:

mvn spring-boot:run -Dspring-boot.run.profiles=dev

Console-Output:

10:30:45.123  INFO LoggingAspect : → Calling PersonService.createPerson
10:30:45.124  INFO PersonService : Creating new person: Max Mustermann
10:30:45.125  INFO PersonService : Person created successfully with ID: 1
10:30:45.126  INFO LoggingAspect : ← Finished PersonService.createPerson

Jetzt wird JEDE Service-Methode automatisch geloggt! 🎉

Öffne: http://localhost:8080/persons/add

Füge Person hinzu → Console zeigt:

10:35:00.001  INFO LoggingAspect : → Calling PersonService.getAllPersons
10:35:00.002 DEBUG PersonService : Getting all persons, current count: 3
10:35:00.003  INFO LoggingAspect : ← Finished PersonService.getAllPersons
10:35:00.010  INFO LoggingAspect : → Calling PersonService.createPerson
10:35:00.011  INFO PersonService : Creating new person: Test User
10:35:00.012  INFO PersonService : Person created successfully with ID: 4
10:35:00.013  INFO LoggingAspect : ← Finished PersonService.createPerson

Das ist AOP-Magie! ✨


Schritt 7: Performance-Messung mit @Around (1.5 Stunden)

7.1 @Around Advice

@Around ist der mächtigste Advice-Typ:

  • Läuft VOR UND NACH der Methode
  • Kann Methoden-Ausführung steuern
  • Kann Rückgabewert ändern
  • Kann Exceptions abfangen

Erstelle: src/main/java/.../aspect/PerformanceAspect.java

package com.example.helloworldapi.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PerformanceAspect {
    
    private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);
    
    @Around("execution(* com.example.helloworldapi.service.*.*(..))")
    public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        // VOR der Methode
        long start = System.currentTimeMillis();
        
        Object result = null;
        try {
            // Methode AUSFÜHREN
            result = joinPoint.proceed();
            
            // NACH der Methode (Erfolg)
            long duration = System.currentTimeMillis() - start;
            log.info("⏱️ {}.{} took {}ms", className, methodName, duration);
            
        } catch (Exception e) {
            // NACH der Methode (Fehler)
            long duration = System.currentTimeMillis() - start;
            log.error("❌ {}.{} failed after {}ms", className, methodName, duration);
            throw e;
        }
        
        return result;
    }
}

Was macht joinPoint.proceed()?

1. Code VOR joinPoint.proceed()  → @Before
2. joinPoint.proceed()            → Eigentliche Methode ausführen
3. Code NACH joinPoint.proceed()  → @After

7.2 Testen

Starte App und füge Person hinzu:

Console-Output:

10:30:45.123  INFO LoggingAspect      : → Calling PersonService.createPerson
10:30:45.124  INFO PersonService      : Creating new person: Max Mustermann
10:30:45.125  INFO PersonService      : Person created successfully with ID: 1
10:30:45.126  INFO PerformanceAspect  : ⏱️ PersonService.createPerson took 3ms
10:30:45.127  INFO LoggingAspect      : ← Finished PersonService.createPerson

Jetzt siehst du die Performance jeder Methode! 🚀


✅ Checkpoint Professional

Kontrolliere:

  • [ ] LoggingAspect ist erstellt und funktioniert
  • [ ] @Before und @After loggen Methoden-Aufrufe
  • [ ] PerformanceAspect ist erstellt
  • [ ] @Around misst die Ausführungszeit
  • [ ] Du siehst Performance-Logs in der Console
  • [ ] Du verstehst Pointcut-Expressions
  • [ ] Du verstehst joinPoint.proceed()

Alles ✅? Weiter zu 🔵 Enterprise Features!


🔵 BONUS: ENTERPRISE FEATURES (Schritte 8-9)

Schritt 8: Custom Annotations erstellen (1.5 Stunden)

8.1 Warum Custom Annotations?

Problem: Der Pointcut ist zu breit!

@Around("execution(* com.example.helloworldapi.service.*.*(..))")

Das matcht ALLE Service-Methoden. Was, wenn du nur bestimmte Methoden messen willst?

Lösung: Custom Annotation!

@Service
public class PersonService {
    
    @Timed  // Nur diese Methode wird gemessen!
    public Person createPerson(Person person) {
        // ...
    }
    
    public List<Person> getAllPersons() {
        // Diese NICHT!
    }
}

8.2 @Timed Annotation erstellen

Erstelle: src/main/java/.../annotation/Timed.java

package com.example.helloworldapi.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)  // Nur auf Methoden
@Retention(RetentionPolicy.RUNTIME)  // Zur Laufzeit verfügbar
public @interface Timed {
    // Keine Parameter nötig
}

Annotation-Elemente erklärt:

ElementBedeutung
@Target(ElementType.METHOD)Annotation kann nur auf Methoden
@Target(ElementType.TYPE)Annotation kann nur auf Klassen
@Retention(RetentionPolicy.RUNTIME)Annotation zur Laufzeit verfügbar (für AOP!)
@Retention(RetentionPolicy.SOURCE)Annotation nur im Source-Code (z.B. @Override)

8.3 Aspect für @Timed anpassen

PerformanceAspect.java anpassen:

@Aspect
@Component
public class PerformanceAspect {
    
    private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);
    
    // Neuer Pointcut: Alle Methoden mit @Timed Annotation!
    @Around("@annotation(com.example.helloworldapi.annotation.Timed)")
    public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        long start = System.currentTimeMillis();
        
        Object result = null;
        try {
            result = joinPoint.proceed();
            
            long duration = System.currentTimeMillis() - start;
            log.info("⏱️ {}.{} took {}ms", className, methodName, duration);
            
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - start;
            log.error("❌ {}.{} failed after {}ms", className, methodName, duration);
            throw e;
        }
        
        return result;
    }
}

Pointcut-Expression:

@Around("@annotation(com.example.helloworldapi.annotation.Timed)")
        ↑                                       ↑
    Auf Methoden mit dieser Annotation

8.4 @Timed verwenden

PersonService.java:

@Service
public class PersonService {
    
    private static final Logger log = LoggerFactory.getLogger(PersonService.class);
    
    private final List<Person> persons = new ArrayList<>();
    private final AtomicLong idCounter = new AtomicLong(1);
    
    // NICHT gemessen
    public List<Person> getAllPersons() {
        log.debug("Getting all persons, current count: {}", persons.size());
        return persons;
    }
    
    // NICHT gemessen
    public Person getPersonById(Long id) {
        log.debug("Looking for person with ID: {}", id);
        return persons.stream()
            .filter(p -> p.getId().equals(id))
            .findFirst()
            .orElse(null);
    }
    
    @Timed  // WIRD gemessen!
    public Person createPerson(Person person) {
        log.info("Creating new person: {} {}", person.getFirstname(), person.getLastname());
        
        person.setId(idCounter.getAndIncrement());
        persons.add(person);
        
        log.info("Person created successfully with ID: {}", person.getId());
        return person;
    }
    
    @Timed  // WIRD gemessen!
    public Person updatePerson(Person person) {
        log.info("Updating person with ID: {}", person.getId());
        
        int index = -1;
        for (int i = 0; i < persons.size(); i++) {
            if (persons.get(i).getId().equals(person.getId())) {
                index = i;
                break;
            }
        }
        
        if (index >= 0) {
            persons.set(index, person);
            log.info("Person {} updated successfully", person.getId());
            return person;
        } else {
            log.error("Failed to update person {} - not found!", person.getId());
            return null;
        }
    }
    
    @Timed  // WIRD gemessen!
    public void deletePerson(Long id) {
        log.info("Deleting person with ID: {}", id);
        
        boolean removed = persons.removeIf(p -> p.getId().equals(id));
        
        if (removed) {
            log.info("Person {} deleted successfully", id);
        } else {
            log.warn("Could not delete person {} - not found", id);
        }
    }
}

8.5 Testen

Starte App und öffne: http://localhost:8080/persons

Console zeigt:

10:30:45.123  INFO PersonViewController : Displaying 3 persons
10:30:45.124 DEBUG PersonService         : Getting all persons, current count: 3

Kein Performance-Log! ✅ (getAllPersons hat kein @Timed)

Füge Person hinzu:

10:35:00.001  INFO PersonService      : Creating new person: Test User
10:35:00.002  INFO PersonService      : Person created successfully with ID: 4
10:35:00.003  INFO PerformanceAspect  : ⏱️ PersonService.createPerson took 2ms

Performance-Log nur für @Timed Methoden! ✅


Schritt 9: Weitere Custom Annotations (Bonus – 1 Stunde)

9.1 @Logged Annotation

Erstelle: src/main/java/.../annotation/Logged.java

package com.example.helloworldapi.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Logged {
    // Optional: Parameter für Log-Level
    String level() default "INFO";
}

Aspect erstellen:

@Aspect
@Component
public class LoggedAspect {
    
    private static final Logger log = LoggerFactory.getLogger(LoggedAspect.class);
    
    @Around("@annotation(logged)")
    public Object logMethodCall(ProceedingJoinPoint joinPoint, Logged logged) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String level = logged.level();
        
        log.info("[{}] → Calling {}.{}", level, className, methodName);
        
        Object result = joinPoint.proceed();
        
        log.info("[{}] ← Finished {}.{}", level, className, methodName);
        
        return result;
    }
}

Verwendung:

@Logged(level = "DEBUG")
public Person createPerson(Person person) {
    // ...
}

9.2 @ValidateNotNull Annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateNotNull {
    // Welche Parameter sollen validiert werden?
    int[] parameterIndices() default {};
}

Aspect:

@Aspect
@Component
public class ValidationAspect {
    
    private static final Logger log = LoggerFactory.getLogger(ValidationAspect.class);
    
    @Before("@annotation(validateNotNull)")
    public void validateParameters(JoinPoint joinPoint, ValidateNotNull validateNotNull) {
        Object[] args = joinPoint.getArgs();
        int[] indices = validateNotNull.parameterIndices();
        
        for (int index : indices) {
            if (index < args.length && args[index] == null) {
                String methodName = joinPoint.getSignature().getName();
                throw new IllegalArgumentException(
                    methodName + ": Parameter at index " + index + " must not be null!");
            }
        }
    }
}

Verwendung:

@ValidateNotNull(parameterIndices = {0})  // Erster Parameter muss != null
public Person createPerson(Person person) {
    // Kein manueller Null-Check nötig!
    person.setId(idCounter.getAndIncrement());
    persons.add(person);
    return person;
}

✅ Checkpoint Enterprise Features

Kontrolliere:

  • [ ] @Timed Annotation erstellt
  • [ ] PerformanceAspect nutzt @Timed
  • [ ] Performance wird nur für @Timed-Methoden gemessen
  • [ ] @Logged Annotation funktioniert (optional)
  • [ ] @ValidateNotNull funktioniert (optional)
  • [ ] Du verstehst, wie Custom Annotations funktionieren
  • [ ] Du kannst eigene Annotations erstellen

Alles ✅? Du bist jetzt ein AOP-Profi! 🎉

Hier sind einige externe Links, die sich hervorragend zum Thema „Konfiguration & Logging in Spring Boot“ eignen — mit guten Erklärungen, Beispielen und Praxisbezug:

  • „Logging :: Spring Boot” — offizielle Dokumentation von Spring Boot zu Logging und Konfiguration. Home+1
  • „Logging in Spring Boot – Baeldung” — ein ausführlicher Artikel über Logging-Frameworks (Logback, Log4j2) in Spring Boot. Baeldung on Kotlin
  • „Spring Boot – Logging | GeeksforGeeks” — Einsteigerfreundlicher Artikel mit Beispielen zur Konfiguration über application.properties und Logging in Dateien. GeeksforGeeks+1
  • „A Guide to Spring Boot Logging: Best Practices & Techniques” — Blogbeitrag mit Überblick über Best Practices beim Logging in Spring Boot-Anwendungen. Last9
  • „How to Configure Default Log Files – Spring Boot Logging” — Praxisanleitung zum Einrichten von Logdateien, statt nur Konsolenausgabe. signoz.io

🔥 Elyndras Real Talk:

Nova kam heute völlig verwirrt zu mir: „Elyndra, ich verstehe das mit DI nicht. Warum soll ich nicht einfach new PersonService() schreiben? Das ist doch viel einfacher!“

Ich musste schmunzeln – diese Frage hatte ich vor Jahren auch!

Meine Antwort: „Stell dir vor, du hast 50 Controller, und alle nutzen PersonService. Jetzt willst du von InMemory auf Datenbank umstellen. Musst du wirklich in 50 Klassen new InMemoryPersonService() durch new DatabasePersonService() ersetzen?“

Nova: „Oh… nein, das wäre…“

„Genau!“ unterbrach ich. „Mit DI änderst du eine Zeile: @Primary von InMemory auf Database. Fertig. Alle 50 Controller nutzen automatisch die neue Implementierung!“

Code Sentinel, der gerade Coffee holte, nickte: „Und Tests! Wie mockst du PersonService, wenn der Controller ihn selbst erstellt? Unmöglich! Mit Constructor Injection kannst du einfach einen Mock injizieren.“

Franz-Martin kam dazu: „AOP ist dasselbe Prinzip. Du willst nicht in jeder Methode dieselben 10 Zeilen Logging-Code. Du willst es EINMAL definieren und überall nutzen. Das ist DRY – Don’t Repeat Yourself!“

Nova: „Okay, verstehe! Aber warum Field Injection schlecht ist, sehe ich immer noch nicht…“

Ich: „Weil es die Abhängigkeiten versteckt! Schau:“

// Field Injection - sieht sauber aus
@Controller
public class MegaController {
    @Autowired private ServiceA a;
    @Autowired private ServiceB b;
    @Autowired private ServiceC c;
    @Autowired private ServiceD d;
    @Autowired private ServiceE e;
    @Autowired private ServiceF f;
    @Autowired private ServiceG g;
    @Autowired private ServiceH h;
    // ... 10 weitere Dependencies
}

Nova: „Sieht kompakt aus…“

„Zu kompakt!“ rief Code Sentinel. „Constructor Injection würde dir sofort zeigen: Diese Klasse hat 18 Dependencies! Das ist ein Design-Problem! Field Injection versteckt das Problem.“

Franz-Martin: „Genau. Wenn dein Constructor 18 Parameter hat, schreit das förmlich: Refactor mich! Split mich auf! Field Injection lässt dich in falscher Sicherheit leben.“

Nova nickte langsam: „Also Constructor Injection ist wie ein Warnsignal für schlechtes Design?“

„Perfekt zusammengefasst!“ Alle drei gleichzeitig. 😄

Das ist der Unterschied zwischen Junior und Senior: Nicht nur Patterns nutzen, sondern verstehen WARUM sie existieren!


🆘 Troubleshooting: Die häufigsten Probleme

Problem 1: Aspect wird nicht ausgeführt

Symptom: Logging/Performance-Aspect macht nichts

Ursache 1: Dependency fehlt

<!-- pom.xml prüfen -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Ursache 2: @Component vergessen

@Aspect
@Component  // MUSS da sein!
public class LoggingAspect {

Ursache 3: Pointcut-Expression falsch

// ❌ FALSCH
@Before("execution(* com.example.service.*.*(..))")  // Package stimmt nicht!

// ✅ RICHTIG
@Before("execution(* com.example.helloworldapi.service.*.*(..))")

Problem 2: „No qualifying bean“ Fehler

Symptom:

No qualifying bean of type 'PersonService' available:
expected single matching bean but found 2

Lösung 1: @Primary nutzen

@Service
@Primary  // Diese Bean ist Standard!
public class InMemoryPersonService implements PersonService {

Lösung 2: @Qualifier nutzen

@Qualifier("inMemoryPersonService")
private final PersonService personService;

Problem 3: Circular Dependency

Symptom:

The dependencies of some of the beans in the application context form a cycle:
   personService
      ↓
   emailService
      ↓
   personService

Ursache: Service A injiziert Service B, Service B injiziert Service A

Lösung 1: Design überdenken (meist ein Design-Problem!)

Lösung 2: @Lazy nutzen (Notlösung!)

@RequiredArgsConstructor
public class PersonService {
    
    @Lazy  // Wird erst bei Bedarf injiziert
    private final EmailService emailService;
}

Problem 4: @PostConstruct wird nicht aufgerufen

Symptom: init()-Methode wird nicht ausgeführt

Ursache: Bean wird nicht von Spring verwaltet

// ❌ FALSCH - Kein @Component/@Service!
public class PersonService {
    @PostConstruct
    public void init() {
        // Wird NIE aufgerufen!
    }
}

// ✅ RICHTIG
@Service  // Bean wird von Spring verwaltet
public class PersonService {
    @PostConstruct
    public void init() {
        // Wird aufgerufen!
    }
}

Problem 5: Custom Annotation wird nicht erkannt

Symptom: @Timed funktioniert nicht

Ursache 1: @Retention fehlt

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)  // MUSS da sein!
public @interface Timed {
}

Ursache 2: Pointcut-Expression falsch

// ❌ FALSCH - Package fehlt
@Around("@annotation(Timed)")

// ✅ RICHTIG - Voller Package-Name
@Around("@annotation(com.example.helloworldapi.annotation.Timed)")

Problem 6: joinPoint.proceed() wirft Exception

Symptom: ProceedingJoinPoint hat keine proceed()-Methode

Ursache: Falscher JoinPoint-Typ

// ❌ FALSCH
@Around("...")
public Object measure(JoinPoint joinPoint) {  // Falscher Typ!
    joinPoint.proceed();  // Methode existiert nicht!
}

// ✅ RICHTIG
@Around("...")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
    joinPoint.proceed();  // Funktioniert!
}

Problem 7: Field Injection funktioniert nicht im Test

Symptom: personService ist null im Unit-Test

Ursache: Field Injection ist schwer testbar!

Lösung: Constructor Injection nutzen

// ❌ SCHLECHT (Field Injection)
@Controller
public class PersonViewController {
    @Autowired
    private PersonService personService;
    
    // Wie setzt du das im Test???
}

// ✅ GUT (Constructor Injection)
@Controller
@RequiredArgsConstructor
public class PersonViewController {
    private final PersonService personService;
    
    // Im Test einfach:
    // new PersonViewController(mockService)
}

❓ FAQ (Häufige Fragen)

Q: Warum Constructor Injection statt Field Injection?
A: Constructor Injection ist testbar, immutable (final), null-safe und zeigt Dependencies offen. Field Injection versteckt Abhängigkeiten und ist schwer testbar.

Q: Wann nutze ich @Qualifier und wann @Primary?
A: @Primary, wenn eine Implementierung der Standard ist. @Qualifier, wenn verschiedene Stellen verschiedene Implementierungen brauchen.

Q: Brauche ich wirklich AOP?
A: Für Cross-Cutting Concerns (Logging, Performance, Security) JA! Ohne AOP würdest du denselben Code in jeder Methode wiederholen. Das ist DRY-Violation.

Q: Was ist der Unterschied zwischen @Before und @Around?
A: @Before läuft nur VOR der Methode. @Around läuft VOR UND NACH und kann die Methode sogar überspringen oder den Rückgabewert ändern. @Around ist mächtiger aber komplexer.

Q: Kann ich mehrere Aspects auf eine Methode anwenden?
A: Ja! Du kannst @Logged, @Timed, @ValidateNotNull gleichzeitig nutzen. Spring führt alle passenden Aspects aus.

Q: Was passiert, wenn Aspect eine Exception wirft?
A: Die ursprüngliche Methode wird NICHT ausgeführt. Exception wird an Caller weitergegeben.

Q: Performance-Impact von AOP?
A: Minimal! Spring nutzt Proxies (CGLIB oder JDK Dynamic Proxies). Overhead ist meist < 1ms. Für 99% der Apps irrelevant.


📅 Nächster Kurstag: Tag 7

Morgen im Kurs / Nächster Blogbeitrag:

„Scopes in Spring – Singleton, Prototype, Request, Session“

Was du lernen wirst:

  • Bean Scopes verstehen (Singleton vs Prototype)
  • Request Scope für Web-Apps
  • Session Scope für User-Daten
  • Application Scope
  • Wann welchen Scope nutzen?
  • Scoped Proxies

Dauer: 8 Stunden
Voraussetzung: Tag 6 abgeschlossen

👉 Zum Blogbeitrag Tag 7 (erscheint morgen)


📚 Deine Fortschritts-Übersicht

TagThemaStatus
✅ 1Erste REST APIABGESCHLOSSEN! 🎉
✅ 2Spring Container & DIABGESCHLOSSEN! 🎉
✅ 3@Controller & Thymeleaf BasicsABGESCHLOSSEN! 🎉
✅ 4Thymeleaf Forms & MVC-PatternABGESCHLOSSEN! 🎉
✅ 5Konfiguration & LoggingABGESCHLOSSEN! 🎉
✅ 6DI & AOP im DetailABGESCHLOSSEN! 🎉
→ 7Scopes in SpringAls nächstes
8WebSocketsNoch offen
9JAX-RS in Spring BootNoch offen
10Integration & AbschlussNoch offen

Du hast 60% des Kurses geschafft! 💪

Alle Blogbeiträge dieser Serie:
👉 Spring Boot Basic – Komplette Übersicht


📥 Download & Ressourcen

Projekt zum Download:
👉 SpringBootAOP-v1.0.zip (Stand: 18.10.2025)

Was ist im ZIP enthalten:

  • ✅ Komplettes Maven-Projekt
  • ✅ Constructor Injection Beispiele
  • ✅ @Qualifier und @Primary Beispiele
  • ✅ LoggingAspect & PerformanceAspect
  • ✅ Custom @Timed Annotation
  • ✅ @PostConstruct und @PreDestroy Beispiele
  • ✅ README mit Schnellstart
  • ✅ AOP-GUIDE.md mit allen Pointcut-Expressions

Projekt starten:

# ZIP entpacken
# In NetBeans öffnen: File → Open Project
# Oder im Terminal:
mvn spring-boot:run -Dspring-boot.run.profiles=dev

Probleme? Issue melden oder schreib mir: elyndra@java-developer.online


Das war Tag 6 von Spring Boot Basic!

Du kannst jetzt:

  • ✅ Dependency Injection komplett verstehen
  • ✅ Constructor Injection richtig einsetzen
  • ✅ @Qualifier und @Primary nutzen
  • ✅ Bean Lifecycle verstehen
  • ✅ AOP mit @Aspect erstellen
  • ✅ Custom Annotations entwickeln
  • ✅ Cross-Cutting Concerns elegant lösen!

Morgen: Scopes in Spring – Singleton, Prototype & mehr! 🚀

Keep coding, keep learning! 💙


Tag 7 erscheint morgen. Bis dahin: Happy Coding!


Tags: #SpringBoot #DependencyInjection #AOP #Aspect #CustomAnnotations #Tag6

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.