1. Sinn der Spring Boot-Schichten
Man möchte das Thema gerne mit einfachem Copy-Paste „besiegen“: ein Dockerfile im Internet finden, einfügen, freuen. Das Problem ist, dass Layering eine Geschichte über die Änderungshäufigkeit ist. Wenn Sie nicht verstehen, was dependencies bedeutet und wodurch es sich von application unterscheidet, können Sie weder erklären, warum der Build schneller wurde (oder warum nicht), noch können Sie seltsame Cache-Effekte diagnostizieren.
Wenn wir von „Spring Boot-Schichten“ sprechen, geht es nicht darum, dass Docker plötzlich Spring-orientiert geworden wäre. Docker ist nach wie vor eine Ansammlung von Schichten gemäß den Dockerfile-Schritten. Spring Boot hilft lediglich dabei, den Inhalt der JAR in logische Teile zu zerlegen, sodass man sie im Dockerfile separat kopieren kann. Dadurch erhält Docker die Chance, stabilere Teile (Abhängigkeiten) wiederzuverwenden und nur das neu aufzubauen, was sich tatsächlich häufig ändert (Ihr Code).
Ganz einfach ausgedrückt: Die Standard-Schichten von Spring Boot sind der Versuch, die JAR weniger wie „einen riesigen Koffer“ und eher wie einen ordentlichen Schrank aussehen zu lassen: Unten liegen schwere, selten wechselnde Dinge, oben das, was Sie ständig herausholen und umräumen.
2. Standard-Schichten von Spring Boot
Bevor wir jede Schicht im Detail betrachten, ist es sinnvoll, eine einfache „Landkarte“ zu fixieren. In den meisten Spring Boot-Anwendungen (einschließlich unseres Container-Ready Catalog Service) sehen Sie vier Standard-Schichten. Sie spiegeln die Idee wider: Abhängigkeiten ändern sich selten, Ihr Code ändert sich häufig, und der Boot-Loader ist ein separater stabiler „Dienstleistungsteil“, den man ebenfalls sinnvollerweise getrennt hält.
In Tabellenform sieht das so aus:
| Name der Schicht | Ungefährer Inhalt | Häufigkeit der Änderung | Warum für Docker vorteilhaft |
|---|---|---|---|
| dependencies | normale released‑Abhängigkeiten (JARs aus Maven/Gradle) | selten | große Schicht wird gecacht und wiederverwendet |
| spring-boot-loader | Klassen des Spring‑Boot‑Loaders (das, was die executable JAR startet) | selten (normalerweise bei einem Wechsel der Boot-Version) | stabile Schicht wird nicht angetastet, solange Boot nicht aktualisiert wird |
| snapshot-dependencies | Abhängigkeiten, die sich potenziell häufig ändern (z. B. SNAPSHOT) | kann sich häufiger ändern | Trennung der „wackeligen“ Schicht von der stabilen |
| application | Ihr Code, Ressourcen, application.yml, Klassen, statische Dateien | häufig | nur die oberste Schicht ändert sich, der Cache darunter bleibt erhalten |
Die Namen selbst sind aus zwei Gründen wichtig. Erstens werden Sie sie in der Ausgabe von list-layers und in den Verzeichnissen nach extract sehen. Zweitens werden Sie sie (oder zumindest in der richtigen Reihenfolge) im Dockerfile angeben. Und ja: Die Reihenfolge ist nicht dekorativ – sie ist ein direkter Hinweis an Docker, was „langfristig“ gecacht werden sollte.
dependencies: „Das schwere Regal mit Konserven“
Die Schicht dependencies ist in der Regel die „gewichtigtste“ und die dankbarste für den Cache. Stellen Sie sich vor, Ihr Service ist eine Küche. Ihre Abhängigkeiten sind die Vorräte – Getreide, Nudeln, Konserven und Gewürze in der Speisekammer. Sie werden jeden Tag gebraucht, aber Sie stellen die Dosen nicht jedes Mal um, wenn Sie das Rezept für ein Omelett ändern. dependencies ist quasi die Speisekammer, und Ihr neuer Code ist das Omelett, das Sie gerade zubereiten.
In dependencies liegen die JARs der Bibliotheken, die Gradle aus den Repositories geholt hat: Spring Framework, Jackson, Logback, Micrometer und so weiter. Für unseren Beispielservice landen dort auch die Abhängigkeiten für die Web-Schicht, Actuator und alles, was bereits als Baseline festgelegt ist. Der Sinn der Schicht liegt darin, dass Sie bei der normalen Entwicklung häufiger den Code eines Controllers oder Services ändern als die Liste der Abhängigkeiten in der build.gradle.kts.
Der praktische Effekt in Docker ist: Sie ändern ein paar Zeilen Java-Code, bauen das Image neu, und Docker (wenn das Dockerfile richtig aufgebaut ist) erkennt, dass sich die Schicht dependencies nicht geändert hat. Also baut es sie nicht neu, packt sie nicht erneut und zwingt Sie nicht, dieses Volumen bei Push/Pull erneut „herumzuschleppen“. Der Spar Effekt ist in der Regel spürbar: Wenn die Schicht groß ist, bringt die Wiederverwendung des Caches eine sehr angenehme Beschleunigung – besonders wenn das Projekt nicht mehr „Hello World“ ist.
spring-boot-loader: Der serviceorientierte „Startmotor“
Die Schicht spring-boot-loader ruft bei Anfängern oft leichte Skepsis hervor: „Wozu irgendwelche Loader? Ich will doch einfach java -jar.“ Aber die executable JAR von Spring Boot ist nicht einfach ein „Zip mit Klassen“. Es ist eine JAR, die sich „schön“ und kontrolliert starten lässt: Sie enthält einen Loader, der den richtigen Classpath aufbaut und Ihre Anwendung startet.
Dieser Loader lebt in der Schicht spring-boot-loader. Man kann ihn als einen kleinen eingebauten „Launcher“ betrachten, der weiß, wie man Ihr Boot-Archiv startet. Er enthält keine Geschäftslogik und ändert sich normalerweise nicht bei Code-Änderungen. Er ändert sich hauptsächlich, wenn Sie die Version von Spring Boot aktualisieren (oder die Art der Anwendungspaketierung ändern, was wir im Kurs nicht tun).
Warum ist das eine separate Schicht? Weil es ein stabiler Teil ist. Er ist fast so stabil wie die Abhängigkeiten, aber logisch etwas anderes: Abhängigkeiten sind „Ihre Bibliotheken“, der Loader ist der „Startmechanismus von Boot“. Die Trennung hilft sowohl beim Cache als auch beim Verständnis der Struktur. Wenn Sie eine separate Schicht spring-boot-loader sehen, hören Sie auf, die JAR als monolithisches Ding „mit allem vermischt“ zu betrachten, und beginnen, darin Engineering-Komponenten zu erkennen.
Wenn Sie jemals den Gedanken hatten: „Warum startet mein Projekt so umständlich?“, dann ist spring-boot-loader ein Teil der Antwort. Nicht der einzige, aber ein sehr wichtiger.
snapshot-dependencies: Die „wackelige“ Schicht
Die Schicht snapshot-dependencies erscheint vielen überflüssig, weil wir in Übungsprojekten (und bei klarem Verstand) selten SNAPSHOT-Abhängigkeiten als Lebensbasis verwenden. Aber Spring Boot Layering denkt weiter: Es geht davon aus, dass Abhängigkeiten manchmal „beweglicher“ sein können als normale released-Bibliotheken.
Ein typischer Fall: Im Team wird eine interne Bibliothek verwendet, die sich häufig ändert und als Snapshot/unstable-Version eingebunden wird. Wenn man sie in eine Schicht mit den normalen stabilen Abhängigkeiten legt, würde jede Änderung dieser „wackligen“ Bibliothek die gesamte Abhängigkeitsschicht invalidieren – das wäre schmerzhaft.
Daher ist snapshot-dependencies separat ausgegliedert: Docker kann die riesige dependencies-Schicht weiterverwenden und nur die Schicht mit den „wackligen“ Abhängigkeiten ändern. In unserem Beispielservice kann diese Schicht durchaus leer oder fast leer sein – das ist in Ordnung. Der Sinn der Schicht liegt nicht darin, dass sie groß sein muss, sondern dass sie „potenziell häufig Änderndes“ von „üblicherweise Stabilem“ trennt.
Und hier gibt es einen wichtigen methodischen Punkt: Wenn ein Student eine Schicht sieht, die „sinnlos“ erscheint, versucht er oft, sie zu entfernen. In echten Teams ist ein solcher Refactoring manchmal gerechtfertigt, aber im Kurs ist es uns wichtiger, die Standardstruktur lesen zu lernen, weil genau diese Struktur am häufigsten vorkommen wird.
application: Ihr Code und Ihre Ressourcen
Die Schicht application ist der Grund, warum wir die ganze Geschichte mit den Schichten überhaupt begonnen haben. Sie ist die „Spitze des Eisbergs“, die sich am häufigsten ändert. Hier landen Ihr kompilierter Bytecode, Ihre Ressourcen, Konfigurationsdateien, Migrationen, statische Dateien – alles, was zur Anwendung selbst gehört, nicht zur Startplattform und nicht zu den Bibliotheken.
In unserem Container-Ready Catalog Service landen hier die Klassen der Katalog-Controller, Export-Services, DTOs, Ressourcen wie application.yml, application-standalone.yml und so weiter. Wenn Sie GET /api/catalog/items bearbeiten, ein Log hinzufügen oder das CSV-Exportformat ändern – dann berühren Sie genau application.
Und hier kommt der entscheidende Trick: Wenn application in einem separaten Schritt und als letztes (nach den Abhängigkeiten und dem Loader) in das Docker-Image kopiert wird, dann wird Docker bei jeder Ihrer Änderungen hauptsächlich nur diese oberste Schicht neu aufbauen. Alles „darunter“ bleibt gleich und kommt aus dem Cache. Das fühlt sich an, als würde man nur ein Buch im Regal austauschen, anstatt den gesamten Schrank umzuziehen.
Ein weiterer Bonus: Wenn Sie das neue Image in die Registry pushen (oder einfach über das Netzwerk übertragen), ändert sich nur die oberste Schicht. Selbst wenn Sie im Kurs nicht täglich pushen, ist das Verständnis dieser Mechanik ein wichtiger Teil des erwachsenen Denkens: Nicht nur der Build, sondern auch die Auslieferung wird schneller.
3. Reihenfolge der Schichten und COPY im Dockerfile
Wenn Sie sich bis heute nur einen Gedanken zum Docker-Cache gemerkt haben, dann diesen: „Wenn eine frühe Schicht geändert wird, wird alles danach neu gebaut.“ Jetzt wenden wir denselben Gedanken auf einer feineren Ebene an: Nicht nur „Quellen getrennt von Abhängigkeiten“, sondern „innerhalb der JAR: Abhängigkeiten getrennt vom Code“.
Die Reihenfolge der Spring-Boot-Schichten ist buchstäblich ein Hinweis darauf, was „tiefer“ im Image liegen sollte und was „höher“. Die stabilste Schicht ist dependencies, dann folgt der stabile spring-boot-loader, dann die potenziell veränderlichere snapshot-dependencies, und ganz oben die veränderlichste application.
Um dies zu festigen, ist eine Minitabelle hilfreich: „Was wir ändern – was betroffen ist“:
| Was Sie im Projekt geändert haben | Welche Schicht ändert sich meistens | Was wird Docker beim nächsten Build merken |
|---|---|---|
| Controller / Service / DTO korrigiert | application | Die oberste Schicht wird neu gebaut, die untere bleibt im Cache |
| application.yml geändert | application | Gleiches: nur die obere Schicht wird angefasst |
| Abhängigkeit in Gradle hinzugefügt | dependencies (und manchmal ) |
Die große Schicht wird neu gebaut, aber das passiert seltener |
| Spring-Boot-Version aktualisiert | spring-boot-loader (und Abhängigkeiten) | Die „Plattform“-Schichten ändern sich, das ist ein normaler großer Schritt |
| SNAPSHOT-Bibliothek eingebunden | snapshot-dependencies | Der Cache von bleibt erhalten, nur die Snapshot-Schicht ändert sich |
Wichtig: Das ist keine Mathematik, sondern eine Ingenieursheuristik. Die tatsächliche Zusammensetzung der Schichten hängt davon ab, was genau und wie gebaut wird. Aber als „Karte für den Kopf“ funktioniert es hervorragend und hilft vorherzusagen, was beim Build passieren wird.
Um ein Beispiel für das Dockerfile zu zeigen (rein als Idee der Kopierreihenfolge), würde es so aussehen:
# Arbeitsverzeichnis im Image: Alle COPY legen Dateien hier ab.
WORKDIR /app
# Schichten in der Reihenfolge von den stabilsten zu den veränderlichsten kopieren:
# So wird der Docker-Cache bei normaler Entwicklung so selten wie möglich zerstört.
COPY --from=extract /build/extracted/dependencies/ ./
COPY --from=extract /build/extracted/spring-boot-loader/ ./
COPY --from=extract /build/extracted/snapshot-dependencies/ ./
# Zuletzt die Anwendungsschicht kopieren: Sie ändert sich am häufigsten (Ihr Code und Ressourcen).
COPY --from=extract /build/extracted/application/ ./
Hier gibt es keine Magie. Wir kopieren zuerst das, was stabiler sein sollte, und erst am Ende das, was sich häufiger ändert. Docker cached die COPY-Schritte, und wenn sich nur application ändert, werden die ersten drei COPY-Schritte aus dem Cache genommen.
4. Schichten auf der Festplatte nach dem Extract
Ein häufiges Problem von Docker-Anfängern ist, dass alles abstrakt bleibt, bis man Ordner und Dateien auf der Festplatte sieht. Daher ist es hilfreich, einmal auf der Ebene „einfach anschauen“ zu sehen, was Spring Boot aus der JAR holt. Wir wissen bereits, wie man extract macht (das war in der letzten Lektion), und jetzt interessiert uns, was am Ende herauskommt.
Stellen wir uns vor, wir haben die Schichten in das Verzeichnis layers/ extrahiert. Dann sehen Sie in der Praxis in etwa die folgende Struktur:
layers/
├── dependencies/
├── spring-boot-loader/
├── snapshot-dependencies/
└── application/
Und wenn Sie zum Beispiel in application/ hineinschauen, finden Sie dort Ihre Klassen und Ressourcen. Wenn Sie in dependencies/ schauen, finden Sie JARs. In spring-boot-loader/ sind die Klassen des Boot-Loaders. Selbst wenn Sie die genauen Dateinamen nicht kennen, vermittelt die Tatsache getrennter Verzeichnisse bereits ein Gefühl von Ordnung: „Hier sind meine Bibliotheken“, „hier ist der Service-Teil“, „hier ist mein Code“.
Für einen angehenden Entwickler ist dies ein wichtiger Moment: Sie hören auf, „auf’s Wort“ zu vertrauen, und beginnen, sich auf die beobachtbare Struktur zu stützen. In der Docker-Welt ist das eine Superkraft: Wenn etwas unklar ist, kann man meistens einfach nachschauen, was im Image oder im Build-Verzeichnis liegt.
Layer-Index: „Die Karte der Schichten“
Wenn es heißt „Spring Boot beschreibt die Schichten“, ist es wichtig zu verstehen, dass es sich nicht um eine mystische telepathische Fähigkeit handelt. Innerhalb der JAR gibt es eine servicoorientierte Information – eine Art „Zuordnungskarte“, die Dateien und Pfadmuster mit Schichtnamen verknüpft. Deshalb heißen die Schichten standardisiert und lassen sich vorhersagbar extrahieren: Sie haben sich nicht den Ordner dependencies/ ausgedacht, er folgt aus den Metadaten der JAR.
In der Praxis müssen Sie diesen Index selten von Hand lesen. Der praktische Weg ist einfacher: Sie verwenden list-layers, um die Namen der Schichten zu sehen, und extract, um die Verzeichnisse zu erhalten. Aber das Verständnis des Layer-Index ist methodisch nützlich. Es beantwortet die Frage „Warum funktioniert das überhaupt?“ und verhindert, dass das Lernen zu einem „Wiederhole mir drei Zaubersprüche nach und dann wird’s schon gut“ verkommt.
Wenn Sie jemals „prüfen möchten, ob in der JAR wirklich Service-Dateien sind“, können Sie das durch einfaches Ansehen des JAR-Inhalts tun. Zum Beispiel so (ohne Fanatismus und ohne in die Tiefe zu gehen):
# Schnell die inneren der JAR ohne Entpacken ansehen: nützlich, wenn man „mit den Händen anfassen“ möchte.
jar tf build/libs/*.jar | head -n 10
# META-INF/
# META-INF/MANIFEST.MF
# org/
# org/springframework/boot/loader/...
# BOOT-INF/
# BOOT-INF/classes/...
# BOOT-INF/lib/...
Schon diese kleine Ausgabe zeigt die Trennung: BOOT-INF/classes – das ist Ihr Code und Ihre Ressourcen, BOOT-INF/lib – die Abhängigkeiten, und die Loader-Klassen sind ein separater Teil. Das sind bereits fast die Schichten. Der Layer-Index formalisiert diese Aufteilung lediglich und macht sie für das Werkzeug tools nutzbar.
5. Typische Fehler beim Arbeiten mit Schichten
Fehler Nr. 1: dependencies und application als „einfach zwei Ordner“ betrachten und sie in beliebiger Reihenfolge im Dockerfile kopieren.
Wenn die Reihenfolge der COPY-Anweisungen nicht mehr die Änderungshäufigkeit widerspiegelt, berauben Sie sich des Hauptvorteils des Layering. Wenn Sie zuerst application und dann dependencies kopieren, invalidieren Sie bei jeder Code-Änderung auch den Cache auf der Abhängigkeitsebene. Die Schichten sind zwar vorhanden, aber der Nutzen ist fast null – das ist die ärgerlichste Situation.
Fehler Nr. 2: Panik wegen snapshot-dependencies, wenn die Schicht leer ist, und der Versuch, den Standard zu „vereinfachen“.
Ein Anfänger denkt oft: „Ein leerer Ordner ist überflüssig – weg damit.“ Aber die Standard-Schichten von Boot sind ein kanonisches Muster, das Sie in Artikeln, Repositorien und Teams antreffen werden. Ein leeres snapshot-dependencies ist ein normaler Zustand für ein Projekt ohne Snapshot-Abhängigkeiten. Es ist nicht schädlich, aber die Gewohnheit, „die Struktur als Standard zu lesen“, ist nützlich.
Fehler Nr. 3: Verwechslung der Spring-Boot-Schichten (logische Aufteilung des JAR-Inhalts) mit den Docker-Schichten (Ergebnis der Dockerfile-Schritte).
Spring Boot „erzeugt“ nicht selbst Docker-Layer. Es legt die Dateien nur in Verzeichnisse. Docker-Layer entstehen erst, wenn Sie separate COPY-Schritte im Dockerfile ausführen. Wenn Sie alle Verzeichnisse mit einem einzigen COPY kopieren, sieht Docker nur einen Schritt, eine Schicht – und das Layering wird dekorativ.
Fehler Nr. 4: Erwarten, dass sich spring-boot-loader durch Änderungen am Geschäftscode ändert.
Wenn nach einer Controller-Änderung plötzlich der spring-boot-loader nicht mehr gecacht wird, liegt das Problem nicht an Boot, sondern an Ihrem Build oder Dockerfile: Sie bauen die JAR anders, Sie cachen Schritte nicht richtig, oder Sie kopieren zu viele Dateien in einem Schritt. Normalerweise sollte der Loader zwischen gewöhnlichen Code-Änderungen stabil bleiben.
Fehler Nr. 5: Versuch, die Schichten durch Interna des Classloaders zu erklären und die Lektion über Docker in eine Lektion über JVM-Magie zu verwandeln.
Neugier ist gut, aber methodisch ist es wichtiger, den Fokus zu behalten: Wir lernen Schichten, um den Build und die Auslieferung von Images zu beschleunigen. Dafür reicht es zu verstehen: Abhängigkeiten separat, Loader separat, Anwendung separat. Alles andere ist interessante, aber nicht zwingende Tiefe, die leicht vom Weg abbringt und den Kopf überlastet.
GO TO FULL VERSION