Almost one year after the release of Spring Boot 2.1, we get a new release: Spring Boot 2.2. The Spring Boot team continues to further improve performance, support the latest Java version and provide useful features. As there are a lot of updates with this release, I'm focussing on the most (from my point of view) important ones. For one of my projects we upgraded right after the release date and already use some features in production. You can find the full list of updates, new and noteworthy things, and deprecations, at the official release notes on GitHub.
Lazy initialization of beans in Spring Boot 2.2
You might remember the lazy initialization option for JPA repositories which was introduced with Spring Data Lovelace and Spring Boot 2.1 last year. One of the main reasons for this is to reduce startup time and initialize the repositories once they are requested. With Spring Boot 2.2 we can now configure global lazy initializations of our beans (not only JPA repositories) with:
1 | spring.main.lazy-initialization=true |
Single beans can opt-out of this configuration with the @Lazy(false)
annotation or while using a LazyIntializationExcludeFilter
.
To provide you an example, I'm using a REST controller which injects a bean to provide data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @RestController public class SampleEndpoint { private SampleService sampleService; private SampleEndpoint(SampleService sampleService) { this.sampleService = sampleService; } @PostConstruct public void init() { System.out.println("SampleEndpoint is now initialized"); } @GetMapping("/hello") public ResponseEntity<String> sayHello() { return ResponseEntity.ok(sampleService.getMessage()); } } |
Next, the SampleService
opts-out for the global lazy initialization:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Service @Lazy(false) public class SampleService { @PostConstruct public void init() { System.out.println("SampleService is now initialized"); } public String getMessage() { return "Hello World"; } } |
When we now start the application, we see that the SampleService
is initialized right ahead and our SampleEndpoint
bean only once someone requests http://localhost:8080/hello
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.0.RELEASE) 2019-10-21 05:36:51.229 INFO 9203 --- [ main] de.rieckpil.blog.Application : Starting Application on duke with PID 9203 (/home/rieckpil/Development/git/blog-tutorials/whats-new-in-spring-boot-2.2/target/classes started by rieckpil in /home/rieckpil/Development/git/blog-tutorials/whats-new-in-spring-boot-2.2) 2019-10-21 05:36:51.232 INFO 9203 --- [ main] de.rieckpil.blog.Application : No active profile set, falling back to default profiles: default 2019-10-21 05:36:51.525 INFO 9203 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode. 2019-10-21 05:36:51.536 INFO 9203 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 7ms. Found 0 repository interfaces. 2019-10-21 05:36:51.694 INFO 9203 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2019-10-21 05:36:51.825 INFO 9203 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2019-10-21 05:36:51.830 INFO 9203 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2019-10-21 05:36:51.830 INFO 9203 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.27] 2019-10-21 05:36:51.870 INFO 9203 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2019-10-21 05:36:51.870 INFO 9203 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 616 ms 2019-10-21 05:36:51.917 INFO 9203 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2019-10-21 05:36:51.955 INFO 9203 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2019-10-21 05:36:51.972 INFO 9203 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2019-10-21 05:36:52.000 INFO 9203 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {5.4.6.Final} 2019-10-21 05:36:52.042 INFO 9203 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.0.Final} 2019-10-21 05:36:52.077 INFO 9203 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect 2019-10-21 05:36:52.165 INFO 9203 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] 2019-10-21 05:36:52.168 INFO 9203 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' SampleService is now initialized 2019-10-21 05:36:52.220 WARN 9203 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. 2019-10-21 05:36:52.238 INFO 9203 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2019-10-21 05:36:52.240 INFO 9203 --- [ main] de.rieckpil.blog.Application : Started Application in 1.194 seconds (JVM running for 1.444) 2019-10-21 05:36:58.338 INFO 9203 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2019-10-21 05:36:58.338 INFO 9203 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2019-10-21 05:36:58.432 INFO 9203 --- [nio-8080-exec-1] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2019-10-21 05:36:58.458 INFO 9203 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 120 ms SampleEndpoint is now initialized |
Please note that this new feature comes at costs as the initial HTTP request might take longer due to deferred initialization. Furthermore, you detect failures while initializing your bean only during your application's runtime and not at startup time.
JUnit 5 by default
With Spring Boot 2.2 and spring-boot-starter-test
we now get JUnit 5 by default. It's still possible to use JUnit 4 tests and also a mixture of JUnit 4 & 5 with the JUnit 5 vintage engine. Both versions are part of the spring-boot-starter-test
dependency without further configuration.
However, if you plan to enforce the usage of only JUnit 5 for your application, you can exclude the junit-vintage-engine
. This will also exclude the JUnit 4.12 dependency:
1 2 3 4 5 6 7 8 9 10 11 | <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> |
The pre-generated @SpringBootTest
you get while using the Spring Initializr then needs to be re-written for JUnit 5:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @SpringBootTest public class ApplicationTests { @Test public void contextLoads() { } } |
Performance improvements with Spring Boot 2.2
With this Spring Boot release, there are several performance improvements besides the lazy initialization feature:
- Hibernate’s entity scanning is now disabled as Spring Boot fully prepares a
PersistenceUnit
by scanning JPA entities - Binding large numbers of configuration properties are now way faster
- Injection points in auto-configurations have been refined to only apply when a bean has to be created
- Beans related to Actuator endpoints are now only created if the endpoint is both enabled and exposed (via JMX or HTTP)
- The HTTP trace and auditing Actuator features are not enabled by default anymore to reduce memory consumption while using the default implementations
- Tomcat’s MBean Registry is disabled by default, saving approximately 2MB of Heap memory
Further updates with this release
RSocket support
RSocket is now a first-class citizen for your Spring Boot application. There is now a dedicated Spring Boot starter for RSocket: spring-boot-starter-rsocket
. With this starter, you get all the required dependencies for building an application with RSocket. Similarly, the RSocket strategies are auto-configured to provide the required infrastructure for encoding and decoding RSocket payloads using CBOR and JSON.
Furthermore, you'll get auto-configuration for Spring Security if the RSocket security module is on the classpath.
Actuator updates
The JSON format for the /actuator/health
endpoint has changed by renaming the top-level details
attribute to components
. This helps to differentiate the actual details returned by a HealthIndicator
as they also contain a details
field. To use the old JSON format, you can request it with an HTTP accept header with the V2 media type, application/vnd.spring-boot.actuator.v2+json
. The new media type is application/vnd.spring-boot.actuator.v3+json
/actuator/health
. The management.endpoint.health.show-components
property works in a similar way to show-details
. The available values are never
, when_authorized
or always
.You can use this, for example, to alway show the components of your health check, but the details only to authorized users:
1 2 | management.endpoint.health.show-components=always management.endpoint.health.show-details=when_authorized |
A non-authorized user then gets the following output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | { "status": "UP", "components": { "custom-livness": { "status": "UP" }, "db": { "status": "UP" }, "diskSpace": { "status": "UP" }, "ping": { "status": "UP" } }, "groups": [ "liveness" ] } |
Whereas an authorized user gets details about each component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | { "status": "UP", "components": { "custom-livness": { "status": "UP" }, "db": { "status": "UP", "details": { "database": "H2", "result": 1, "validationQuery": "SELECT 1" } }, "diskSpace": { "status": "UP", "details": { "total": 490652508160, "free": 344082546688, "threshold": 10485760 } }, "ping": { "status": "UP" } }, "groups": [ "liveness" ] } |
It is now possible to group health indicators and organize them with a logical name. A good example of this feature is to differentiate between liveness and readiness checks.
Imagine you use a custom HealthIndicator
to check the liveness of your application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Component("custom-liveness") public class CustomLivenessCheck implements HealthIndicator { @Override public Health health() { if (checkLiveness()) { return Health.up().build(); } return Health.down().withDetail("Error Code", 42).build(); } private boolean checkLiveness() { return ThreadLocalRandom.current().nextBoolean(); } } |
You can now configure a liveness
group with your custom check and the DataSource
check like the following:
1 | management.endpoint.health.group.liveness.include=db,custom-liveness |
The liveness
group is then available at localhost:8080/actuator/health/liveness:
1 2 3 4 5 6 7 8 9 10 11 | { "status": "DOWN", "components": { "custom-liveness": { "status": "DOWN" }, "db": { "status": "UP" } } } |
Kubernetes detection
You are now able to detect if your application is running inside a Kubernetes cluster with the ConditionalOnCloudPlatform
annotation:
1 2 3 4 5 6 7 8 9 10 | @Service @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) public class KubernetesDetector implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("--- You are running on KUBERNETES ;)"); } } |
Support for Java 13
Spring Boot 2.2 now supports Java 8, 11, and 13.
Dependency upgrades with Spring Boot 2.2
Besides other dependency upgrades, theses are one of the most important ones:
- Spring Data Moore 2.2.0: Further reactive support (transactions, Querydsl, etc.) and much more
- Reactor Dysprosium 3.3.0: A lot to discover, take a look at the official release notes
- Spring Framework 5.2: aka Core Container Revisited, find a great talk by Juergen Hoeller itself here
- Flyway 6.0: Start using the latest database (PostgreSQL 11 & 12, SQL Server 2019, etc.) versions and get now five years of support starting with their release (both for Community and Pro-Edition). Find further updates here
- Spring HATEOAS 1.0: If you are looking for a great introduction to this topic, take a look at Oliver Drotbohm's talk on SpringOne
- Spring Security 5.2: Great improvements for OAuth 2.0 (both client & resource server)
- Jackson 2.10: This fixes some of the CVEs with previous versions of Jackson
- … and much more
If you are looking for the official release notes, have a look at the wiki page of Spring Boot.
You can find the source code for this example on GitHub.
Have fun using Spring Boot 2.2,
Philip