In this blog, I will highlight the changes and challenges that I have faced while doing the migration of services from java 8 to 17 and Spring Boot 2.x to 3.x.
javax to jakarta namespace
The most significant change concerning javax libraries when migrating from Java 8 to Java 17 is the transition from Java EE (Java Platform, Enterprise Edition) to Jakarta EE.
- Package Renaming:The core change is the renaming of package names from
javax.*tojakarta.*. This affects all specifications that were part of Java EE and are now part of Jakarta EE, such as:javax.servletbecomesjakarta.servletjavax.persistencebecomesjakarta.persistencejavax.ws.rsbecomesjakarta.ws.rsjavax.xml.bindbecomesjakarta.xml.bind(JAXB)
However, not all javax is changed to jakarta. For example, javax.sql.DataSource remains the same since it belongs to the standard java library. So make sure to roll back certain import statements.
@Async only supports void or Future return types
When migrating a Spring Boot application from version 2 to 3, an “invalid return type for async method” error indicates a stricter enforcement of the @Async annotation’s return type constraints in Spring Framework 6 (used by Spring Boot 3).
The core issue:
- In previous Spring versions, if an
@Asyncmethod returned a type other thanvoidorFuture(includingCompletableFutureandListenableFuture), it might have returnednullwithout throwing an error. - Stricter validation in Spring Boot 3 / Spring Framework 6:
Spring Framework 6 explicitly validates the return type of@Asyncmethods. If the return type is notvoidor aFutureimplementation, an exception is now thrown.
Resolution:
- Ensure
voidorFuturereturn type:- If the asynchronous method does not need to return a value, change its return type to
void. - If the asynchronous method needs to return a value, ensure its return type is
java.util.concurrent.Future,org.springframework.util.concurrent.ListenableFuture, orjava.util.concurrent.CompletableFuture.
- If the asynchronous method does not need to return a value, change its return type to
// Example of a valid async method returning a CompletableFuture
@Async
public CompletableFuture<String> performAsyncTask() {
// ... asynchronous logic ...
return CompletableFuture.completedFuture("Result of async task");
}
// Example of a valid async method with no return value
@Async
public void performVoidAsyncTask() {
// ... asynchronous logic ...
}
add-opens
Some tools and libraries use reflection to access parts of the JDK that are meant for internal use only. This use of reflection negatively impacts the security and maintainability of the JDK. To aid migration, JDK 9 through JDK 16 allowed this reflection to continue, but emitted warnings about illegal reflective access. However, JDK 17 and later is strongly encapsulated, so this reflection is no longer permitted by default. Code that accesses non-public fields and methods of java.* APIs will throw an InaccessibleObjectException.
Some tools and libraries use the reflection API (setAccessible(true)) to attempt to access non-public fields and methods of java.* APIs. This is no longer possible by default on JDK 17, but you can use the --add-opens option on the command line to enable it for specific tools and libraries.
The syntax for --add-opens is:
--add-opens <module>/<package>=<target-module>(,<target-module>)*
This option allows <module> to open <package> to <target-module>, regardless of the module declaration.
As a special case, if the <target-module> is ALL-UNNAMED, then the source package is exported to all unnamed modules, whether they exist initially or are created later on. For example:
--add-opens java.management/sun.management=ALL-UNNAMED
This example allows all of the code on the class path to access nonpublic members of public types in the java.management/sun.management package.
Changed JDK and JRE Layout
After you install the JDK, if you look at the file system, you’ll notice that the directory layout is different from that of releases before JDK 9.
JDK 11 and later does not have the JRE image. See Installed Directory Structure of JDK in Java Platform, Standard Edition Installation Guide. So, make sure to give the proper path in java command.
spring.config.use-legacy-processing and ‘optional:’ in spring.config.location
Spring Boot 3.0 has removed support for legacy properties processing, including the spring.config.use-legacy-processing property that could be used in earlier versions (like Spring Boot 2.4) to revert to the old behavior. This means that if you are migrating a Spring Boot application from a version prior to 3.0 and were relying on this legacy behavior, you will need to adapt your application to the new property processing mechanisms.
In Spring Boot applications, prior to version 2.4, specifying a non-existent properties file in spring.config.location= without the optional: prefix would not cause the application to fail to start. This behavior was changed in Spring Boot 2.4 and later versions.
In Spring Boot 3, if a properties file specified in spring.config.location does not exist, the application will not start by default and will throw a ConfigDataLocationNotFoundException.
The optional: prefix is still required if you want to specify a configuration location that might not always exist and you want your application to start without an error in such cases. Without this prefix, Spring Boot will consider the non-existence of the specified file as a critical error, preventing the application from launching.
This behavior applies to spring.config.location, spring.config.additional-location, and spring.config.import declarations.
If you found this helpful, please like the blog and share the feedback in the comment.
Great content! Keep up the good work!