Spring: Alles neu macht der November
Das Spring Framework, Spring Boot und das gesamte Ökosystem entwickeln sich ständig weiter und bieten kontinuierlich Verbesserungen sowie neue Funktionen, die darauf abzielen, die Produktivität der Entwickler:innen und die Leistung der Anwendungen zu optimieren. Mit dem aktuellen Release erwarten uns spannende Änderungen, die wir im Detail analysieren wollen – von Optimierungen im Core Container über Anpassungen der Autokonfiguration bis hin zu einem sanfteren Shutdown-Prozess und weiteren aufregenden Neuerungen im Spring-Ökosystem. Spring Framework 6.2 und Spring Boot 3.4 im Fokus: Wir zeigen die wichtigsten Neuerungen und Updates für deinen Code.
Ein weiterer bedeutender Fortschritt für das Spring Framework ist da: Spring Framework 6.2 ist seit November 2024 allgemein verfügbar und bringt umfassende Überarbeitungen im Core Container sowie in der Webunterstützung mit sich. Dieses neue Release ist für den Einsatz mit JDK 17 bis 23 und Jakarta EE 9 bis 10 konzipiert [1].
Die Mindestanforderungen für das neue Spring-Boot-Release bleiben unverändert. Ist das aktuelle Set-up bereits mit Spring Boot 3.x kompatibel, kann ohne Bedenken auf die neueste Version angepasst werden, ohne größere Kompatibilitätsprobleme befürchten zu müssen [2].
Property-Platzhalter bieten eine elegante Möglichkeit, Umgebungsvariablen in beliebigen Strings zu ersetzen. Nehmen wir an, dass in dem Environment die Property customer.name den Wert John Developer hat, dann würde Customer ${customer.name} zu Customer John Developer aufgelöst werden. Es gibt jedoch Anwendungsfälle, bei denen man den ursprünglichen Platzhalter beibehalten möchte, anstatt ihn aufzulösen.
Die neue Version des Frameworks führt hierfür einen konfigurierbaren Escape-Mechanismus ein, der standardmäßig den Backslash als Escape-Zeichen verwendet. Greifen wir das vorherige Beispiel auf: Der Ausdruck Customer \${customer.name} wird nun zu Customer ${customer.name} aufgelöst – der Platzhalter bleibt also erhalten.
Die neue Version des Core-Frameworks bringt einen leicht überarbeiteten Autowiring-Algorithmus mit sich. In diesem neuen Ansatz haben unter einer Gruppe von Kandidaten-Beans, die typbasiert übereinstimmen, die Übereinstimmungen bei den Parameternamen und die Verwendung von @Qualifier Vorrang vor der @jakarta.annotation.Priority-Rangfolge. Zuvor war es genau umgekehrt. Es sei jedoch angemerkt, dass nicht empfohlen wird, diese Qualifikationsmechanismen zu mischen und @Priority nicht für die Identifikation einzelner Kandidaten, sondern nur zur Rangordnung mehrerer Kandidaten in einer injizierten Sammlung zu verwenden. Beans, die mit @Primary markiert sind, haben stets Priorität.
Ein weiteres neues Feature ist die erweiterte generische Typübereinstimmung. Spring ist jetzt weniger nachsichtig bei der Akzeptanz von Fallback-Übereinstimmungen und besteht darauf, dass der auflösbare Teil der Typsignatur übereinstimmt, selbst wenn der verbleibende Teil nachsichtig bei nicht auflösbaren Typvariablen oder Wildcards akzeptiert wird.
Komponentenscans finden früh in der Initialisierung der BeanFactory statt und sind daher nicht geeignet, um durch Bedingungen geschützt zu werden, die spät ausgewertet werden. Wenn man @ComponentScan mit einer REGISTER_BEAN-Bedingung, wie Spring Boots @ConditionalOnBean verwendet, schlägt das nun fehl.
Darüber hinaus haben wir klargestellt, dass das Überschreiben von Bean-Definitionen in Produktionscode nicht empfohlen wird. Der Container protokolliert jede Überschreibung auf INFO-Log-Level.
Fallback Beans kommen dann zum Einsatz, wenn keine andere Bean des entsprechenden Typs bereitgestellt wurde. Das ist quasi das Gegenstück zu @Primary, ohne dessen typische Nachteile bei der Bean-Deklaration. Betrachten wir folgendes Szenario: Eine Komponente benötigt eine definierte ExampleService Bean. Dabei möchte man eine Standardimplementierung des Service bereitstellen, aber gleichzeitig dem Benutzer die Möglichkeit geben, eine eigene Implementierung transparent per Type Injection einzubinden. Bisher musste der Benutzer seine spezifische Bean mit @Primary annotieren, um sicherzustellen, dass diese verwendet wird, da nun zwei Beans desselben Typs definiert sind. Diese Konfiguration lässt sich nun, wie in Listing 1 gezeigt, elegant mittels @Fallback umsetzen.
Listing 1: Konfiguration einer Fallback Bean
@Configuration
class ExampleConfiguration {
@Bean
MyComponent exampleComponent(ExampleService service) {
//...
}
@Bean
@Fallback
ExampleService defaultExampleService() {
//...
}
}
Wenn keine andere ExampleService Bean definiert ist, wird defaultExampleService verwendet. Andernfalls wählt der Container automatisch und transparent die extern definierte Bean aus. Das vereinfacht die Konfiguration erheblich, da keine zusätzlichen Annotationen bei der benutzerdefinierten Implementierung erforderlich sind.
Das Spring Framework erweitert nun auch die Bean-Konfiguration um eine neue Möglichkeit zur Performanceoptimierung: Einzelne Beans können jetzt mittels des neu eingeführten Bootstrap-Attributs im Hintergrund initialisiert werden (Listing 2).
Listing 2: Konfiguration einer Fallback Bean
@Configuration
class ExampleConfiguration {
@Bean(bootstrap = BACKGROUND)
ExpensiveComponent myComponent() {
//...
}
}
Diese Funktionalität ist besonders nützlich bei ressourcenintensiven Komponenten, die nicht unmittelbar beim Anwendungsstart benötigt werden. Durch die Verlagerung der Initialisierung in den Hintergrund kann die Startzeit der Anwendung signifikant reduziert werden. Die Bean-Definition erfolgt weiterhin deklarativ und transparent, lediglich ergänzt um das Bootstrap-Attribut, das die Ausführungsstrategie steuert.
Ebenfalls neu sind elegante Möglichkeiten, Beans in Integrationstests gezielt zu überschreiben. Während das Überschreiben von Beans im Produktivcode nicht empfohlen wird, ist es in Testszenarien durchaus legitim und oft notwendig. Die neuen Features ermöglichen dabei eine präzise Kontrolle über das Bean Overriding in Tests. Mit der neuen Annotation @TestBean lässt sich eine Bean einfach und transparent überschreiben (Listing 3).
Listing 3: Überschreiben einer Bean mit @TestBean
@Configuration
class ProductionConfiguration {
@Bean
ExampleService exampleService() {
return new DefaultExampleService();
}
}
@SpringJUnitConfig
class MyServiceIntegrationTests {
@TestBean
ExampleService exampleService;
static ExampleService exampleServiceTestOverride() {
return new SimplifiedExampleService();
}
@Test
void test(ApplicationContext context) {
assertThat(context.getBean("exampleService")
.isSameAs(this.exampleService)
.isInstanceOf(SimplifiedExampleService.class);
//...
}
}
Der Name des annotierten Feldes wird dabei als Bean-Name interpretiert. Optional kann über das Attribut beanName ein abweichender Name angegeben werden. Die Factory-Methode folgt standardmäßig der Namenskonvention {beanName}TestOverride, kann aber über das methodName-Attribut angepasst werden.
Für Mock-basiertes Testing bietet Spring zusätzlich eine nahtlose Mockito-Integration mit zwei spezialisierten Annotationen, wie in Listing 4 beschrieben.
Listing 4: Verwendung von Mockito-Annotationen
@SpringJUnitConfig
class ExampleServiceIntegrationTests {
@MockitoSpyBean(reset = MockReset.NONE)
ExampleService exampleService; // Erzeugt einen Spy der originalen Bean
@MockitoBean
AnotherService anotherService; // Ersetzt die Bean durch einen Mock
@Test
void testService() {
// Test-Implementation mit Mock/Spy
}
}
@MockitoBean ersetzt automatisch die Ziel-Bean durch einen Mock, während @MockitoSpyBean die originale Bean in einen Spy einpackt. Beide Annotationen bieten zusätzliche Konfigurationsmöglichkeiten, wie etwa das Verhalten beim Reset zwischen Tests. Standardmäßig werden Mocks und Spies nach jedem Testdurchlauf zurückgesetzt, was sich aber über das reset-Attribut anpassen lässt.
Diese neuen Funktionen erleichtern das Testen von Spring-Anwendungen erheblich und bieten dabei eine gute Balance zwischen Flexibilität und Kontrolle. Sie ermöglichen es Entwickler:innen, präzise und explizit Beans in Testszenarien zu ersetzen, während unbeabsichtigtes Überschreiben im Produktivcode verhindert wird.
Spring Framework erweitert nun seine Testunterstützung um AssertJ-Integration für MockMvc (Listing 5). Während Spring Boot bereits seit einiger Zeit AssertJ nutzt, war das Framework-Team bisher zurückhaltender. Die bisherige Hamcrest-Unterstützung hat ihre Grenzen, insbesondere bei der API-Auffindbarkeit durch statische Imports und der Erstellung benutzerdefinierter Assertions.
Listing 5: Einfache Instanziierung eines MvcTesters
MvcTester mvc = MvcTester.from(webApplicationContext);
// Oder für Unit-Tests einzelner Controller
MvcTester mvc = MvcTester.of(List.of(new ExampleController()), builder ->
builder.defaultRequest(get("/ping")
.accept(MediaType.APPLICATION_JSON))
.build());
Die Testausführung erfolgt mit der gewohnten AssertJ-Syntax, die nun ein intuitiveres und flüssigeres API bietet (Listing 6).
Listing 6: Ausführung von Tests mit AssertJ-Assertions
assertThat(mvc.perform(get("/vehicle/{id}", "12")
.accept(MediaType.TEXT_PLAIN)))
.hasStatusOk()
.body().isEqualTo("Honda Civic");
Besonders hervorzuheben ist die erweiterte JSON-Unterstützung, die in Listing 7 zu sehen ist.
Listing 7: Erweiterte JSON-Validierung
assertThat(mvc.perform(get("/message")))
.body().json()
.isLenientlyEqualTo(new ClassPathResource("samples/message.json"));
Diese neue Integration bietet nicht nur alle Features der bekannten Hamcrest-Matcher, sondern erweitert diese um fortgeschrittene JSON-Funktionalitäten. Die AssertJ-basierte Syntax ermöglicht dabei eine bessere IDE-Unterstützung und macht das Schreiben von Tests intuitiver und lesbarer.
Das Spring Framework erweitert die Funktionalität von @ExceptionHandler-Methoden um Content Negotiation während der Fehlerbehandlung (Listing 8). Dies ermöglicht eine differenzierte Fehlerbehandlung je nach angefordertem Content-Type des HTTP-Clients.
Listing 8: Content-Type-spezifische Fehlerbehandlung
@ExceptionHandler(produces = "application/json")
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
return ResponseEntity.badRequest()
.body(new ErrorMessage(exc.getMessage(), 42));
}
@ExceptionHandler(produces = "text/html")
public String handle(IllegalArgumentException exc, Model model) {
model.addAttribute("error",
new ErrorMessage(exc.getMessage(), 42));
return "errorView";
}
Das wiederum ermöglicht eine flexiblere und clientspezifischere Fehlerbehandlung, was besonders in modernen Anwendungen mit verschiedenen Clienttypen von Vorteil ist.
Neue Annotationen helfen bei der Registrierung von Reflection-Hints für Native-Anwendungen deutlich zu vereinfachen. Diese Verbesserungen sind besonders relevant für Entwickler:innen, die ihre Spring-Anwendungen als Native-Anwendungen kompilieren möchten.
Listing 9: Beispiel für Reflection-Hint-Registrierung
// Direkte Registrierung für einzelne Typen
@Configuration
@RegisterReflection(classes = ExampleService.class, memberCategories =
{ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS })
class ExampleConfiguration {
}
// Scanning für Reflection-Hints in beliebigen Klassen
@ReflectionScan("com.example.app")
class ExampleConfiguration {
// ...
}
Die neue Annotation @RegisterReflection ermöglicht eine deklarative und unkomplizierte Registrierung von Reflection Hints für beliebige Datentypen (Listing 9). Mit @ReflectionScan lässt sich das Scanning von Reflection Hints auf beliebige Klassen ausweiten – nicht nur auf Spring Beans. Dies vereinfacht die Entwicklung von Native-Anwendungen erheblich und reduziert den manuellen Konfigurationsaufwand.
Nachdem wir die Änderungen im Spring Framework besprochen haben, geht es nun um Spring Boot. In dieser neuen Version von Spring Boot wurden viele Verbesserungen und neue Features eingeführt, die darauf abzielen, die Entwicklung und Wartung von Anwendungen zu vereinfachen.
Spring Boot 3.4 bringt eine wesentliche Änderung bei der Erstellung von OCI Images mit sich: Der Standard-Builder wurde von paketobuildpacks/builder-jammy-base auf paketobuildpacks/builder-jammy-tiny umgestellt. Diese Anpassung resultiert in deutlich kleineren Images, was sich positiv auf die Bereitstellungszeiten sowie den Ressourcenverbrauch auswirkt.
Um schnellere Startzeiten und eine effizientere Auflösung von WebJars Assets zu erreichen, muss man die pom.xml- oder build.gradle-Datei aktualisieren, um von org.webjars:webjars-locator-core auf org.webjars:webjars-locator-lite umzustellen. Beide Abhängigkeiten werden von Spring Boot verwaltet. Man sollte beachten, dass die Unterstützung für org.webjars:webjars-locator-core in einer zukünftigen Version entfernt wird.
Spring Boot unterstützt jetzt strukturiertes Logging (Listing 10) mit integrierter Unterstützung für die Formate Elastic Common Schema (ECS), Logstash, Graylog Extended Log Format (GELF) und benutzerdefiniertes JSON. Um strukturiertes Datei-Logging zu aktivieren, kann beispielsweise logging.structured.format.file auf ecs, logstash, gelf oder json gesetzt werden. Ähnlich kann auch strukturiertes Konsolen-Logging aktiviert werden, indem logging.structured.format.console auf einen der unterstützten Werte gesetzt wird.
Listing 10: Structured Logging
// Properties zur Konfiguration des Loggers
logging.structured.format.console=ecs
logging.structured.format.file=ecs
// Console / File Log-Output
{"@timestamp":"2024-10-31T11:11:00.042464855Z","log.level":"INFO","process.pid":31142,"process.thread.name":"main","service.name":"exampleService","log.logger":"com.example.Application","message":"No active profile set, falling back to 1 default profile: \"default\"","ecs.version":"8.11"}
Spring Boot bietet nun eine elegante Lösung für ein kontrolliertes Herunterfahren von Anwendungen. Beim Graceful Shutdown werden laufende Aufgaben abgeschlossen, bevor die Anwendung vollständig beendet wird. Das ist besonders wichtig, um Datenkonsistenz sicherzustellen und Ressourcen effizient zu verwalten. Das Graceful Shutdown des eingebetteten Webservers (Jetty, Reactor Netty, Tomcat oder Undertow) ist jetzt standardmäßig aktiviert. Um das vorherige Verhalten wiederherzustellen, kann server.shutdown auf immediate gesetzt werden.
Spring Boot 3.4 bietet nun Unterstützung für die automatische Konfiguration von RestClient und RestTemplate, sodass sowohl Reactor Netty's HttpClient als auch der HttpClient des JDK verwendet werden können. Die unterstützten Clients werden in folgender Reihenfolge bevorzugt:
Apache HTTP Components (HttpComponentsClientHttpRequestFactory)
Jetty Client (JettyClientHttpRequestFactory)
Reactor Netty HttpClient (ReactorClientHttpRequestFactory)
JDK HttpClient (JdkClientHttpRequestFactory)
Simple JDK HttpURLConnection (SimpleClientHttpRequestFactory)
Falls keine HTTP-Client-Bibliothek im Classpath vorhanden ist, wird standardmäßig der JdkClientHttpRequestFactory verwendet – anstelle des vorherigen SimpleClientHttpRequestFactory. Um einen bestimmten Client auszuwählen, kann die Einstellung spring.http.client.factory verwendet werden, wobei die unterstützten Werte http-components, jetty, reactor, jdk und simple sind. Standardmäßig folgen alle fünf Clients Redirects. Um dieses Verhalten zu deaktivieren, setzt man spring.http.client.redirects auf dont-follow.
Neben den bereits erwähnten Neuerungen in Spring Framework 6.2 und Spring Boot 3.4 wurden zahlreiche Abhängigkeiten aktualisiert, um sicherzustellen, dass alle verwendeten Bibliotheken auf dem neuesten Stand sind. Das umfasst insbesondere auch die Spring Boot Starter.
Ein weiterer wichtiger Punkt ist die Anhebung der Mindestversion für Gradle 8.x auf 8.4, während der Support für Gradle 7.x, konkret 7.6.4, weiterhin besteht. Diese Änderung fördert nicht nur die Nutzung moderner Funktionen, sondern verbessert auch die Build-Zeit und die allgemeine Stabilität des Projekts.
Darüber hinaus wurden viele kleinere Anpassungen vorgenommen, die darauf abzielen, die Benutzerfreundlichkeit und Leistung von Spring Boot weiter zu optimieren. Diese Verbesserungen tragen dazu bei, dass Entwickler:innen effizienter arbeiten und die Vorteile der neuesten Technologien sowie Best Practices nutzen können.
[1] Spring Framework 6.2 Release Notes: https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-6.2-Release-Notes
[2] Spring Boot 3.4 Release Notes: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.4-Release-Notes