<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Day 2: Intermediate Spring · Learn Spring Boot 4</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/index.html</link><description>Versioned APIs, null safety, security, observability, resilience, concurrency and deployment.</description><generator>Hugo</generator><language>en</language><atom:link href="https://learn-spring-boot-4.pages.dev/03-intermediate-spring/index.xml" rel="self" type="application/rss+xml"/><item><title>API Versioning</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/09-api-versioning/index.html</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/09-api-versioning/index.html</guid><description>Spring Boot 4 versions endpoints for you — no /v1/ copies of every controller, no manual header parsing.
Enable it spring.mvc.apiversion.default=1.0 spring.mvc.apiversion.use.header=X-API-Version Other strategies:
Property Client sends spring.mvc.apiversion.use.header=X-API-Version a header spring.mvc.apiversion.use.query-parameter=version ?version=1.1 spring.mvc.apiversion.use.path-segment=1 /api/1.1/books spring.mvc.apiversion.use.media-type-parameter=… Accept: application/json;version=1.1 Reactive apps use spring.webflux.apiversion.*.
Version a mapping @RestController @RequestMapping("/api/books/{id}") public class BookController { @GetMapping // any version — lowest priority Book get(@PathVariable long id) { … } @GetMapping(version = "1.1") // exactly 1.1 BookV1_1 get1_1(@PathVariable long id) { … } @GetMapping(version = "1.2+") // 1.2 and everything above BookV1_2 get1_2(@PathVariable long id) { … } } curl -H "X-API-Version: 1.2" localhost:8080/api/books/1 Matching picks the highest version at or below the requested one. A request above every declared version fails with NotAcceptableApiVersionException → 400.</description></item><item><title>Null Safety with JSpecify</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/10-null-safety/index.html</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/10-null-safety/index.html</guid><description>Spring Framework 7 and Spring Boot 4 annotate the whole portfolio with JSpecify. Your IDE and build can now tell you where a null can appear — before it appears at runtime.
The four annotations Annotation Meaning @NullMarked in this scope everything is non-null unless marked otherwise @Nullable this type may be null @NonNull this type is not null (rarely needed inside @NullMarked) @NullUnmarked opt a scope back out &lt;dependency&gt; &lt;groupId&gt;org.jspecify&lt;/groupId&gt; &lt;artifactId&gt;jspecify&lt;/artifactId&gt; &lt;/dependency&gt; Mark your packages src/main/java/com/example/demo/package-info.java:</description></item><item><title>Security</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/11-security/index.html</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/11-security/index.html</guid><description>Add the starter &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; Every endpoint is now protected, and a generated password is printed at startup. That default is a reminder to configure it, not a setup.
A filter chain @Configuration @EnableWebSecurity public class SecurityConfig { @Bean SecurityFilterChain api(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -&gt; auth .requestMatchers("/actuator/health", "/public/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/books/**").hasRole("USER") .requestMatchers("/api/**").hasRole("ADMIN") .anyRequest().authenticated()) .csrf(csrf -&gt; csrf.disable()) // stateless API with tokens only .httpBasic(Customizer.withDefaults()) .build(); } } Rules are matched top to bottom — put the specific ones first.</description></item><item><title>Actuator &amp; Observability</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/12-actuator/index.html</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/12-actuator/index.html</guid><description>Actuator &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt; &lt;/dependency&gt; Only /actuator/health is exposed over HTTP by default. Open more explicitly:
management.endpoints.web.exposure.include=health,info,metrics,env,loggers management.endpoint.health.show-details=when-authorized Endpoint Shows /actuator/health up/down, plus per-component checks /actuator/info build and git info /actuator/metrics counters, gauges, timers /actuator/loggers log levels, changeable at runtime /actuator/env resolved configuration /actuator/prometheus metrics in Prometheus format curl localhost:8080/actuator/health curl localhost:8080/actuator/metrics/http.server.requests Warning env, heapdump and threaddump leak internals. Expose them only behind authentication, or move the whole actuator to its own port: management.server.port=9001.</description></item><item><title>Resilience</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/13-resilience/index.html</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/13-resilience/index.html</guid><description>Retries and concurrency limits moved into the core framework in Spring 7 — no extra project, no Resilience4j needed for the common cases.
Enable it @Configuration @EnableResilientMethods class ResilienceConfig {} @Retryable import org.springframework.resilience.annotation.Retryable; @Retryable public void sendNotification() { … } // 3 retries, 1 s apart @Retryable( includes = RemoteServiceException.class, maxRetries = 4, delay = 100, // ms jitter = 10, // random spread, avoids thundering herds multiplier = 2, // exponential backoff: 100, 200, 400, 800 maxDelay = 1000) public Book fetch(long id) { … } Attribute Default Meaning maxRetries 3 attempts after the first one delay 1000 milliseconds before retrying jitter 0 random addition to the delay multiplier 1 backoff factor maxDelay – cap for the growing delay includes / excludes all / none which exceptions to retry Reactive return types are retried too:</description></item><item><title>Async, Scheduling &amp; Virtual Threads</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/14-async-threads/index.html</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/14-async-threads/index.html</guid><description>Virtual threads One property, and every request plus every @Async task runs on a virtual thread:
spring.threads.virtual.enabled=true Blocking code stops being expensive: a thread parked on I/O costs almost nothing, so an MVC app handles thousands of concurrent requests without going reactive.
Two rules Do not pool virtual threads — create one per task. And avoid synchronized around blocking calls; use ReentrantLock instead.</description></item><item><title>Caching &amp; Messaging</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/15-caching-messaging/index.html</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/15-caching-messaging/index.html</guid><description>Caching @Configuration @EnableCaching class CacheConfig {} @Service public class BookService { @Cacheable("books") public Book byId(long id) { … } // called once per id @CachePut(value = "books", key = "#book.id") public Book update(Book book) { … } // always runs, refreshes the entry @CacheEvict(value = "books", key = "#id") public void delete(long id) { … } @CacheEvict(value = "books", allEntries = true) public void reload() { … } } Keys default to the method arguments; key = "#id" or a SpEL expression overrides that.</description></item><item><title>Packaging &amp; Deployment</title><link>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/16-deployment/index.html</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://learn-spring-boot-4.pages.dev/03-intermediate-spring/16-deployment/index.html</guid><description>The executable jar ./mvnw clean package java -jar target/demo-0.0.1-SNAPSHOT.jar One file with your classes, the dependencies and a launcher. No application server.
java -jar app.jar --server.port=9000 --spring.profiles.active=prod java -Dspring.profiles.active=prod -jar app.jar SPRING_PROFILES_ACTIVE=prod java -jar app.jar Container image, no Dockerfile ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=demo:1.0 docker run -p 8080:8080 demo:1.0 Cloud Native Buildpacks pick a JDK, layer the image and set sane defaults.</description></item></channel></rss>