Top 10 Spring Boot Hacks – Pragmatismus statt Magie

Die Veröffentlichung von Spring Boot hat vor über einem Jahrzehnt die Art und Weise, wie Unternehmensanwendungen in Java programmiert werden, revolutioniert. Die Idee der Starter, die kompatiblen Dependencies zu bündeln und das "Convention over Configuration"- Prinzip ermöglichen die Erstellung von Webanwendungen in wenigen Zeilen eigenen Codes. Und das Ganze startet ohne Application Server nur mit dem Embedded Tomcat. Heute eine Selbstverständlichkeit, damals eine kleine Sensation.
Anders als bei vergleichbaren Frameworks gelingt es den Machern von Spring Boot, die Konfiguration mit jeder Major Version noch einmal zu vereinfachen. Das einst revolutionär schlanke Spring Boot 1 erscheint im Vergleich zur aktuellen Version 3 fast schwergewichtig.
Eine Grunderfahrung beim Umstieg auf Spring Boot ist die Erkenntnis, dass es im Spring-Ökosystem für sehr viele Probleme bereits eine fertige Lösung gibt. Nicht wenige Projektteams löschen bei der Migration viel eigenen Code, weil entsprechende Bibliotheken die Funktionalität bereits mitbringen. Die Herausforderung ist, die Möglichkeiten zu kennen und gezielt im eigenen Projekt einzusetzen. Weniger Code bietet weniger Raum für Bugs und erleichtert die Wartung und Weiterentwicklung.
Dieser Artikel gibt einen Überblick über zehn praxiserprobte Tipps. Es gibt definitiv noch viele mehr. Auch wenn du schon sieben davon kennst, hast du nach dem Lesen drei neue Impulse mitgenommen, oder?
Doch auch wer mit Spring Boot arbeitet, merkt schnell: Die eigentliche Magie liegt in den vielen kleinen Kniffen – und genau hier beginnt die Geschichte von Team "Aurora".
- Ein typisches Entwicklungsteam
- Tipp 1: @Transactional in Tests verwenden
- Tipp 2: Flyway und @Sql kombinieren
- Tipp 3: Testcontainers verwenden
- Tipp 4: Mockito: Innen-Mocking
- Tipp 5: WireMock: Außen-Mocking
- Tipp 6: Konfiguration aus mehr als 15 Quellen
- Tipp 7: Spring Profile einsetzen
- Tipp 8: Spring Security einsetzen und mit @WithMockUser kombinieren
- Tipp 9: Spring Data JPA zum Datenzugriff
- Tipp 10: Caching mit @Cacheable
- Zehn Tipps – ein roter Faden
- Quellen
Ein typisches Entwicklungsteam
Das fiktive Entwicklungsteam "Aurora" kämpft sich durch den Alltag mit Spring Boot. Nach der Umstellung auf die Technologie fremdeln einige, vor allem ältere Teammitglieder mit der Magie und den unsichtbaren Abläufen.
Schlimmer sind jedoch die Themen die in der täglichen Arbeit Zeit und Nerven kosten:
Liebes Tagebuch, heute war ein schwieriger Tag. Wir mussten eine neue Testklasse hinzufügen. Wie immer in diesem Fall schlug nun plötzlich ein komplett anderer Test fehl und wir mussten unsere Testdaten einen ganzen Nachmittag lang aufräumen.
Kommt dir das bekannt vor? Eine eigentlich stabile CI/CD Pipeline schlägt plötzlich fehl, nur, weil eine neue Testklasse dazugekommen ist? Dann hilft dir vielleicht einer der folgenden Tipps.
Tipp 1: @Transactional in Tests verwenden
Eine mögliche Ursache des oben beschriebenen Verhaltens sind von Tests erzeugte und nicht aufgeräumte Testdaten. Mit etwas Glück stören diese Daten nachfolgende Tests nicht, daher ist alles grün. Das Hinzufügen einer neuen Testklasse oder das Umbenennen einer bestehenden beeinflusst nun die Ausführungsreihenfolge der Tests in der Pipeline. Und plötzlich führen die Daten-Überbleibsel zum Fehlschlag eines ganz anderen Tests.
Besonders unangenehm daran ist, dass das Debugging äußerst schwierig ist: Der Fehlschlag findet an einer komplett anderen Stelle als die Verursachung des Problems statt. Im schlimmsten Fall im letzten ausgeführten Test der Suite, dann kommen alle anderen zuvor ausgeführten Klassen als Übeltäter in Frage.
Werden Testklassen generell mit @Transactional annotiert, führt Spring Boot den Test in einer eigenen Datenbank-Transaktion aus. Diese wird, unabhängig vom Testergebnis, nach dem Test wieder zurückgerollt. Alle Datenänderungen, also Inserts, Updates und Deletes, werden verworfen und der nächste Test, insbesondere schon die nächste Testmethode in der gleichen Klasse, findet eine verlässliche Ausgangslage vor.
Die flächendeckende Verwendung von @Transactional in Tests stärkt die Isolation der Tests: Egal in welcher Reihenfolge die Klassen in der Suite und in welcher Reihenfolge die Test-Methoden in den Klassen ausgeführt werden, das Ergebnis ist immer das gleiche.
Warnung
Kein Licht ohne Schatten: Transaktionale Tests lösen eine Menge Probleme, haben aber natürlich auch ihren Preis. So treten die gefürchteten LazyInitializationExceptions durch den Transaktionskontext nicht mehr im Test auf, sondern erst zur Laufzeit. Erfahrungsgemäß ist dieser Preis jedoch in Anbetracht stabilerer und parallelisierbarer Pipelines angemessen.
Tipp 2: Flyway und @Sql kombinieren
Aufmerksamen Leser:innen ist aufgefallen, dass ich oben nur von Datenänderungen gesprochen habe, die zurückgerollt werden. Aber was ist mit den Strukturen, also Tabellen, Spalten, Constraints, etc.?
Zur Pflege des eigentlichen Datenbankschemas empfehle ich ein sogenanntes "Higher Level Migration Tool". Spring Boot bietet eine nahtlose Integration für Flyway und Liquibase [1]. Die Idee dieser Tools ist, dass die Anwendung sich selbst um die Anpassung des Datenbankschemas kümmert. Nicht nur Team Aurora hatte früher SQL Skripte, die im Moment des Live-Deployments händisch eingespielt werden mussten. Und nicht nur im Team Aurora wurde das regelmäßig vergessen... Die Migrationstools prüfen beim Hochfahren der Anwendung ob neue Skripte eingespielt werden müssen, und führen das bei Bedarf in der richtigen Reihenfolge durch.
Dies geschieht in Tests in dem Moment, wenn der Spring Context erzeugt wird – und damit außerhalb der Test Transaktion. Dementsprechend werden Daten, die in Flyway Skripts erzeugt werden, nicht durch die Test Transaktion zurückgerollt.
In der Praxis empfiehlt sich daher eine klare Entscheidungsregel für die von Tests genutzten Daten zu definieren:
- Schema und Basiskonfiguration legt Flyway oder ein anderes Migrationstool an. Basiskonfiguration umfasst hier die Daten, ohne die die Anwendung nicht starten kann, zum Beispiel Rollen oder der Super-Admin des Systems.
- Mit @Sql annotierte Skripte legen Mandatendaten und Testszenarien an. Zum Beispiel zählt die Grundkonfiguration eines Kunden oder eines Webshops zu den Mandantendaten, währenddessen ein vorbereiteter Warenkorb ein Szenario für einen konkreten Test ist.
Der Trick daran ist, dass die per @Sql erzeugten Daten innerhalb des Transaktionskontextes leben. Das heißt, dass jeder Test immer wieder den vorbereiteten initialen Webshop vorfindet.
Profi-Tipp
Die Annotation @Sql ist auf Klassen- oder Methodenebene erlaubt. Eigentlich würde die Annotation an der Methode, die Annotation auf der Klasse überschreiben. Dagegen hilft folgendes Snippet:
@SpringBootTest
@Transactional
@SqlMergeMode(MERGE)
@Sql({"classpath:/schema-init.sql", "classpath:/data-init.sql"})
public class PersonRepositoryTest {
@Test
@Sql("classpath:/individual-test-data.sql")
public void testReadAllPersons() {
/* test content here */
}
}
Der explizit auf den Wert MERGE gesetzte SqlMergeMode erzwingt, dass Spring Boot sowohl die auf der Klasse als auch die auf der Methode gesetzte Annotation auswertet und die jeweiligen Skripte laufen lässt.
Warnung
Je weiter die Testdatenerzeugung von der eigentlichen Testmethode separiert wird, umso schwieriger wird die Recherche im Falle eines Testfehlschlags. Im Zweifelsfall lieber mehrere kleinere Skripte verwenden oder die Testdaten direkt im Test erzeugen.
Tipp 3: Testcontainers verwenden
Embedded Databases wie H2 sind ein hervorragendes Mittel, um stabile Entwicklungs-Setups bereitzustellen. Eine Maven Dependency bringt die komplette Datenbank in die Anwendung und es ist keine weitere Installation auf den Entwicklungsmaschinen notwendig.
Dennoch birgt jeder Konfigurationsunterschied zwischen Entwicklungsmaschine, Test Stages und Produktivsystem eine potenzielle Fehlerquelle. Die Verwendung von Testcontainers hilft, diese Unterschiede auf ein Minimum zu reduzieren.
Die Integration ist in der aktuellen Version von Spring Boot in wenigen Zeilen getan:
@SpringBootTest
@Testcontainers
public class PostgresIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16.2")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
...
}
Bei der ersten Verwendung lädt und startet der Test ein postgres:16.2 Docker Image mit den fixen Credentials. Der eigentliche Trick ist die Rückführung dieser Parameter in die Spring Boot-Konfiguration: Die mit @DynamicPropertySource annotierte Methode setzt die Konfiguration innerhalb des Spring-Kontextes. Nicht nur für das Setup von Testcontainers ein sinnvoller Weg!
Warnung
Durch Embedded Tomcat und ein durchdachtes Dependency-Konzept reicht eine installierte JDK, um eine Spring Boot-Anwendung lokal entwickeln zu können. Die Verwendung von Testcontainers erfordert zusätzlich jedoch eine Docker-Installation auf der Entwicklungsmaschine. Das kann in manchen Unternehmen durchaus ein Problem sein und sollte innerhalb des Entwicklungsteams zuvor abgeklärt werden. Diese schmerzliche Erfahrung stammt übrigens von mir und nicht von Team Aurora…
Liebes Tagebuch, heute war ein schlimmer Tag. Wir mussten unsere Pipeline für das Staging Deployment viermal neu starten, da die Tests gegen die externe API immer wieder fehlschlugen. Die Test API war leider mal wieder total instabil...
Tipp 4: Mockito: Innen-Mocking
Grundsätzlich sind Tests sehr erstrebenswert. Eine hohe Test Coverage ebenso. Auch Tests für externe Kommunikationspartner wie APIs können viele Tränen ersparen – sofern sie richtig aufgesetzt sind. Andernfalls tritt das Gegenteil ein, die Pflege der Tests wird zur unangenehmen Zusatzaufgabe.
Automatisierte Tests sollten isoliert sein. Egal ob feingliedriger Modultest oder ein schichtenübergreifender Test, der Grundsatz bleibt der gleiche:
Ein Test darf sein Verhalten – Erfolg oder Fehlschlag – nur bei Änderung des zu Grunde liegenden Produktionscodes ändern.
Zurück zum API-Beispiel: Bindet eine Anwendung einen externen Service zum Abruf von Wetterdaten an, ist der Aufruf des Services vermutlich in der Businesslogik eingebunden. Wird bei jedem Testlauf tatsächlich die API aufgerufen, reagieren einzelne Tests bei Verlust der Netzwerkverbindung mit einem Fehlschlag, um im nächsten Lauf wieder grün zu sein. Ein klassischer Fall eines integrierten Tests. Anders als ein Integration-Test, der die Frage beantwortet, ob die Anwendung mit allen externen Partnern korrekt interagiert, soll der isolierte Test nur die Funktion innerhalb der Anwendung prüfen. Im Beispiel ist die Anforderung der Aufruf des API Services mit einer bestimmten Payload.
Das Framework Mockito liefert die Annotationen @MockitoBean und @MockitoSpyBean, die zunächst eine Spring Bean injecten. Diese Bean wird dann in ihrem Verhalten beeinflusst, um unabhängig von der externen API zu sein.
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class CityServiceTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private OpenMeteoService openMeteoService;
@BeforeEach
public void setUp() {
when(openMeteoService.getWeatherForLocation(48.37052388061071, 10.896021212946511))
.thenReturn(new OpenMeteoWeatherResponse(48.37052388061071, 10.896021212946511, new OpenMeteoCurrentWeather(15.0, 4.3, 80)));
// … further stubs ...
}
@Test
public void testGetAugsburgWithWeather() throws Exception {
mockMvc.perform(get("/cities/augsburg"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.displayName", is("Augsburg")))
.andExpect(jsonPath("$.currentTemperature", is(15.0)))
.andExpect(jsonPath("$.windDirection", is("O")))
.andExpect(jsonPath("$.latitude", is(48.37052388061071)))
.andExpect(jsonPath("$.longitude", is(10.896021212946511)));
}
}
Das Listing zeigt, wie der Mock des OpenMeteoService in der setUp-Methode beeinflusst und dann innerhalb des Spring-Kontextes verwendet wird. Der eigentliche Test ruft den Service nicht direkt auf, erhält aber das gemockte Ergebnis, ohne dass ein realer HTTP Call gegen die Fremd-API geschickt wird. Das vollständige Beispiel befindet sich im Beispielrepository [2] im Branch feature/mockito.
Profi-Tipp
Eine mit @MockitoBean injizierte Spring Bean ist erstmal ohne Funktion, sie stellt nur die Fassade zur Verfügung. Ruft ein Test oder eine andere Bean eine Methode auf, die nicht gestubbed wurde, liefert das Framework einen Fehler.
Dagegen bringt @MockitoSpyBean erstmal den gesamten Funktionsumfang der eigentlichen Bean mit. Einzelne Methoden können bei Bedarf überschrieben werden, oft kommen Spys aber zum Einsatz, um Interaktionen zu verifizieren: Mockito unterstützt dabei Assertions für die Anzahl von Interaktionen mit bestimmten Methoden.
Tipp 5: WireMock: Außen-Mocking
Die im vorherigen Tipp gezeigte Mockingstrategie bringt Spring Boot-Projekte deutlich näher an den Wunschzustand vollkommen isolierter Tests. Dennoch entsteht Kopplung zwischen Produktionscode und Testcode: Ändert sich eine Methodensignatur, erfordert dies eine Anpassung der Stubs. Die Library WireMock unterstützt einen Mocking-Ansatz, der wirklich außerhalb des eigenen Quellcodes liegt: Die Stubs werden auf Ebene der HTTP-Kommunikation eingehängt, die Spring-Komponenten richten ihre Anfragen gegen eine definierte Adresse, die dann mit fixen Inhalten antwortet.
@BeforeEach
public void setup() {
stub-For(WireMock.get(urlEqualTo("/v1/forecast?latitude=48.37052388061071&longitude=10.896021212946511¤t_weather=true"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("""
{
"latitude": 48.37052388061071,
"longitude": 10.896021212946511,
"current_weather": {
"temperature": 15.0,
"windspeed": 15.5,
"winddirection": 109
}
}
""")));
/* further stubs...*/
Das vollständige Beispiel befindet sich im Beispielrepository [2] im Branch feature/mockito.
Dieses Vorgehen zieht das Mocking wirklich nach außen, die einzige Intervention ist auf Ebene der Konfiguration von Spring Boot. WireMock bietet zudem sehr viele Möglichkeiten, von der Definition spezieller Szenarien über die Ablage der JSON Payloads als Dateien und vieles mehr.
Einzig die Konfiguration erfordert zweimaliges Hinsehen, ist sie doch sehr knapp und elegant gehalten. Aber das führt direkt zum nächsten Tipp.
Liebes Tagebuch, heute war kein schöner Tag. Jemand hat unsere Staging-Datenbank geleert. Die Zugangsdaten liegen ausschließlich in Git, ein File pro Umgebung. Es muss also jemand aus der Entwicklungsabteilung gewesen sein...
Tipp 6: Konfiguration aus mehr als 15 Quellen
Spring Boot bringt durch das Prinzip "Convention over Configuration" seit jeher ein sehr durchdachtes System zur Verwaltung von Konfigurationsparametern mit. Konfigurationsweichen im Quellcode gehören damit definitiv der Vergangenheit an und versehentlich geleakte Passwörter durch einen unvorsichtigen Commit sowieso.
Die Grundidee: Ein benannter Parameter kann an verschiedenen Stellen gesetzt und überschrieben werden. Diese Einstiegspunkte sind in einer klaren Hierarchie definiert und überschreiben die Konfigurationen aus den vorherigen Stufen. Das Codebeispiel zur Konfiguration von WireMock macht sich das zunutze: Die API URL ist in der Datei application.properties definiert:
meteo.service.baseurl = real.url-going.hereVerantwortlich für die HTTP-Kommunikation mit der API ist die RestClient Bean. Zu deren Erstellung injiziert Spring den entsprechenden Konfigurationsparameter und setzt ihn als Stamm-Pfad.
@Bean
public RestClient webClient(RestClient.Builder restClientBuilder, @Value("${meteo.service.baseurl}") String baseUrl) {
return restClientBuilder
.baseUrl(baseUrl)
.build();
}
In der lokalen Entwicklung wird der Aufruf zunächst fehlschlagen. Jede:r Entwickler:in kann aber eine Umgebungsvariable mit dem Namen METEO_SERVICE_BASEURL anlegen oder den Variablenwert als Startparameter in der IDE konfigurieren und überschreiben. Und genau das macht sich die Konfiguration von WireMock im Test auch zunutze: Die Annotation am Test zieht den dynamischen Port von WireMock an und überschreibt den Parameter für die Base URL.
@TestPropertySource(properties = {"meteo.service.baseurl=http://localhost:${wiremock.server.port}"})Die Äquivalenz der Schreibweisen der Parameternamen ist in der Dokumentation definiert und erklärt. Aber auch über die Tests hinaus zeigt das Konzept seine Stärken: Individuelle Konfigurationen befinden sich auf den einzelnen Stages, insbesondere Passwörter oder API Tokens sind nur einer kleinen Personengruppe zugänglich.
Um einen Überblick über die verschiedenen Möglichkeiten zu behalten, lohnt sich ein Bookmark der Übersichtsseite des Spring Boot Frameworks [3].
Warnung
Die 15 möglichen Konfigurationsquellen sind natürlich auch 15 Quellen für vergessene Konfigurationen. Im dümmsten Fall überschreiben sie gewünschte Werte und führen zu einer verzweifelten Suche. Unterstützung bietet hier zum Beispiel die Übersicht des Spring Boot Actuators. Der Endpunkt configprops erleichtert die Suche nach der Herkunft eines mehrfach überschriebenen Werts.
Tipp 7: Spring Profile einsetzen
Auch das beste Konfigurationskonzept wird mit zunehmender Anzahl Parameter unübersichtlich. Spring-Profile schaffen hier Abhilfe. Am besten bekannt ist wohl die Möglichkeit, Properties Files für Profile zu definieren, um die Parameter zu gruppieren.
So lädt Spring gemäß der Konfigurationsquellen zunächst die application.properties und überschreibt alle im application-{profile}.properties stehenden Parameter gemäß der Reihenfolge der Konfigurationsquellen.
Aber das Konzept greift noch tiefer: Die Annotation @Profile schaltet bestimmte Beans explizit ein oder aus. So wird eine mit @Profile("DEV") annotierte Bean nur in den Spring-Kontext geladen, wenn das Profil DEV aktiviert ist. Die Negation funktioniert auch, @Profile("!DEV") lädt die Bean nur, wenn das Profil DEV nicht aktiv ist.
Spring Boot wäre nicht Spring Boot, wenn es keine einfache Adaption für das automatisierte Testing gäbe. So setzt die Annotation @ActiveProfiles("DEV") das Profil DEV für den Test. Spring fährt in dieser Konfiguration den Kontext hoch, Parameter und Beans werden passend initialisiert.
Warnung
Spring Boot erlaubt mehrere parallel aktive Profile zur Laufzeit. Das kann das Management der Beans etwas unübersichtlich machen und viele Tests erfordern, um alle Szenarien abzudecken.
Apropos Tests: Ändert sich das Profil eines Tests im Vergleich zur zuvor ausgeführten Testklasse, muss der Spring-Kontext neu aufgebaut werden. Das kostet jedes Mal Zeit, häufige Profilwechsel führen daher zu einer deutlich längeren Laufzeit der Tests. Spring Boot cached Teile des Kontextes, um die Performance zu verbessern – im ungünstigsten Fall leider die falschen Teile. Im Zweifelsfall hilft ein Zurücksetzen des Context Caches auf 1 mit dem Parameter spring.test.context.cache.maxSize. Einen guten Einblick in das Context Caching bietet die Spring-Dokume.
Liebes Tagebuch, heute war ein fürchterlicher Tag. Ein Angreifer ist in unser Livesystem eingedrungen. Seltsam, unsere individuelle Login-Lösung lief eigentlich seit Jahren stabil…
Tipp 8: Spring Security einsetzen und mit @WithMockUser kombinieren
Nach viel Arbeit innerhalb des eigenen Projekt-Scopes richten wir den Blick nun auf die Außenwelt. Vorab jedoch ein Ratschlag, der sich nicht nur auf die Arbeit mit Spring Boot bezieht: Verschlüsselung, Datumsarithmetik und Security bitte niemals selbst implementieren! Viele haben es probiert und so gut wie alle sind gescheitert. Spring Boot und die verwendeten Frameworks werden seit Jahren entwickelt, erhalten fortlaufend Patches und sind praxiserprobt. Welches Softwareteam möchte sich anmaßen, diese Dinge besser zu machen?
Vor allem, weil Spring Security sehr leichtgewichtig daherkommt. In der einfachsten Form genügt es, die Methode loadUserByUsername in einer Implementierung des Interfaces UserDetailsService auszuprogrammieren: Gegeben ein Username, liefert die Implementierung ein UserDetails-Objekt und Spring Security kümmert sich um den Rest - Abgleich des Passworts nach bewährten Standards, Verwaltung des Users mit Rollen und Rechten im Security Kontext und natürlich wieder Hilfsmittel, die das Testen leichter machen.
Hier wählt Spring Security einen zunächst eigenwillig anmutenden Ansatz: Tests können, zum Beispiel mit der Annotation @WithMockUser, direkt im Security-Kontext eines gegebenen Users ausgeführt werden. Dies erleichtert das Setup ungemein, da über die Annotation direkt Rollen und Rechte gesetzt werden, um mit wenig Infrastruktur sehr viele unterschiedliche Fälle abtesten zu können.
@Test
@WithMockUser(username = “mail@example.com“, roles = "FINANCEVIEW")
public void callPage_onlyOneDate_authorizedUserView() throws Exception {
// do your tests here...
}
Warnung
Diese Abstraktion ist Fluch und Segen zugleich: In den allermeisten Fällen erleichtert sie die Arbeit ungemein, dennoch sollte sie durch Integrationstests, die Vorgänge wie Login und Logout prüfen, ergänzt werden.
Liebes Tagebuch, heute war ein guter Tag! Wir haben endlich unsere Datenzugriffe so umgestellt, dass wir alle Tabellen paginiert anfragen können! Ein halbes Jahr Arbeit, jetzt können wir die Früchte ernten!
Tipp 9: Spring Data JPA zum Datenzugriff
Die Stimmung bei Team Aurora wird besser: Es gibt ein Erfolgserlebnis beim Thema Datenzugriff. Die schlechte Nachricht: Vermutlich haben sie Konstrukte gebaut, die es im Spring-Ökosystem längst gibt. Die Dependency Spring Data JPA bindet den Object Relational Mapper JPA mit all seiner Leichtigkeit und dem doch beeindruckenden Funktionsumfang in das Projekt ein.
Von mächtigen Abfragen, die das Framework allein aus dem Namen der Query Method erstellt, bis zur Pagination, die zum Nulltarif kommt, erleichtert JPA die tägliche Arbeit mit relationalen Datenbanken.
Natürlich ist nicht alles Gold, was glänzt: Datenzugriffe sind schnell erstellt, aber auf produktiven Datenmengen schlagen dann auf einmal Effekte wie das N+1-Querys Problem zu. Es wird ausgelöst, weil JPA Daten im Hintergrund nachlädt und dabei viele einzelne Querys an die Datenbank schickt. Eine ausführliche Analyse des Problems findet sich hier [4].
Aber auch dafür bietet Spring Data JPA mit einer annotationsbasierten Adaption der EntityGraphs eine einfache Lösung. Kostprobe gefällig?
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = {"roles","department"})
Page<User> findByActiveTrue(Pageable pageable);
}
Die zwei Zeilen liefern ohne explizite Implementierung der Methode schon das Folgende:
- Man erhält einen Überblick über alle aktiven User, d.h. User, deren Eigenschaft active den Wert true hat.
- Das Ergebnis wird als Page-Objekt beliebiger Größe ausgeliefert.
- Im Pageable-Objekt versteckt sich noch die Möglichkeit, nach einer oder mehreren Eigenschaften zu sortieren.
- Die Rollen und die Abteilung werden in einer Query als Join mitgeladen.
Hand aufs Herz: Mit noch weniger Code ist das eigentlich nicht möglich, oder?
Tipp 10: Caching mit @Cacheable
Wenn Spring Data JPA oder externe API Calls das Entwicklungsteam an die Performance-Grenzen bringen, liefert Spring Boot mit der Annotation @Cacheable eine elegante Möglichkeit, Werte zwischenzuspeichern.
Das gesamte Feature wird mit @EnableCaching für den Kontext aktiviert und ab diesem Zeitpunkt gibt es eigentlich nur noch zwei Stolpersteine:
- Die Annotation @Cacheable muss auf der richtigen Ebene vergeben werden.
- Der Cache muss zu sinnvollen Zeitpunkten geleert werden.
Der erste Punkt ist ein generelles Thema in Spring Boot. Annotationen auf Methodenebene berücksichtigt das Framework nur, wenn sie an einer public-Methode hinterlegt sind und der Aufruf nicht aus der gleichen Bean heraus erfolgt. Das ist leider ein klassischer Fall von "muss man wissen". Entweder man investiert Stunden im Debugging und erinnert sich deshalb ewig daran, oder man liest die Info zufällig in einem Blogpost wie diesem und prägt es sich ein.
Das Zurücksetzen eines Caches ist kein Spring Boot-spezifisches Problem. Aber das Framework bietet verschiedene Zugänge: Vom direkten Zugriff auf den CacheManager über – wie könnte es anders sein – einen annotationsbasierten Weg.
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Cacheable(value = "EmployeeServiceImpl.getEmployee")
public Optional<Employee> getEmployee(Long id) { /* */ }
@CacheEvict(value = "EmployeeServiceImpl.getEmployee", allEntries = true)
public Employee saveEmployee(@NonNull Employee employee) { /* */ }
}
Natürlich lässt sich dieses Verhalten auch durch einen Unit-Test sicherstellen, alle notwendigen Konstrukte wurden weiter oben angerissen. Kleiner Tipp als Starthilfe: @MockitoSpyBean und die Methode verify von Mockito verwenden. Ansonsten bietet dieser Testfall [5] mit allen verwendeten Klassen eine gute Starthilfe.
Zehn Tipps – ein roter Faden
Die Liste an guten Tipps und kleinen Tricks für Spring Boot ist vermutlich unendlich lang. Doch zieht sich der Gedanke der Einfachheit und Wartbarkeit durch das Framework. Mit den hier aufgeführten Tipps gelingt es, diese Ideen in das eigene Projekt zu überführen, um Ballast loszuwerden. Ballast durch Code, den es bereits im Framework gibt, Ballast durch unnötig komplexe Implementierungen, Ballast durch Boilerplate Code. Code smells – ein Grundsatz, der nicht erst seit gestern gilt.
Mit der gekonnten Anwendung von Spring Boot entsteht weniger eigener Code. Die Codebase wird wartbarer, die Architektur besser und Teams können sich stärker auf die Fachlichkeit konzentrieren!
[1] Spring Boot Higher-level Database Migration Tool
[2] Beispielrepository via Gitlab/Mischok
[3] Spring Boot Frameworks - Externalized Configuration
[4] Beispiel:NPlusOneTest.java via Gitlab/Mischok
[5] Testfall vua Gitlab/Mischok












