1. Scope in Spring: Bedeutung und lifetime
Scope wird oft zum ersten Mal als Zeile @Scope("prototype") gesehen und als Schalter „ein Objekt oder viele Objekte“ wahrgenommen. Das stimmt zwar … aber es ist eine „ärgerlich oberflächliche“ Sichtweise. In der Praxis ist scope ein Vertrag zwischen Ihnen und dem Container darüber, welche lifetime ein Bean hat und an welcher Stelle dieser Bean überhaupt sinnvoll ist. Wenn wir Scopes ignorieren, landen wir meistens entweder bei merkwürdigen Workarounds („Lass uns alles von Hand neu erstellen“) oder bei verstecktem Zustand in Singletons (und wundern uns dann: „Warum hat es sich von selbst geändert?“).
Formulieren wir scope „erwachsen“, aber in menschlicher Sprache. Scope beantwortet zwei Fragen gleichzeitig. Erstens: Wie viele Instanzen dieses Beans wird es für den Container geben? Zweitens: Wie lange wird der Container diese Instanzen als „seine eigenen“ betrachten und verwalten? Das heißt, scope geht es nicht um schönen Code, sondern um das Lebenszeitmodell eines Objekts in der Anwendung.
Um das Bild zu festigen, ist es hilfreich, das folgende Schema im Kopf zu behalten:
flowchart TD
A[ApplicationContext] --> B[BeanDefinition Name + Typ + Scope + Metadaten]
B -->|singleton| C[Eine Instanz pro Kontext]
B -->|prototype| D[Neue Instanz bei jeder Anfrage an den Container]
Heute besprechen wir zwei grundlegende Scopes, die Sie jetzt schon brauchen: singleton und prototype. Sie bilden das Fundament, auf das später die anderen, einschließlich der Web-Scopes, aufbauen.
2. Scope als Eigenschaft von BeanDefinition
Am Anfang denkt man am einfachsten: „Es gibt eine Klasse AuditService, also ist sie singleton“. Aber Spring denkt anders: Der Container verwaltet keine Klassen, sondern Bean-Definitionen. Die Klasse ist nur „das, was erstellt werden muss“. Aber „wie viele erstellt werden und wie sie leben“ – das sind Metadaten der BeanDefinition. Daher kann dieselbe Java-Klasse streng genommen auf unterschiedliche Weise im Container registriert werden (obwohl das im echten Leben selten und nur mit Bedacht gemacht wird).
Warum ist das wichtig? Weil es eine schädliche Illusion beseitigt: dass scope so etwas wie final oder static sei, also für immer an den Typ gebunden. Nein, scope wird im Moment der Bean-Registrierung gewählt, und die Registrierung kann auf verschiedene Weise erfolgen: über @Component/@Service (Scanning), über @Bean in der Konfiguration, über XML (irgendwann später, keine Panik).
Stellen Sie sich eine alltägliche Situation vor. Sie haben einen „Typ“ – zum Beispiel OrderProcessingSession. Er kann ein „Einweg-Notizblock für einen Vorgang“ sein. Oder ein „gemeinsames Logbuch, in das wir alles schreiben“. Der Typ ist derselbe (Papier und Papier), aber Sinn und Lebensdauer sind unterschiedlich. In Spring wird das dadurch ausgedrückt, dass scope ein Teil der Bean-Definition ist, nicht die DNA der Klasse.
Und noch ein kleiner, aber praktischer Gedanke. Wenn Sie die Projektkonfiguration lesen (und wir wollen in ContextFlow in der Lage sein, sie zu lesen), ist es hilfreich, sich im Geiste zu fragen: „Soll dieser Bean lange leben (wie ein Service) oder kurz (wie ein Objekt eines bestimmten Vorgangs)?“ Die Antwort darauf verrät fast immer den passenden scope.
3. singleton scope: Standardverhalten
Singleton ist der Standard-Scope. Das heißt, wenn Sie überhaupt nichts schreiben, betrachtet Spring den Bean „stillschweigend“ als singleton. Und in einem normalen Projekt ist das … gut. Denn die meisten Dienste sind wie eine Kaffeemaschine im Büro: Es gibt eine, und wir bauen nicht für jede Kleinigkeit eine neue Kaffeemaschine, sondern nutzen einfach dieselbe, solange das Büro existiert.
In Container-Begriffen bedeutet singleton: Innerhalb eines ApplicationContext existiert eine Instanz dieses Beans, und bei jeder Anfrage gibt der Container genau diese zurück. Wichtig ist, dass es „eine pro Kontext“ ist, nicht „eine pro JVM, Planet und Multiversum“. Wir haben den Unterschied zwischen Spring-Singleton und GoF-Singleton bereits besprochen, daher heute nur eine kurze Erinnerung: Spring verlangt von Ihnen kein private static final INSTANCE.
Sehen wir uns das im Code ganz direkt an. Nehmen wir zum Beispiel unseren Audit-Service (in ContextFlow gibt es ihn als AuditService). Standardmäßig wird er ein singleton sein:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Service;
@Service
class AuditService {
// Standardmäßig Singleton: Der Container erstellt eine Instanz pro ApplicationContext
}
class Demo {
public static void main(String[] args) {
// Starten wir einen minimalen Kontext nur mit einer Komponente
try (var ctx = new AnnotationConfigApplicationContext(AuditService.class)) {
// Zweimal denselben Bean vom Container anfordern
AuditService first = ctx.getBean(AuditService.class);
AuditService second = ctx.getBean(AuditService.class);
// Bei Singleton ist es dasselbe Objekt
System.out.println(first == second); // true
}
}
}
Hier geht es nicht um den Check == an sich (obwohl er ehrlich „dasselbe Objekt“ zeigt), sondern um die Bedeutung: Der Container hat das AuditService erstellt, gespeichert und gibt dann dieselbe Instanz zurück.
Eine weitere Nuance, die sich gut mit dem gestrigen Tag über Lifecycle verbinden lässt. Singleton-Beans (wenn sie nicht lazy sind) werden normalerweise beim Start des Kontexts erstellt, und beim Schließen des Kontexts ruft der Container die Destroy-Callbacks auf (@PreDestroy, destroyMethod usw.). Das heißt, der lifecycle eines Singletons ist meistens „vollständig“: Der Container hat es nicht nur erstellt, sondern auch ordnungsgemäß beendet.
4. prototype scope: Neue Instanz bei jeder Anfrage an den Container
Prototype ist der zweite grundlegende Scope, der wirklich in Anwendungen benötigt wird, in denen es kurzlebige Objekte gibt. Die Bedeutung von prototype klingt einfach: Der Container erstellt jedes Mal eine neue Instanz des Beans, wenn Sie diesen Bean beim Container anfordern. Das heißt, ctx.getBean(...) gibt ein Objekt zurück, ein zweites getBean(...) gibt ein anderes Objekt zurück, und das ist normal, so ist es beabsichtigt.
Das ist wie ein Einwegbecher im Café: Sie waschen und wiederverwenden ihn nicht „für den ganzen Tag“, sondern nehmen für jede Portion einen neuen. In der Anwendung sind solche „Becher“ Objekte, die den Zustand eines einzigen Vorgangs tragen: einer Bestellverarbeitung, einer Berichtsgenerierung, eines kurzen Skriptschritts.
Ein minimales Beispiel sieht so aus:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype") // Bitten den Container, bei jedem getBean(...) eine neue Instanz zu erstellen
class OrderProcessingSession {
// Typischer Prototype: Zustandsobjekt für einen einzigen Vorgang/ein Skript
}
class Demo {
public static void main(String[] args) {
// Der Kontext wird gestartet und kennt eine Komponente
try (var ctx = new AnnotationConfigApplicationContext(OrderProcessingSession.class)) {
// Jede Anfrage an den Container erstellt eine neue Instanz des Prototype-Beans
OrderProcessingSession first = ctx.getBean(OrderProcessingSession.class);
OrderProcessingSession second = ctx.getBean(OrderProcessingSession.class);
System.out.println(first == second); // false
}
}
}
Nun ein wichtiger Vorbehalt, den Anfänger fast immer übersehen (und dann leiden). Prototype bedeutet „neu bei jeder Anfrage an den Container“, aber nicht „neu bei jedem Methodenaufruf eines Singleton-Service“. Wenn Sie einen Prototype einmal erhalten und in einem Feld speichern, ist er kein „Einwegbecher“ mehr, sondern wird zu einem „Becher, den Sie eine Woche lang mit sich herumtragen“. Für den Moment ist es wichtig, diese Falle zu erkennen: Ein Prototype bleibt nur kurzlebig, solange Sie jedes Mal zum Container kommen, um ihn zu holen.
Es gibt noch eine weitere Nuance, die gut mit dem Lifecycle-Thema vom gestrigen Tag zusammenhängt. Für einen Prototype-Bean garantiert der Container die Erstellung und die Dependency-Injection, aber verwaltet die Zerstörung nicht so wie bei singleton. Das heißt, Spring erstellt den Prototype, gibt ihn Ihnen, und ab dann sind Sie erwachsen: Wenn Ressourcen vorhanden sind, müssen Sie sie selbst schließen. In Übungsbeispielen machen wir Prototypes normalerweise leichtgewichtig (ohne Datei-Handles und Sockets), um nicht in den „Überlebenskurs“ zu verfallen.
5. Wie man scope festlegt: @Scope
Wenn Sie @Scope("prototype") zum ersten Mal sehen, möchten Sie fragen: „Warum als String? Warum kein Enum? Warum nicht Scope.PROTOTYPE?“ Willkommen in der Realität von Java-Enterprise, wo vieles historisch als String gehandhabt wird, und Spring ist damit nicht allein. Die gute Nachricht: Sie werden diesen String nicht allzu oft schreiben, denn der Default ist singleton, und prototype wird nur für wirklich kurzlebige Dinge benötigt.
Scope kann an zwei gängigen Stellen festgelegt werden. Die erste Variante ist an der Komponente, die per Scanning in den Container gelangt:
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // Konstante ist sicherer als der String "prototype"
class OrderProcessingSession {
// Jede Anfrage an den Container gibt eine neue Instanz dieser Komponente zurück
}
Beachten Sie: Ich verwende die Konstante ConfigurableBeanFactory.SCOPE_PROTOTYPE und nicht den String "prototype". So ist die Wahrscheinlichkeit geringer, sich in den Anführungszeichen zu vertun und dann eine halbe Stunde lang auf die Anwendung zu starren, die „aus irgendeinem Grund nicht zum Prototype geworden ist“. Ja, das ist eine Kleinigkeit. Aber das Leben besteht aus Kleinigkeiten.
Die zweite Variante ist, scope in einer @Bean-Methode innerhalb einer @Configuration festzulegen. Das ist nützlich, wenn Sie die Klasse nicht als Komponente annotieren möchten (oder können) oder wenn die Erstellung eine explizite Konfiguration erfordert:
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
class SessionConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // Neue Instanz bei jedem Zugriff auf den Container
OrderProcessingSession orderProcessingSession() {
// Hier beschreiben wir dem Container explizit, wie der Prototype-Bean erstellt wird
return new OrderProcessingSession();
}
}
Der methodische Schlüsselgedanke hier: Wir sehen wieder, dass scope eine Eigenschaft der Bean-Definition ist, und die @Bean-Methode ist genau die „Fabrik der Bean-Definition“. Die Klasse OrderProcessingSession an sich wird nicht als Prototype „geboren“. Sie wird zum Prototype, weil wir es dem Container so beschrieben haben.
Falls Ihnen plötzlich die Idee kommt, @Scope("singleton") zu schreiben – technisch machbar, aber in den meisten Projekten ist das unnötiges Rauschen. Singleton ist bereits der Default; es ist besser, kein „Annotations-Spam“ zur Kontrollillusion zu betreiben.
6. Scope und Zustand: Die stateful/stateless-Regel
Scope wird nicht in dem Moment wirklich verständlich, in dem Sie @Scope sehen, sondern wenn Sie es mit der Frage verbinden: „Darf dieses Objekt Zustand zwischen Aufrufen speichern?“ Hier beginnen die ernsteren Gespräche, aber wir bleiben im Rahmen des ersten Kurses, ohne Fanatismus.
Es gibt eine nützliche Regel, die erstaunlich oft funktioniert: Langlebige Dienste sollten stateless sein und passen daher hervorragend zu singleton. Stateless bedeutet, dass der Dienst keine „aktuellen Betriebsdaten“ in Feldern speichert; er erhält Daten als Methodenparameter, erledigt die Arbeit und gibt das Ergebnis zurück. In ContextFlow sind solche Objekte OrderPlacementService, AuditService, ReportingService und andere Orchestrierungsdienste: Sie koordinieren Schritte, sollten sich aber nicht in eine „Kiste mit aktuellem Zustand“ verwandeln.
Stateful Objekte hingegen, die ihrem Wesen nach „einen Vorgang“ leben, sind oft sinnvollerweise als prototype zu implementieren. Zum Beispiel kann eine „Bestellverarbeitungssitzung“ Zwischenschritte, Messungen, Zeitstempel und eine Liste der durchgeführten Aktionen ansammeln. Sie muss nicht länger als einen Vorgang leben. Und genau deshalb ist sie ein guter Kandidat für prototype.
Aber eine wichtige Grenze. Prototype ist nicht für alles Temporäre zwingend. Manchmal ist es einfacher und ehrlicher, ein Objekt mit einem einfachen new innerhalb der Methode zu erstellen und als lokale Variable zu halten. Der Container wird dort benötigt, wo das Objekt eine echte Abhängigkeit ist, die Sie konfigurieren/austauschen/ mit Spring-Mechanismen umhüllen möchten, oder wo es logisch Teil des Objektgraphen ist. Wenn das Objekt nur eine „temporäre Liste von drei Zeilen“ ist, muss der Container nichts davon wissen (sonst wird Spring zum „Manager von allem auf der Welt“ und Sie zu seiner Geisel).
Um den Unterschied zu festigen, hier eine kleine Tabelle, die man im Kopf behalten sollte:
| Frage | Eher singleton | Eher prototype |
|---|---|---|
| Objekt lebt so lange wie die Anwendung | Ja | Nein |
| Objekt ist ein Dienst/Infrastruktur, nicht „Daten eines Vorgangs“ | Ja | Manchmal |
| Im Objekt gibt es keinen veränderlichen Zustand, der an einen bestimmten Aufruf gebunden ist | Ja | Nicht unbedingt |
| Objekt trägt den Zustand eines einzigen Szenarios/Vorgangs | Normalerweise nein | Ja |
| Es wird „neuer Zustand“ für jedes Mal benötigt | Nicht Scope, sondern Design | Oft ja |
Heute ist es uns wichtig, nicht die Tabelle auswendig zu lernen, sondern den Gedanken zu fühlen: Scope ist ein Werkzeug, das hilft, die Lebensdauer des Verhaltens nicht mit der Lebensdauer der Daten zu verwechseln.
7. Mini-Inkrement für ContextFlow
Jetzt machen wir einen kleinen, ehrlichen Schritt in Richtung unseres Projekts ContextFlow. Wir werden noch keine komplexen Schemata bauen, keine Session in Dienste einfügen – das ist genau der Bereich, in dem man leicht falsche Erwartungen weckt. Heute ist unsere Aufgabe einfacher: Wir legen ein Objekt an, das vom Wesen her wirklich „einmalig“ ist, weisen ihm den scope prototype zu und stellen sicher, dass der Container verschiedene Instanzen ausgibt.
Fügen wir dem Projekt die Klasse OrderProcessingSession hinzu. Sie soll die „Schritte“ der Verarbeitung speichern – einfach Strings. Das ist recht lebensnah (kann später für die Diagnose verwendet werden), überlastet aber das Übungsprojekt nicht:
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class OrderProcessingSession {
private final List<String> steps = new ArrayList<>();
void addStep(String step) {
steps.add(step);
}
List<String> steps() {
return List.copyOf(steps);
}
}
Jetzt – eine kleine Demonstration am Einstiegspunkt (wichtig: nicht im Business-Service, sondern im Bootstrap-/Demo-Code). Wir bitten den Container einfach um zwei Instanzen und prüfen, dass sie unterschiedlich sind:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
class ScopeDemo {
public static void main(String[] args) {
try (var ctx = new AnnotationConfigApplicationContext(OrderProcessingSession.class)) {
var s1 = ctx.getBean(OrderProcessingSession.class);
var s2 = ctx.getBean(OrderProcessingSession.class);
System.out.println(s1 == s2); // false
}
}
}
Wenn Sie etwas „Handfesteres“ sehen möchten, können Sie der ersten Session Schritte hinzufügen und feststellen, dass die zweite keine hat – weil es ein anderes Objekt ist:
s1.addStep("validate");
System.out.println(s1.steps()); // [validate]
System.out.println(s2.steps()); // []
Hier haben wir vorerst nur eines getan: Wir haben einen Kandidaten für eine kurzlebige Abhängigkeit in das Projekt eingeführt und festgestellt, dass der Container verschiedene Instanzen ausgibt. Das reicht schon, um im Kopf zwei Rollen zu trennen: „langlebige Dienste“ und „Zustand eines einzelnen Vorgangs“ sind nicht dasselbe.
8. Typische Fehler im Umgang mit scope
Fehler Nr. 1: Zu denken, dass scope eine Eigenschaft der Klasse ist, nicht des Beans.
Manchmal bildet sich im Kopf die Formulierung „Die Klasse OrderProcessingSession ist prototype“. Aber korrekter ist: „Der Bean orderProcessingSession ist prototype“. Das ist keine Pedanterie: Sobald Sie anfangen, Konfiguration zu lesen und Wiring zu verstehen, müssen Sie in Container-Begriffen denken – also in Begriffen von Bean-Definitionen, nicht in „Auren von Klassen“.
Fehler Nr. 2: Spring-singleton mit dem globalen GoF-Singleton verwechseln.
Wenn in Ihrem Kopf die Idee lebt, dass „Singleton bedeutet ein Exemplar für immer im gesamten Programm“, werden Sie schnell anfangen, Dienste so zu entwerfen, als wären sie globale statische Dinge. Spring funktioniert so nicht: Singleton ist eine Instanz pro Container. Es kann mehrere Container geben (in Tests, in separaten Modulen, in verschiedenen Startern), und das ist normal.
Fehler Nr. 3: Prototype „nur für den Fall“ einschalten, weil es sicherer erscheint.
Prototype ist kein „Modus für erhöhte Sicherheit“. Es ist ein Modus mit einer anderen Lebensdauer. Wenn Sie ohne Grund einen Dienst als Prototype deklarieren, der eigentlich gemeinsam genutzt werden sollte, erschweren Sie die Diagnose, erhöhen die Anzahl der Objekte und landen häufiger bei unerwartetem Verhalten. Besser ist es, zuerst ehrlich zu antworten: Trägt das Objekt wirklich den Zustand eines einzigen Vorgangs oder nicht?
Fehler Nr. 4: Erwarten, dass ein Prototype in einem Singleton-Service automatisch „bei jedem Methodenaufruf ein neues Objekt“ wird.
Das ist die häufigste Falle. Ein Prototype wird bei jeder Anfrage an den Container erstellt, aber wenn Sie ihn einmal erhalten und als Feld speichern, ist er nicht mehr „einmalig“. Daraus entsteht der Satz „Prototype innerhalb von Singleton funktioniert nicht“ – dabei funktioniert es schon, die Erwartung war nur falsch.
Fehler Nr. 5: Vergessen, dass der Container nicht verpflichtet ist, Prototype-Beans zu „zerstören“.
Nach dem Lifecycle-Thema möchte man glauben, dass @PreDestroy immer aufgerufen wird. Aber bei Prototype ist das Modell anders: Der Container hat erstellt, ausgegeben – und dann verwalten Sie die Lebensdauer selbst. Daher ist es besser, Prototypes leichtgewichtig zu halten, ohne Ressourcen, die eine obligatorische Schließung erfordern, insbesondere im Übungsprojekt.
GO TO FULL VERSION