Nedeljko Kovacevic
1 article
February 24, 2025
Software Development
Java 17 and Spring Boot 3: Upgrade Roadmap
Introduction It has been a decade since Java 8 was released, and today's world of technology feels like a century. The newer versions of Java brought many improvements, including performance optimization, better memory management, improved security features, and new syntax solutions. Migration to a newer version of Java isn't a one-click job and can be a real struggle and a significant burden on every team's capacity. This article is written to help you humble the effort by giving you some handy bits of advice. Reasons to migrate from Java 8 to Java 17 Why should anyone bother upgrading to a higher Java version? It's a valid question, especially if your applications run perfectly well on Java 8, Java 11, Java 14, or another version. However, migrating from Java 8 to Java 17 brings performance enhancements, improved language features, stronger security, and long-term support. Improved Language Features Java 17 introduces several new language features, such as records, pattern matching for instanceof, and switch expressions, which make code easier to read and maintain. Pattern Matching (JEP 441): Between Java 8 and Java 17, pattern matching emerged, enabling developers to write cleaner and more concise code. More about pattern matching is available in the previous ABH tech bite article, Pattern Matching for instanceof and Switch in Java. Records (JEP 395): Java record is a restricted form of a class. The main goal when the records were introduced was to provide a nominal object-oriented immutable tuple-like structure. Reducing boilerplate code when creating POJOs is a welcome side effect. Simple POJO class: public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{name='" + name + "', age=" + age + "}"; } } Equivalent Java Record: //Equivalent Java Record public record Person(String name, int age) { } This one line of code generates the following: Final fields name and age A constructor that initializes these fields Accessor methods name() and age() equals() and hashCode() methods for object comparison A toString() method The Java Platform Module System (JPMS) (JSR 376): Introduced in Java 9, lets developers organize code into modules, clearly defining which parts are visible to other modules and what dependencies are needed. This makes applications lighter, reduces version conflicts, and improves security by controlling access to different parts of the code. Before Java 9, you could call public methods from any class directly or through reflection without much restriction. But with JPMS, there are new rules, especially around reflective access. The Reflection API's setAccessible() method is used to bypass access checks, but now modules can decide what parts of their code are open for reflection using the opens keyword. This lets developers allow reflective access to certain packages without fully exposing them. At first, in Java 9, illegal reflective access just triggered warnings. By Java 16, these were blocked by default, though you could still allow them with the --illegal-access flag. But in Java 17, that flag was removed, meaning stricter security is now fully enforced, and illegal reflective access is no longer allowed. Performance enhancements Java 17 provides significant performance improvements, including reduced JVM startup time and more efficient memory management through features like ahead-of-time (AOT) compilation and new garbage collectors. Developers need to explicitly configure and use tools like Java Ahead-Of-Time Compiler to generate native code. These enhancements result in faster startups, efficient memory usage, and better responsiveness under heavy loads. Security improvement Java 17 introduces several security improvements, including enhanced TLS support, stronger encryption algorithms, and more robust authentication mechanisms, making applications more secure and less vulnerable to threats. The butterfly effect of Java upgrade Even though Spring Boot 2.5.x supports Java 17, upgrading to Spring Boot 3.x and Spring Framework 6.x is a logical step. Spring Boot 3.x requires Java 17 as the minimum version. Upgrading Spring Framework and Spring Boot may trigger a cascade of dependencies, requiring updates to libraries like Spring Kafka, Spring Retry, Spring Security, Hibernate, Apache HttpClient, Flyway, and Netty, among others. A complete list of dependencies for Spring Boot 3.x is available in the official guide. Steps Ensure Java 17 is installed. Upgrade to the Latest 2.7.x Version Before upgrading to version 3 it is always a good starting point to upgrade to the latest minor version. This approach will also make sure that you are building against the most recent dependencies of that line. Review Deprecations from Spring Boot 2.x Compare dependency management for 2.7.x with dependency management for 3.0.x to find out what are the major dependency changes in your application and if there are any breaking changes. Also, classes, methods, and properties that were deprecated in Spring Boot 2.x have been removed in this release. Please ensure that you aren’t calling deprecated methods before upgrading. Upgrade to SpringBoot 3.x and Spring Framework 6.x Finally, upgrade your application to these versions. Configuration Properties Migration Some of the configuration properties were renamed or removed, so application.properties and application.yml need to be changed as well. Spring Boot provides a spring-boot-properties-migrator module. Once we add this module as a dependency to our project, this will temporarily migrate properties at runtime for us. Apart from that, it will also analyze our application’s environment and print diagnostics at startup. You can add the migrator by adding the following to your Maven pom.xml: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope> </dependency> or if you use Gradle: runtimeOnly("org.springframework.boot:spring-boot-properties-migrator") Update packages starting from ‘javax’ to ‘jakarta’ Java EE has been changed to Jakarta EE, so the Spring Boot did the same. All package names starting with ‘javax’ need to be changed to ‘jakarta’ accordingly, except javax.sql.* and javax.crypto.* because they are part of Java 17 JDK, not of Java EE. javax.persistence.* -> jakarta.persistence.* Of course, those are just basic steps, you can find more on the official migration guide for SpringBoot 3.0. Some of the potential issues are also discussed in the following chapter. Potential issues and challenges Path Mappings. Let’s consider the case where you have defined a controller like this: @RestController public class TechBiteController { @GetMapping("/abh/techbite") public String getTechBite() { return "ABH TechBite!"; } } Invoking the GET /abh/techbite and GET /abh/techbite/ was the same thing in the past, but now you will probably get an HTTP 404 error when trying to invoke /abh/techbite/. The reason behind this is because of Spring Framework 6, the trailing slash matching configuration option has been deprecated, and its default value is set to false. This means that previously, the following controller would match both paths, but not anymore. Now developers should instead configure explicit redirects/rewrites through a proxy, a Servlet/web filter, or even declare the additional route explicitly on the controller handler (like @GetMapping ("/abh/techbite", "/abh/techbite/") for more targeted cases. Spring Autoconfiguration. This small but important change can go under the radar at first, but you will figure it out probably after spending some time searching for the solution. The way you register your own autoconfiguration has now changed. The spring.factories file has been changed to AutoConfiguration.imports, and the content is only the fully qualified AutoConfiguration class name or a list of them containing only one per line. Also, the location of this file is changed from META-INF/spring.factories to META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Using spring.factories: org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.tech.bite.AbhAutoConfiguration Using AutoConfiguration.imports: com.tech.bite.AbhAutoConfiguration This change was introduced in Spring Boot 2.7 with backward compatibility, but with the version 3.0 release, this support ended. Accessing JDK internal classes. Some parts of your application code might be using JDK classes, which are now not public. JDK has restricted access to those classes, but there is a workaround to use these packages by adding the VM argument in the following format: --add-opens module/package=target-module Removing those packages is highly recommended because it is a matter of time when access will be restricted entirely. Hibernate Criteria API removal. One of the breaking changes Hibernate migration introduced is the removal of the Hibernate Criteria API. Hibernate Criteria API is used for creating dynamic, type-safe queries in Java applications. The legacy Hibernate Criteria API was deprecated back in Hibernate 5.x and now is removed in version 6. Usually, all queries using the legacy API can be modeled with the JPA Criteria API. SELECT * FROM Person WHERE age > 18; This static SQL query can be translated to Hibernate Criteria API as: Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Person.class); criteria.add(Restrictions.gt("age", 18)); List<Person> result = criteria.list(); or to JPA Criteria Query API as: CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Person> query = cb.createQuery(Person.class); Root<Person> person = query.from(Person.class); query.select(person).where(cb.gt(person.get("age"), 18)); List<Person> result = entityManager.createQuery(query).getResultList(); Conclusion Following these steps, you may efficaciously migrate your application to Java 17 and Spring Boot 3, but there is one thing left to do. There is no better way to conclude this topic than testing. Perform unit tests, integration tests, and regression tests to verify that all functions are as required. Testing is an essential requirement to ensure that the migration process has been successful. "Java 17 and Spring Boot 3: Upgrade Roadmap" Tech Bite was brought to you by Nedeljko Kovačević, Software Engineer at Atlantbh. (more…)
Ready to Achieve More?
We’ll help you reach your goals quickly with an easy and straightforward process to kick off our collaboration. Here’s what happens next.