1. Aware als scharfes Werkzeug
Aware-Callbacks haben bereits gezeigt, welche Dienstinformationen der Container an einen Bean übergeben kann. Jetzt ist etwas anderes wichtiger: wo das wirklich sinnvoll ist und wo Aware nur versteckte Abhängigkeiten tarnt und uns zurück zum Service-Locator-Geruch (smell) bringt.
Von allen Aware-Mechanismen ist ApplicationContextAware meist der verführerischste: Man erhält den Kontext und scheint keine Abhängigkeiten mehr ehrlich auflisten zu müssen. Genau hier wird Aware am leichtesten von einem Infrastruktur-Callback zu einer schlechten DI-Umgehung. Aware ist wirklich mächtig, aber genau deshalb gefährlich.
Der Schlüsselgedanke hier ist: Aware ist kein Weg, um „bequem Abhängigkeiten einzufügen“, sondern ein Weg, dem Bean Informationen über den Container und die Umgebung zu geben. Das heißt, der Bean sagt zum Container: „Ich muss etwas über dich wissen.“ Und wenn diese Anfrage aus einer Geschäftsklasse (Use-Case-Service) kommt, bedeutet das meistens nicht, dass die Klasse „sehr schlau ist“, sondern dass die Verantwortungsgrenzen verschwommen sind.
Wenn wir es auf eine Alltagsanalogie reduzieren: Constructor Injection ist wie eine Einkaufsliste – Sie sagen genau, was Sie brauchen, und es wird Ihnen gebracht. Aware ist wie die Schlüssel zum Lager – Sie können hingehen und nehmen, was immer Sie wollen. Die Schlüssel zum Lager werden manchmal benötigt (für Sicherheit, Inventur, Diagnose), aber wenn sie jedem ausgehändigt werden, der Brot holen kommt, wird die Ordnung im Lager schnell zur Legende.
2. Framework-Leakage im Geschäftscode
Framework-Leakage sieht in diesem Thema sehr konkret aus: Eine Geschäftsklasse beginnt von Spring-spezifischen APIs abzuhängen, so dass sie ohne Container nicht mehr denkbar ist. Meistens sind es ApplicationContext oder BeanFactory innerhalb eines Use-Case-Service und getBean() innerhalb einer Geschäftsmethode. Formal funktioniert der Code, aber die Abhängigkeit wird versteckt.
Das Unangenehme ist, dass Leakage fast nie mit den Worten beginnt: „Jetzt mache ich die Anwendung schlecht.“ Normalerweise klingt der Anlass harmlos: eine Implementierung je nach Modus wählen, eine Property lesen, manchmal einen anderen Bean holen. Aber sobald die Hand nach ApplicationContextAware greift, macht man einen Schritt vom DI-Denken zu einem „Mini-Container innerhalb des Service“.
Es gibt auch einen zweiten Effekt: Sie verlieren das „Fail-Fast“-Prinzip. Wenn die Abhängigkeiten im Konstruktor stehen, überprüft Spring das Wiring beim Start und stürzt sofort ab, wenn etwas fehlt. Wenn Sie getBean() innerhalb einer Methode aufrufen, kann der Fehler mitten im Szenario auftauchen, bei der dritten Bestellung, genau dann, wenn Sie am liebsten nur den Bericht ausdrucken und nach Hause gehen möchten.
3. ApplicationContextAware und der „Mini-Container“ im Service
Stellen wir uns eine typische „harmlose“ Situation vor. Wir haben einen Service, der eine Bestellung aufgibt, und er benötigt eine Auditierung. Jemand entscheidet: „Warum AuditWriter in den Konstruktor ziehen? Ich bin doch in Spring, ich hole ihn aus dem Kontext – flexibel und elegant.“ An dieser Stelle seufzt Spring leise, weil sein DI-Modell gerade als Service Locator missbraucht wird.
Hier ist ein minimales Beispiel, wie ein solcher Code aussieht:
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class BadOrderPlacementService implements ApplicationContextAware {
// Nicht das Feld an sich ist schlecht, sondern dass es die "Schlüssel zum Lager" sind:
// Der Service erhält Zugriff auf den gesamten Container.
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext context) {
// Der Container reicht den Kontext hier selbst herein (das ist "Aware").
this.context = context;
}
public void placeOrder(Order order) {
// Versteckte Abhängigkeit: AuditWriter ist nirgends in der Signatur der Klasse/des Konstruktors sichtbar.
AuditWriter auditWriter = context.getBean(AuditWriter.class);
// Textblock macht die Vorlage lesbarer als String-Konkatenation.
String message = """\
erstellt %s""".formatted(order.getId());
// Der Aufruf des Audit wirkt "harmlos", aber die Abhängigkeit wird zur Laufzeit bezogen.
auditWriter.write(message);
}
}
Auf den ersten Blick ist der Konstruktor „sauber“: Es gibt gar keinen, also scheint es keine Abhängigkeiten zu geben. Aber das ist eine Illusion. Die Methode placeOrder() hat sehr wohl eine echte Abhängigkeit – AuditWriter. Sie ist nur versteckt. Wenn Sie die Datei öffnen, können Sie an der Signatur nicht erkennen, dass der Service eine Auditierung benötigt. Sie müssen den Methodenrumpf lesen (und manchmal auch mehrere if-Verzweigungen, weil sich getBean() schnell vermehrt).
Stellen Sie sich nun einen Test vor. Bei Constructor Injection machen Sie einfach new OrderPlacementService(fakeAuditWriter) und überprüfen das Verhalten. Hier hingegen müssen Sie entweder den Spring-Kontext hochfahren oder einen Mock von ApplicationContext schreiben und konfigurieren, dass er den gewünschten Bean zurückgibt. Sie haben also das Testen erschwert, nur um … einen Konstruktorparameter nicht schreiben zu müssen.
Es gibt noch eine Nuance: Solcher Code führt uns oft zu dem Problem zurück, von dem wir schon vor Tagen bei DI weggekommen sind. Wir erhalten wieder einen versteckten Abhängigkeitsgraph. In Plain Java war es ein „internes new“, hier ist es ein „internes getBean()“. Andere Worte, ähnlicher Schmerz.
Um es sich visuell zu veranschaulichen, kann man den Ablauf so darstellen:
%% Zeigt, dass die Abhängigkeit innerhalb der Geschäftsmethode bezogen wird, nicht vorab vom Container gesammelt.
flowchart LR
A["placeOrder()"] --> B["ApplicationContext.getBean(AuditWriter)"]
B --> C["AuditWriter.write(...)"]
Das Hauptwarnsignal ist hier nicht, dass der Aufruf komplex ist, sondern dass der Service beginnt, die Rolle eines „Abhängigkeitssuchers“ zu spielen. Der DI-Container soll den Graphen von außen zusammenstellen, nicht den Geschäftsmethoden die Möglichkeit geben, ihn „on the fly“ zusammenzustellen.
4. Alternativen zu Aware im Geschäftscode
Fast immer taucht Aware in einer Geschäftsklasse nicht aus böser Absicht auf, sondern aus dem Wunsch, eine von drei Aufgaben zu lösen: einen Collaborator zu erhalten, eine Einstellung zu bekommen oder ein Verhalten in Abhängigkeit von der Umgebung zu wählen. Die gute Nachricht ist, dass alle drei Aufgaben ohne ApplicationContextAware gelöst werden können – und die Lösungen sind in der Regel einfacher, transparenter und besser testbar. Und ja, dies ist ein Fall, in dem „einfacher“ nicht „schlechter“ bedeutet.
Um nicht alles in der Luft hängen zu lassen, halten wir das in einer kleinen Tabelle fest (sie ersetzt ein Dutzend „nützlicher“ Aufzählungspunkte und schont das Gehirn):
| Was Sie im Geschäftscode benötigen | Häufige schlechte Idee | Normale Idee | Warum ist das besser |
|---|---|---|---|
| Ein anderes Objekt/Service benötigt | ApplicationContext.getBean() | Constructor Injection | Abhängigkeiten sichtbar, Wiring fail-fast, Tests einfacher |
| Eine einzelne Einstellung benötigt | EnvironmentAware | @Value(...) | Weniger Spring-API im Geschäft, klare Konfiguration |
| Unterschiedliches Verhalten je nach Modus benötigt | if (profile == ...) im Service | Profile/bedingte Container-Assembly | Auswahl erfolgt auf Assembly-Ebene, nicht in der Geschäftslogik |
Als Nächstes besprechen wir jeden Fall ruhiger und anhand von kleinem Code.
Wenn ein Collaborator benötigt wird – machen Sie die Abhängigkeit explizit über den Konstruktor
Der beliebteste Grund, ApplicationContext in einen Service zu ziehen, ist: „Ich brauche noch einen Bean, will aber den Konstruktor nicht erweitern.“ Klingt logisch … bis Sie daran denken, dass der Konstruktor genau die Deklaration des Klassenvertrags ist. Wenn eine Klasse nicht bereit ist, ehrlich zu sagen, was sie braucht, wird sie zu einer „Black Box mit Überraschungen“, und solche Überraschungen sind meistens nicht aus Schokolade.
Beispiel eines „ehrlichen“ Ansatzes:
public class OrderPlacementService {
// Die Abhängigkeit ist sofort sichtbar: Der Service benötigt eine Auditierung.
private final AuditWriter auditWriter;
public OrderPlacementService(AuditWriter auditWriter) {
// Der Container (oder Test) muss den Collaborator explizit bereitstellen.
this.auditWriter = auditWriter;
}
public void placeOrder(Order order) {
// Die Geschäftsmethode sucht die Abhängigkeit nicht mehr – sie verwendet sie einfach.
String message = """\
erstellt %s""".formatted(order.getId());
auditWriter.write(message);
}
}
Beachten Sie: Der Leser der Klasse sieht jetzt auf einen Blick, dass für die Auftragserteilung eine Auditierung benötigt wird. Der Container kann das Wiring beim Start überprüfen (und sofort stürzen, wenn AuditWriter nicht registriert ist). Auch der Test ohne Spring wird zum Kinderspiel: Fake/Stub eingesetzt – los geht’s.
Und noch ein psychologischer Bonus: Wenn der Konstruktor wächst, signalisiert er ehrlich, dass die Klasse möglicherweise aufgebläht ist. Das ist kein „Problem des Konstruktors“, sondern ein Hinweis für die Architektur. ApplicationContextAware unterdrückt diesen Hinweis hingegen wie einen Wecker am Montagmorgen.
Wenn eine einzelne Einstellung benötigt wird – injizieren Sie den Wert, nicht die gesamte Environment
Der zweite typische Grund, Aware zu verwenden, ist: „Ich muss das aktive Profil erfahren“ oder „Ich brauche eine Property“. Und hier kann man sehr leicht einen Kühlschrank für eine einzige Wurst transportieren: den gesamten Environment in den Service schleppen (oder ihn über EnvironmentAware bekommen), obwohl eigentlich nur ein String benötigt wird.
Wenn eine einzelne Einstellung benötigt wird, ist es einfacher und ehrlicher, sie explizit zu übergeben:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ReportOutputManager {
// In die Geschäftsklasse kommt bereits ein konkreter Wert, nicht die gesamte Environment.
private final String outputDir;
public ReportOutputManager(@Value("${contextflow.report.output-dir}") String outputDir) {
// Diese Stelle ist die Grenze: Der Container hat den Wert zusammengestellt, die Klasse nimmt ihn einfach entgegen.
this.outputDir = outputDir;
}
}
In diesem Beispiel schleppen wir nicht Environment in die Klasse, rufen nicht environment.getProperty(...) auf und binden die Geschäftslogik nicht an Profile. Die Klasse erhält einen bereits „fertigen“ Wert. Der Container ist der Ort, an dem Abhängigkeiten und Werte zusammengestellt werden, die Klasse der Ort, an dem sie angewendet werden.
Wenn mehrere Einstellungen zusammenhängen, ist es oft bequemer, anstelle von Environment ein kleines Konfigurationsobjekt (einen normalen Bean) zu erstellen und dieses zu injizieren. Dann hängt der Geschäftscode von Ihrem eigenen Typ ab, nicht von der Spring-API. Das ist ein kleiner Schritt zu einer saubereren Architektur, jedoch ohne „magische“ Mechanismen.
Wenn eine umgebungsabhängige Logik benötigt wird – verlagern Sie die Auswahl in die Container-Assembly
Der dritte beliebte Fall: „Im Demo-Profil möchte ich einen AuditWriter, im Dev einen anderen. Lassen wir den Service direkt das Profil prüfen.“ Normalerweise sieht das so aus: Der Service erhält Environment, macht ein if und wählt eine der Implementierungen aus. Das Problem ist, dass Sie Geschäftscode bekommen, der etwas über Profile weiß. Profile sind jedoch ein Mechanismus der Anwendungsassembly, keine Fachlogik.
Ein ehrlicherer Ansatz: Lassen Sie den Container selbst die benötigte Implementierung auswählen, und der Service arbeitet mit einem einzigen AuditWriter. Zum Beispiel über @Profile in der Konfiguration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class AuditConfig {
@Bean
@Profile("demo")
AuditWriter fileAuditWriter() {
// Im Demo-Profil wird der Container die Dateiimplementierung zusammenstellen.
return new FileAuditWriter();
}
@Bean
@Profile("!demo")
AuditWriter consoleAuditWriter() {
// In allen anderen Profilen wird es die Konsolenimplementierung geben.
return new ConsoleAuditWriter();
}
}
Jetzt weiß die Geschäftsklasse nicht, welches Profil aktiv ist. Sie weiß nur: „Ich habe einen AuditWriter“. Und das ist genau die Einfachheit, wegen der es DI-Container überhaupt gibt.
5. Wo Aware angemessen ist: Support-Schicht und Adapter
Es ist wichtig, diese Lektion nicht in „Aware ist böse, verbieten, verbrennen“ zu verwandeln. Das ist nicht der Fall. Aware ist ein normaler Infrastrukturmechanismus, wenn ein Bean von Natur aus eine Dienstaufgabe erfüllt: Diagnose, Anpassung an die Umgebung, Integration mit dem Container. Das Problem entsteht nicht im Werkzeug selbst, sondern im Einsatzort. Ein Hammer ist auch nützlich – solange Sie nicht versuchen, damit eine Telefonnummer zu wählen.
Ein klassischer „guter“ Kandidat ist ein Diagnose-Bean, der beim Start ausgeben kann: welchen Namen er im Container hat, welche Profile aktiv sind, und Ihnen so hilft zu verstehen, dass die Anwendung wirklich im erwarteten Modus gestartet ist. Ein solcher Bean lebt in support.diagnostics, und seine öffentliche API ist normalerweise sehr klein: etwa summary() oder dump().
Ein Beispiel eines „sicheren“ Aware-Beans sieht etwa so aus:
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
public class ContextDiagnosticsBean implements BeanNameAware, EnvironmentAware {
// Dienstdaten: Bean-Name und Umgebung – das sind Informationen "über den Container".
private String beanName;
private Environment environment;
@Override
public void setBeanName(String name) {
// Spring teilt dem Bean seinen Namen mit: praktisch für Logs und Diagnose.
this.beanName = name;
}
@Override
public void setEnvironment(Environment environment) {
// Spring übergibt die Environment: Der Bean erhält Informationen über Profile und Eigenschaften.
this.environment = environment;
}
public String summary() {
// Hier gibt es kein getBean() und keine Geschäftsverzweigungen – nur Diagnose.
return """\
%s Profile=%s""".formatted(beanName, String.join(",", environment.getActiveProfiles()));
}
}
Beachten Sie, was hier nicht vorkommt: kein getBean() innerhalb von Geschäftsmethoden, keine Auswahl von Geschäftszweigen nach Profil, kein „Suchen von Abhängigkeiten im laufenden Betrieb“. Der Bean erhält die Dienstdaten einmal und erstellt daraus eine menschenlesbare Beschreibung. Das ähnelt der Fahrzeugdiagnose: „Temperatur, Drehzahl, Tankfüllstand“. Sie sollte nicht selbst entscheiden müssen, wohin das Auto fährt.
Und dieser Ansatz passt sehr gut zur Paketdisziplin, die wir in ContextFlow bereits verfolgen: Geschäftslogik in domain/application, Infrastruktur in infrastructure, Container-„Innereien“ in support und config. Sobald Aware in application.service auftaucht, ist das fast immer ein Grund innezuhalten und zu fragen: „Was behandeln wir hier – ein echtes Problem oder die Abneigung, das Wiring transparent zu machen?“
6. Mini-Diagnose von Leakage beim Code-Review
In einem echten Projekt schreibt niemand auf ein Schild: „Diese Klasse – Framework Leakage“. Normalerweise erkennt man es an Kleinigkeiten: an Imports, an Konstruktorsignaturen, an seltsamen Aufrufen innerhalb von Methoden. Die gute Nachricht: Solche Kleinigkeiten kann man fast automatisch erkennen, sobald man weiß, wonach man suchen muss. Die schlechte Nachricht: Anfangs wird man sie auch im eigenen Code finden. Das ist normal, wir alle sind durch diese Phase gegangen.
Es gibt einige sehr praktische Signale, die als „Schnellscanner“ gut funktionieren:
| Signal | Warum ist es verdächtig | Was sich fragen |
|---|---|---|
| implements ApplicationContextAware im Service | Service hat Zugriff auf den Container erhalten | „Warum reicht mir Constructor Injection nicht?“ |
ApplicationContext/ im Konstruktor einer Geschäftsklasse |
versteckter Service Locator | „Welche echten Abhängigkeiten verstecken sich hinter dem Kontext?“ |
| environment.getProperty(...) in einer Use-Case-Methode | Geschäftslogik beginnt von der Konfiguration zu leben | „Warum ist das nicht @Value oder ein Konfigurations-Bean?“ |
| getBean() innerhalb von placeOrder()/cancelOrder()/generateReport() | Abhängigkeit wurde zur Laufzeitsuche | „Ist es nicht einfacher, die Auswahl auf Wiring-Ebene zu verlagern?“ |
Wenn es noch einfacher sein soll, können Sie den „Herausziehtest“ verwenden. Stellen Sie sich vor, Sie möchten die Klasse mit einem normalen Unit-Test testen (ohne Spring). Wenn die Klasse ApplicationContext erfordert, müssen Sie entweder die Hälfte der Spring-API mocken oder aufgeben und den Container hochfahren. Und genau dieses „Aufgeben“ ist oft ein Zeichen dafür, dass die Klasse zu viele Rollen erfüllt: Sie ist sowohl Geschäftsorchestrator als auch Container-integrierte Komponente zugleich.
Eine weitere gute Frage klingt etwas philosophisch, funktioniert aber: „Wenn wir morgen das Wiring auf einen anderen DI-Container umschreiben (oder sogar auf einen manuellen Composition Root), bleibt diese Klasse dann sinnvoll?“ Für eine Geschäftsklasse sollte die Antwort „ja, fast ohne Änderungen“ lauten. Für einen diagnostischen Support-Bean – „nein, und das ist in Ordnung“. Und genau hier verläuft die Grenze.
7. Typische Fehler beim Umgang mit Aware
Fehler mit Aware sind keine „Anfängerfehler“, sondern eine natürliche Reaktion auf ein mächtiges Werkzeug. Spring gibt tatsächlich die Möglichkeit, den Container zu erhalten, und das Gehirn versucht automatisch, dies zu einem universellen Problemlösungsmechanismus zu machen. Daher ist das Folgende keine Liste von „Wie man es nicht machen sollte“, sondern vielmehr eine Sammlung typischer Szenarien, bei denen man innehalten und das DI-Denken wiederherstellen sollte.
Fehler Nr. 1: ApplicationContextAware verwenden, um „den Konstruktor nicht erweitern zu müssen“.
Meistens ist es einfach der Wunsch, den Code an einer Stelle kürzer zu machen, aber auf Kosten der Lesbarkeit des gesamten Projekts. Wenn eine Abhängigkeit wirklich benötigt wird, soll sie im Konstruktor stehen. Der Konstruktor beißt nicht, er ist einfach ehrlich. Versteckte Abhängigkeiten beißen dagegen meist im unpassendsten Moment.
Fehler Nr. 2: getBean() innerhalb von Geschäftsmethoden aufrufen.
Das Problem ist nicht einmal getBean() selbst, sondern dass Sie die Geschäftsmethode zu einem Ort der Zusammenstellung von Abhängigkeiten machen. Das bricht das Fail-Fast-Verhalten: Die Anwendung kann starten und erst beim ersten echten Aufruf eines Szenarios abstürzen. Außerdem wird das Testen einer solchen Methode ohne Container unangenehm: Sie müssen die Arbeit des Kontextes emulieren, anstatt die Geschäftslogik zu testen.
Fehler Nr. 3: Environment in den Service ziehen „wegen einer einzigen Property“.
Environment ist eine nützliche Sache, aber es ist immer noch die Spring-API. Wenn eine einzelne Einstellung benötigt wird, injizieren Sie den konkreten Wert über @Value (oder einen separaten Konfigurations-Bean). Wenn der Service selbst Properties liest, beginnt er, Fragen der Anwendungsassembly zu lösen – das ist nicht seine Rolle.
Fehler Nr. 4: Geschäftsverzweigungen basierend auf dem aktiven Profil durchführen.
Profile sind ein Mechanismus, um zu bestimmen, „welche Beans in diesem Lauf überhaupt existieren“, nicht „welcher Zweig im Geschäftsalgorithmus gewählt werden soll“. Wenn innerhalb von audit() oder sendNotification() ein if (demo) ... else ... auftaucht, modellieren Sie höchstwahrscheinlich Konfiguration als Geschäftslogik. Richtig wäre, verschiedene Implementierungen auf Profile zu verteilen und eine vom Container ausgewählte zu injizieren.
Fehler Nr. 5: Aware aus der Support-Schicht in application.service verlagern „weil es so schneller geht“.
Das ist der klassische Weg, auf dem ein kleines Projekt innerhalb eines Monats zu einer Ansammlung von Klassen wird, von denen jede „ein bisschen etwas über Spring weiß“. Später versuchen Sie zu verstehen, warum die Änderung einer einzigen Einstellung das halbe Projekt kaputt macht, und stellen fest, dass die Hälfte der Services die Umgebung direkt liest. Es ist besser, Container-Mechaniken von vornherein lokalisiert zu halten: support und config – ja, application und domain – nein.
GO TO FULL VERSION