1. @Profile als Filter
Wenn Menschen zum ersten Mal das Wort „Profil“ hören, wird oft die Fantasie angeregt, dass „der Bean einen Schalter zum Umschalten“ bekommt. In Spring ist alles einfacher und ehrlicher: @Profile beeinflusst nicht das Verhalten eines bereits erstellten Objekts, sondern ob dieser Bean überhaupt beim Start im Container registriert wird. Das ist also ein Mechanismus des Anwendungsbaus, keine Geschäftslogik.
Einmal einfach ausgedrückt: @Profile ist wie eine Gesichtskontrolle am Eingang eines Clubs namens ApplicationContext. Der Bean hat einen Aufkleber auf der Stirn: "dev" oder "test". Der Container schaut auf die aktiven Profile und entscheidet: „den lassen wir rein“, „den nicht“. Wird er nicht reingelassen, gibt es kein Objekt, keine Abhängigkeit wird erfüllt, und wenn er benötigt wurde, stürzt der Kontext beim Start ab (und das ist gut, wegen Fail-Fast).
Ein kleines Schema, wo @Profile im Weltbild lebt:
flowchart TD
A[Start ApplicationContext] --> B[Konfiguration lesen und component scanning]
B --> C{"Überprüfung @Profile"}
C -->|Profil passt| D[BeanDefinition registrieren]
C -->|Profil passt nicht| E[Bean überspringen]
D --> F["Objekterstellung + Injection"]
F --> G[Anwendung bereit]
Daraus folgt eine wichtige methodische Regel: Geschäftsklassen (z.B. OrderPlacementService) sollte es grundsätzlich nicht interessieren, welches Profil aktiv ist. Sie sollten von Schnittstellen (AuditWriter, NotificationSender, OrderIdGenerator) abhängen und fertige Implementierungen erhalten. Ein Profil ist eine Entscheidung auf der Ebene „welchen Containerbau wir wollen“, nicht eine Entscheidung „wie eine Bestellung verarbeitet wird“.
Ein Profil kann an mehreren Stellen angebracht werden – auf einer Komponente, auf @Configuration oder auf einem einzelnen @Bean. Das sind nicht drei obligatorische Schichten gleichzeitig, sondern drei Varianten derselben Idee: bedingte Registrierung dort, wo sie am ehrlichsten zu lesen ist.
2. @Profile auf Spring-Komponenten
Der offensichtlichste Weg, @Profile zu verwenden, ist es direkt auf eine Komponente zu setzen, die ohnehin durch component scanning registriert wird. Das ist praktisch, wenn eine Klasse wirklich nur in einem Modus „lebt“ und Sie sie nicht immer im Container halten müssen. Ein Beispiel aus ContextFlow drängt sich geradezu auf: in dev möchte man Audit in der Konsole sehen, anstatt durch Dateien zu gehen.
Stellen wir uns vor, dass das Interface AuditWriter bereits existiert (es liegt bei uns in domain.ports), und die Implementierung ConsoleAuditWriter in infrastructure.audit. Dann sieht das Profil auf der Komponente so aus:
package com.example.contextflow.infrastructure.audit;
import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Profile("dev") // Bean wird nur bei aktivem Profil dev registriert
@Component
public class ConsoleAuditWriter implements AuditWriter {
@Override
public void write(String message) {
// Im dev-Modus ist das Audit am einfachsten direkt in der Konsole zu sehen
System.out.println("AUDIT: " + message); // Beispielausgabe: AUDIT: order created
}
}
Hier ist es wichtig, die Mechanik zu verstehen: Wenn das aktive Profil nicht dev ist, tut der Container einfach so, als gäbe es diese Klasse nicht als Bean. Sie kann im Code existieren, kompilieren, schön sein – aber sie gelangt nicht in den Kontext. Und wenn Sie in einem anderen Profil keine Alternative zu AuditWriter bereitstellen, erhält der Service, der AuditWriter im Konstruktor erwartet, den klassischen Fehler „Bean not found“.
Ein weiterer nützlicher Trick (und er gehört direkt zum Thema der Lektion, weil es um die „Anwendungspunkte von @Profile“ geht): Derselbe Bean kann gleichzeitig in mehreren Profilen aktiv gemacht werden, indem man ein Array von Namen übergibt. Zum Beispiel kann UuidOrderIdGenerator sowohl in dev als auch in demo sinnvoll sein, während DeterministicOrderIdGenerator nur in test sinnvoll ist.
package com.example.contextflow.infrastructure.id;
import com.example.contextflow.domain.ports.OrderIdGenerator;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Profile({"dev", "demo"}) // Eine Implementierung ist gleichzeitig in mehreren Profilen aktiv
@Component
public class UuidOrderIdGenerator implements OrderIdGenerator {
@Override
public String nextId() {
// UUID ist praktisch für dev/demo, aber nicht für Tests, wo Determinismus benötigt wird
return java.util.UUID.randomUUID().toString();
}
}
Diese Variante liest sich oft besser als der Versuch, einen „universellen Generator mit if-else darin“ zu erfinden. Und ja, Profile sind hier für den Containerbau: in test wollen wir einen anderen Bean, und er wird ein anderer sein, ohne eine einzige Änderung in den Services.
3. @Profile auf @Configuration
Manchmal bedeutet ein Profil nicht „ein anderer Bean“, sondern einen ganzen Satz von Infrastruktur, der logisch zusammengehört. Zum Beispiel kann der demo-Modus ein Datei-Audit, vorhersagbare Texte, feste Einstellungen und überhaupt „alles, um auf einem Projektor schön zu präsentieren“ umfassen. In solchen Situationen ist es praktisch, @Profile auf eine gesamte Konfigurationsklasse zu setzen: Es ergibt sich ein Ordner „demo runtime“.
Wichtiger Gedanke: @Configuration ist auch eine Komponente (im Sinne des Containers). Wenn das Profil nicht passt, nimmt diese Config-Klasse nicht am Bau teil, und alle ihre @Bean-Methoden sind wie gestrichen.
Zeigen wir ein Beispiel, wo demo ein Datei-Audit enthält. Wir werden jetzt nicht in die Details der Dateiaufzeichnung einsteigen (das ist nicht das Ziel der Lektion), aber wir zeigen, dass Abhängigkeiten und Einstellungen wie gewohnt über @Bean-Methodenparameter und @Value bereitgestellt werden können.
package com.example.contextflow.config.profiles;
import java.nio.file.Path;
import com.example.contextflow.domain.ports.AuditWriter;
import com.example.contextflow.infrastructure.audit.FileAuditWriter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
@Profile("demo") // Diese Config nimmt nur im demo-Profil am Bau teil
@Configuration
public class DemoAuditConfig {
@Bean
AuditWriter auditWriter(@Value("${contextflow.audit.file}") Path auditFile) {
// Pfad holen wir aus den Umgebungs-/Profileinstellungen (application-demo.yml usw.)
return new FileAuditWriter(auditFile);
}
}
Warum ist das manchmal praktischer als @Profile auf der Implementierung FileAuditWriter? Weil FileAuditWriter eine „einfache Infrastrukturklasse“ sein kann und wir möchten, dass die Logik, in welchem Modus er erstellt wird und mit welchen Einstellungen, in der Konfiguration liegt. Das hilft beim Lesen des Projekts: Sie öffnen DemoAuditConfig und sehen „aha, demo enthält Datei-Audit“.
Der Preis der Bequemlichkeit ist auch da: Wenn Sie anfangen, Config-Klassen-Klone zu erzeugen, bei denen sich nur eine Zeile ändert, wird config schnell zu einem „Lagerhaus identischer Kisten“. Daher ist @Profile auf @Configuration ein gutes Werkzeug genau für den Fall, dass ein Profil ein Paket von Lösungen enthält, nicht „dort ändert sich ein Bean ein bisschen“.
4. @Profile auf @Bean-Methoden
Wenn sich nicht ein „Lösungspaket“ ändert, sondern ein oder zwei Punkte, gewinnt meist die Methodebene: Wir lassen die Konfigurationsklasse gemeinsam, machen aber einige @Bean-Methoden bedingt. Das ist wie ein sauberer Schalter direkt an der Stelle, wo der Bean registriert wird, ohne dass ganze @Configuration-Klassen vervielfältigt werden müssen.
Das ist besonders praktisch für Dinge wie NotificationSender, wo die Logik „in Tests nichts senden“ sehr natürlich ist. Ja, wir haben verschiedene Implementierungen (ConsoleNotificationSender, SmsNotificationSender, NoOpNotificationSender), und einen Teil davon wollen wir in bestimmten Modi ganz aus dem Abhängigkeitsgraphen entfernen.
Hier eine minimale Konfiguration: in demo verwenden wir Konsolenbenachrichtigungen, in test NoOp.
package com.example.contextflow.config.core;
import com.example.contextflow.domain.ports.NotificationSender;
import com.example.contextflow.infrastructure.notification.ConsoleNotificationSender;
import com.example.contextflow.infrastructure.notification.NoOpNotificationSender;
import org.springframework.context.annotation.*;
@Configuration
public class NotificationConfig {
@Bean
@Profile("demo") // In demo senden wir „echt“, aber in einfacher Form (in die Konsole)
NotificationSender consoleNotificationSender() {
// Bean-Name standardmäßig = Methodenname: consoleNotificationSender
return new ConsoleNotificationSender();
}
@Bean
@Profile("test") // In Tests schalten wir externe Effekte aus
NotificationSender noOpNotificationSender() {
// NoOp-Implementierung erlaubt es, den Abhängigkeitsgraphen aufzubauen, ohne etwas nach außen zu senden
return new NoOpNotificationSender();
}
}
Beachten Sie einen wichtigen praktischen Aspekt: Der Bean-Name wird standardmäßig gleich dem Methodennamen sein (consoleNotificationSender, noOpNotificationSender). Das rettet manchmal die Lesbarkeit, besonders wenn Sie die Diagnose einschalten und die Liste der Beans sehen.
Und noch ein Aspekt, den man besser vorher merken sollte: Die Methodebene ist gut, solange Sie „einen Punkt der Variabilität“ an einer Stelle halten können. Wenn Sie anfangen, Dutzende bedingter @Beans auf zufällige Configs zu verteilen, erhalten Sie wieder die „Baukomplexität“, nur diesmal in der Konfiguration.
Wahl des Ortes für @Profile
An diesem Punkt taucht meist die Frage auf: „Wie macht man es richtig?“. Die gute Nachricht: In Spring ist das wirklich eine Frage der Lesbarkeit, nicht „eine Variante funktioniert, die andere ist kaputt“. Die schlechte Nachricht: Wenn Sie schlecht wählen, erhalten Sie eine Konfiguration, die Sie zu berühren fürchten (und das ist fast schon ein Produktionsszenario, nur ohne Gehalt).
Fassen wir den Vergleich in einer Tabelle zusammen, damit das Gehirn nicht alles im Arbeitsspeicher behalten muss:
| Wo setzen wir @Profile | Wann ist es praktisch | Was gewinnen wir | Womit riskieren wir |
|---|---|---|---|
| Auf der Implementierung (@Component/@Service) | Die Klasse existiert wirklich nur in einem/zwei Modi | Sehr einfach, nah am Code | Die Profillogik verteilt sich über das Projekt; nicht geeignet für externe Klassen |
| Auf der @Configuration-Klasse | Profil enthält ein Lösungspaket (Gruppe von Beans) | Die gesamte Moduslogik an einem Ort | Man kann anfangen, Konfigurationen für kleine Unterschiede zu klonen |
| Auf der @Bean-Methode | Ein Bean unterscheidet sich innerhalb einer gemeinsamen Config | Punktuell und sauber, ohne Copy-Paste | Leicht, die Config in einen „Weihnachtsbaum aus Bedingungen“ zu verwandeln, wenn man übertreibt |
Im selben Layout ist es selten sinnvoll, alle drei Punkte gleichzeitig zu nutzen. Meist gewinnt die Variante, die den konkreten Bau des Projekts am lesbarsten macht.
Wenn man eine Faustregel angeben soll, dann diese: Ein Profil auf der Komponente ist gut für einfache Infrastrukturimplementierungen, ein Profil auf der Config-Klasse ist gut für „Lösungspakete“, und ein Profil auf der @Bean-Methode ist gut, wenn sich ein Punkt ändert und Sie die gemeinsame Konfiguration als Einheit behalten möchten.
Und noch ein sehr wichtiger Punkt zum Stil des Projekts ContextFlow: Versuchen Sie, Profile in den Infrastruktur- und Konfigurationsschichten zu halten. Wenn Profile im application.service auftauchen, bedeutet das normalerweise, dass Sie den Umgebungsmodus versehentlich mit der Geschäftslogik vermischt haben. Profile handeln von „wie man die Anwendung zusammenbaut“, und Geschäft von „wie man eine Bestellung verarbeitet“.
5. Mini-Inkrement in ContextFlow
Lassen Sie uns alles mit unserem Projekt verbinden, sodass das Wesentliche sichtbar wird: Die Services bleiben gleich, der Container setzt verschiedene Implementierungen ein. Das ist „Profil als Bau“ und nicht „Profil als if-else im Code“. Wenn Sie nach dem Einschalten von Profile das Bedürfnis haben, OrderPlacementService umzuschreiben, sind Sie wahrscheinlich in die falsche Richtung abgebogen.
Nehmen wir an, AuditService hängt von AuditWriter über den Konstruktor ab (wie wir es gelernt haben). In ihm gibt es kein und soll es kein if ("demo") ... geben.
package com.example.contextflow.application.service;
import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.stereotype.Service;
@Service
public class AuditService {
private final AuditWriter auditWriter;
public AuditService(AuditWriter auditWriter) {
// Dem Service ist es egal, welche Implementierung kommt: Konsolen-, Datei-, NoOp
this.auditWriter = auditWriter;
}
public void audit(String message) {
// Die ganze „Profiligkeit“ lebt außerhalb des Services – in der Konfiguration/Infrastruktur
auditWriter.write(message);
}
}
Danach haben Sie die Wahl, wo Sie die Profilvariabilität platzieren. Zum Beispiel Konsolen-Audit als profilierte Komponente für dev, Datei-Audit als profilierte @Configuration für demo. Und diese Lösungen können friedlich koexistieren: Spring verlangt nicht, „einen Stil für die ganze Anwendung zu wählen“, es verlangt Vorhersagbarkeit.
Wenn Sie die Struktur sorgfältig in Pakete gelegt haben, wird das Bild auch für Ihr zukünftiges Ich lesbar (und Ihr zukünftiges Ich ist der toxischste Reviewer, glauben Sie mir). Zum Beispiel so:
com.example.contextflow
├── application.service
├── domain.ports
├── infrastructure.audit
├── infrastructure.notification
├── config.core
└── config.profiles
In config.core liegen allgemeine Konfigurationen (z.B. NotificationConfig), und in config.profiles profil-spezifische Konfigurationen wie DemoAuditConfig. Dabei ist nicht wichtig, was genau Sie gewählt haben: @Profile auf der Komponente oder auf der @Bean-Methode. Wichtig ist, dass die Services nichts von den Profilen „wissen“ und der Container nicht versucht, ein bereits erstelltes Objekt „magisch umzuschalten“.
Das Schönste an diesem Modell ist, dass der Container ehrlich bleibt: In jedem Profil muss er einen vollständigen Abhängigkeitsgraphen zusammenbauen. Wenn Sie AuditWriter für einen Modus vergessen haben, erfahren Sie das beim Start. Das ist der seltene Fall, wo ein Anwendungsabsturz eine „Hilfe“ und keine „Katastrophe“ ist.
6. Typische Fehler bei der Verwendung von @Profile
Fehler Nr. 1: Erwartung, dass @Profile einen bereits erstellten Bean während der Laufzeit „umschaltet“.
Manchmal werden Profile als Kippschalter in den Einstellungen wahrgenommen: „Jetzt läuft die Anwendung im dev-Modus, und ich möchte spontan demo aktivieren.“ In Spring sind Profile Teil des Kontextaufbaus. Wenn ein Bean nicht registriert ist, existiert er nicht. Ist er registriert, bleibt er bis zum Ende der Lebensdauer dieses ApplicationContext so.
Fehler Nr. 2: In einem Profil ist der Bean vorhanden, in einem anderen vergessen, und der Kontext stürzt ab.
Das ist eine superhäufige Situation bei Anfängern: Man macht @Profile("demo") für FileAuditWriter, startet dev, und plötzlich kann AuditService nicht starten. Das ist nicht „Spring ist launisch“, sondern Sie haben den Container gebeten, die Anwendung ohne eine notwendige Komponente zusammenzubauen. Die Lösung ist einfach: In jedem Modus muss ein vollständiger Satz notwendiger Abhängigkeiten vorhanden sein.
Fehler Nr. 3: Verwendung von Profilen, wo eine normale Kandidatenauswahl nötig ist.
Wenn gleichzeitig mehrere Implementierungen existieren sollen (z.B. mehrere NotificationSender als Katalog von Strategien), sind Profile nicht geeignet. Ein Profil ist „Bean ein-/ausschalten“ als Teil des Umgebungsbaus. Die Auswahl einer Implementierung innerhalb einer bereits laufenden Anwendung ist die Geschichte von @Qualifier, @Primary oder Map<String, T>, die wir bereits behandelt haben.
Fehler Nr. 4: Klonen großer Config-Klassen für einen einzigen Unterschied.
Es gibt die Versuchung: „Aha, ich mache DevConfig, DemoConfig, TestConfig – und in jeder werden 30 @Bean-Methoden stehen.“ Und dann ändern Sie eine gemeinsame Abhängigkeit und kopieren die Korrektur an drei Stellen (und vergessen natürlich eine). Wenn sich nur ein Bean unterscheidet, ist es oft besser, @Profile auf der @Bean-Methode zu verwenden, anstatt ganze Config-Klone zu erzeugen.
Fehler Nr. 5: Profile sind über das ganze Projekt verteilt, und niemand versteht, wo der Modus eingeschaltet wird.
Wenn @Profile auf Dutzenden von Klassen in verschiedenen Paketen ohne Struktur steht, wird das Lesen des Bau-Modus schwer. Versuchen Sie, „Modusentscheidungen“ nah an der Konfiguration und Infrastruktur zu halten, damit sie logisch auffindbar sind: entweder in config.profiles oder in offensichtlichen Infrastrukturimplementierungen (NoOp... für test).
GO TO FULL VERSION