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

📍 Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| ✅ 1 | Erste REST API | Abgeschlossen |
| ✅ 2 | Spring Container & DI | Abgeschlossen |
| ✅ 3 | @Controller & Thymeleaf Basics | Abgeschlossen |
| ✅ 4 | Thymeleaf Forms & MVC-Pattern | Abgeschlossen |
| ✅ 5 | Konfiguration & Logging | Abgeschlossen |
| → 6 | DI & AOP im Detail | 👉 DU BIST HIER! |
| 7 | Scopes in Spring | Noch nicht freigeschaltet |
| 8 | WebSockets | Noch nicht freigeschaltet |
| 9 | JAX-RS in Spring Boot | Noch nicht freigeschaltet |
| 10 | Integration & Abschluss | Noch 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?
- Tight Coupling (Enge Kopplung)
- Controller ist direkt abhängig von PersonService
- Du kannst PersonService nicht austauschen
- Schwer testbar
- Wie testest du den Controller ohne echten PersonService?
- Mock-Objekte unmöglich
- Keine Flexibilität
- Was, wenn PersonService verschiedene Implementierungen hat?
- PersonServiceImpl, PersonServiceMock, PersonServiceDatabase?
- 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?
- Spring Container startet
- Scannt alle Klassen mit @Component, @Service, @Controller
- Erstellt Beans (Singleton-Instanzen)
- 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
| Feature | Constructor | Field | Setter |
|---|---|---|---|
| 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→inMemoryPersonServiceCachedPersonService→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 | |
|---|---|---|
| Verwendung | Auf Bean-Klasse | Auf Injection-Point |
| Standard-Bean | ✅ Ja | ❌ Nein |
| Spezifische Bean | ❌ Nein | ✅ Ja |
| Wann nutzen? | Eine Implementierung ist Standard | Verschiedene 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?
| Annotation | Wann nutzen? |
|---|---|
| @PostConstruct | Setup-Logic NACH Dependency Injection (Verbindungen aufbauen, Cache laden) |
| @PreDestroy | Cleanup 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
| Begriff | Bedeutung | Beispiel |
|---|---|---|
| Aspect | Der „Aspekt“ (z.B. Logging) | @Aspect class LoggingAspect |
| Join Point | Punkt im Code, wo Aspect wirken kann | Methoden-Aufruf |
| Advice | Was soll ausgeführt werden? | Code, der vor/nach Methode läuft |
| Pointcut | WO soll Aspect wirken? | @Before("execution(* com.example..*.*(..))")) |
Arten von Advice:
| Advice | Wann? | Verwendung |
|---|---|---|
| @Before | VOR Methode | Logging, Validation, Security-Check |
| @After | NACH Methode (immer) | Cleanup, Logging |
| @AfterReturning | NACH erfolgreicher Methode | Success-Logging |
| @AfterThrowing | NACH Exception | Error-Handling |
| @Around | VOR UND NACH Methode | Performance-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ührtexecution(* 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:
| Element | Bedeutung |
|---|---|
@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.propertiesund 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
| Tag | Thema | Status |
|---|---|---|
| ✅ 1 | Erste REST API | ABGESCHLOSSEN! 🎉 |
| ✅ 2 | Spring Container & DI | ABGESCHLOSSEN! 🎉 |
| ✅ 3 | @Controller & Thymeleaf Basics | ABGESCHLOSSEN! 🎉 |
| ✅ 4 | Thymeleaf Forms & MVC-Pattern | ABGESCHLOSSEN! 🎉 |
| ✅ 5 | Konfiguration & Logging | ABGESCHLOSSEN! 🎉 |
| ✅ 6 | DI & AOP im Detail | ABGESCHLOSSEN! 🎉 |
| → 7 | Scopes in Spring | Als nächstes |
| 8 | WebSockets | Noch offen |
| 9 | JAX-RS in Spring Boot | Noch offen |
| 10 | Integration & Abschluss | Noch 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

