by Nikita Zemnitsky, Java Developer at NIX.
Java 17, the new LTS version, entered the market almost a year ago, but has not yet gained significant popularity. According to a NewRelic survey, this version of Java is now used by 0.37% of developers. Many programmers are thinking about the possible problems of migrating from LTS version 11 to a fresh version. After all, many people remember how hard the migration from 8 to 11 was. Personally, I'm already convinced: it's different now. Migrating to new Java is not as hard as it seems. There are many benefits for the developer, from using new features and language applications to improved speed in general. I think further down the road, more developers will want to try Java 17.
I've been developing in Java for more than three years, working on microservices applications. In this article, I will describe the basic steps and features of migrating a project from Java 11 to Java 17. I will try to consider possible errors and options to solve them. Let me stress right away: I will consider migration from the Spring Boot program. This is one of the most popular frameworks that most Java developers use. True, the official support for Java 17 starts with Spring 6 and Spring Boot 3, but these versions are still under development. The latest version of Spring Boot is 2.6.7, which we'll look at. Although it's not officially supported by Java 17, the Spring developers have gone to great lengths to ensure that versions of Spring Boot from 2.6.5 already work with the new LTS to some degree.
How do I even migrate to a different Java version?
First, you need to find properties in pom.xml and change the version from the current version to version 17. This might look like this:
<properties>
<java.version>11</java.version>
</properties>
to
<properties>
<java.version>17</java.version>
</properties>
However, it is likely that you will get an error if you use any dependencies. For example, you may get a message like:
error: release version 17 is not supported
At first, it may be unclear why this happened. So let's try to understand the problem step by step and solve it.
Lombok
The very first bug is related to Lombok. This is the name of a Java library that automates template code generation. It can generate getters, setters, constructors, logging, etc., freeing classes from being cluttered with template code. When migrating from version 11 to version 17 of Java you get the following error:
java.lang.IllegalAccesError: class lombok.javac.apt.LombokProcessor cannot access classcom.sun.tools.javac.proseccing.JavacProcessingEnvironment
This is all about Lombok version 1.18.22. When I migrated my application I had version 1.18.12. To understand the reason for this error we have to look at JEP 396 which added a severe encapsulation of JDK internal components by default. This JEP was implemented back in version 16 of Java, and it is the reason why Lombok can somehow no longer use the JDK internals.
First, the Lombok developers decided to use the following command to override the previous JEP:
--illegal-access=permit
But this was valid only for Java 16. In version 17 this command was removed. So Lombok came up with another solution. You need to add a maven-compiler-plugin in the arguments. This way you get access to the right Lombok components:
<compilerArgs>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
</compilerArgs>
However, manually entering arguments is not very convenient. That's why Lombok released version 1.18.22, which is done automatically—you just need to add this dependency. To do this, the developers changed the visibility of the modules with reflection, and already after that, you have access. Yes, it's not very sexy, but it still works.
MapStruct
MapStruct is a Java annotation processor for automatically generating converters (mappers) between Java components. MapStruct uses generated getters, setters, and constructors to create transducers. After upgrading Lombok to version 1.18.22, converters are no longer generated precisely because of that bug, because JEP 396 was implemented, adding strict encapsulation of internal modules. To get around this, you need to add a Lombok-map struct-binding annotation handler to the maven-compiler-plugin:
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
After that, everything should work without any problems.
ASM
ASM is an environment for manipulating Java byte code. ASM uses CGLIB, which in turn is used by Spring for AOP. In the Spring Framework, the AOP proxy is a dynamic JDK proxy or a CGLIB proxy. Spring uses CGLIB and ASM, and generates proxy classes that are incompatible with the Java 17 runtime environment. Spring Boot is lower than 2.4 and depends on Spring Framework 5.2, which uses a version of CGLIB and ASM that is incompatible with Java 17. But you can't update the CGLIB or ASM libraries because Spring repackages ASM for internal use. So the only way out of this situation is to update Spring Boot. Without this, ASM will not work.
JUnit and the missing spring-boot.version feature
In the new version of Spring Boot, the spring-boot.version property has been removed from spring-boot-dependencies. The developers used it for many tasks, for example, to exclude a dependency. Here is an example with the junit-vintage-engine exception:
<dependencyManagement>
<dependencies>
<dependecy>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependecy>
</dependencies>
</dependencyManagement>
Fortunately, we can now remove this block, since Spring Boot 2.4 removed the Vintage Engine JUnit 5 from the spring-boot-starter-test. However, if your project is still using JUnit 4 and you see compilation errors like java: package org.junit does not exist, this is because the old engine was removed. The old engine is responsible for running the JUnit 4 tests along with the JUnit 5 tests. If you cannot, due to some circumstances, migrate the tests to JUnit 5, add the following dependency to pom:
<dependecy>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependecy>
This will completely solve the problem described.
Jackson
The next possible problem is related to Jackson. Jackson is a library of data processing tools, e.g. for serializing and deserializing JSON in Java components and vice versa. It can handle many data formats but is most commonly used for JSON.
After updating to Spring Boot 2.6.7, the following error occurs:
java.time.OffsetDateTime not supported by default: add Module “com.fasterxml.jackson.datatype:jackson-datatype-jsr310”
The reason is the JSR-310 module is not available for Jackson. Because of the change of Jackson 2.12, this now causes the serialization to fail, rather than Jackson serializing in an unexpected format.
There are two solutions to this problem. First, you can create your own mapper. For version 2.10 and later via JsonMapper.buider it looks like this:
ObjectMapper mapper = JsonMapper.buider()
.addModule(new JavaTimeModule())
.build();
For older versions, this can be done through mapper.registerModule:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
However, all this is not a good option, because creating mappers yourself takes extra time and effort. So I suggest another way: try Jackson autoconfiguration.
Jackson Autoconfiguration
Spring Boot provides autoconfiguration for Jackson and automatically declares a fully-customized ObjectMapper component. Jackson can be configured without defining its own ObjectMapper Bean component, using Jackson2ObjectMapperBuilderCustomizer properties or classes.
You need to check that the com.fasterxml.jackson.datatype:jackson-datatype-jsr310 module is in the classpath (i.e. classpath), and it will be automatically registered in ObjectMapper. ObjectMapper is thread-safe, so it can be created once and reused.
Swagger requested a validator from Atlassian
Atlassian's Swagger is a library for validating Swagger/OpenAPI 3.0 requests and responses. The old version of the library does not use the Spring Boot Jackson autoconfiguration and does not register the JavaTimeModule in its ObjectMapper. But after updating the version of the library, the tests work again. That's why you should definitely use the latest version of the libraries, which usually contains all the necessary fixes.
In general, these are the most notable bugs I encountered during the transition to Java 17 which I found worthy of attention. Of course, in practice, there may be more problems, but they are mostly minor and require not so much special approaches, but more attention and additional time to solve. In my opinion, this is all justified by the advantages of the latest LTS version, as it is more functional and faster. So don't be afraid to migrate your projects from Java 11 to Java 17. It's not as scary as it may seem at first.
Top comments (0)