Technical challenges - Migrating from JDK 8 to 17 and Spring Boot 2.x to 3.x

Technical challenges - Migrating from JDK 8 to 17 and Spring Boot 2.x to 3.x

i call it "jumping through the hoops!"

·

7 min read

Introduction

On November 2022, Spring Boot 3.0 has gone GA. With it came the push for Java 17 as its baseline. This has forced many enterprises which religiously relied on Java 8 as the base for Spring Boot 2.x to migrate to JDK 17 to keep up.

Official spring documentation suggests a two-phase migration. First, migrate to JDK 11 and Spring Boot 2.7 if the project is on prior versions and then make the second phase of migration to the latest builds.

My approach here is for the devs who are brave (and crazy) enough to go for the big leap; i.e., a direct migration. Most of the time you will be pulling your hair; and will curse a lot, but it is worth the experience in my humble opinion.

In this article, I have added my personal experience of what I have gone through during the migration process and provided some preliminary checks and a workflow which could help soften the burden. This is not in-depth, but a comprehensive guide only. Hence please be vigilant when doing this as I am not responsible for any unexpected pitfalls.

This is a series of two articles. The first article (this) discusses only the technical challenges which most of the readers will be expecting. However, there are other design, integration and business challenges related to migrating a codebase especially if it is a microservice. I will separately write a second article discussing the design-related challenges.

Let's get in...

1. JDK standard internal APIs that are depreciated, removed or replaced

1.1 APIs that are either depreciated or removed

Certain APIs or their implementations might have been removed or depreciated. For example, JDK 8 has built-in support for JAXB (Java Architecture for XML Binding) which was entirely removed in JDK 17.

The solution is to simply add this as a dependency. If spring boot is in the classpath, it already has the implementation, hence jaxb-api is enough. Otherwise, you may have to add a suitable implementation as well.

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>

1.2 APIs that are replaced

JDK 17 has replaced certain APIs or hidden them from developers. The following example is part of a class which uses OpenCSV to parse an HTTP response. The read(...) method of the superclass AbstractGenericHttpMessageConverter is overridden to make use of the parsing. Notice the class ParameterizedTypeImpl which is removed in JDK 17.

// Java-8
@Override
public Object read(
        Type type,
        Class<?> aClass, HttpInputMessage httpInputMessage
) throws IOException, HttpMessageNotReadableException {
    Reader reader = new InputStreamReader(httpInputMessage.getBody());
    CsvToBean csvToBean = new CsvToBeanBuilder(reader)
            .withType(
                    (Class) ((sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl) type)
                    .getActualTypeArguments()[0]
            )
            .build();
    return csvToBean.parse();
}

The fix is to simply use the respective java.lang.reflect.ParameterizedType interface instead.

2. Java libraries using depreciated or removed internal APIs will not compile

The infamous sun.misc.Unsafe is a good example which broke some high-performance libraries written in java. Many public libraries which utilized these depreciated and removed APIs from standard java most probably have already updated their versions. If such a library breaks, just find the latest version and update it.

Security-related cryptographic and OAuth libraries, annotation processing tools (e.g.: lombok), bytecode manipulation libraries (e.g.: bytebuddy), java-agents (e.g.: opentelemetry), network-related libraries (e.g.: zookeeper), database connectors (both oracle and postgresql) would normally break. However, there are updated versions available. Unfortunately, some libraries (e.g.: XStream) do not yet support Java 17. In that case, you may have to find alternatives or abort migration altogether.

Spring Boot 3 also does not support some of the other spring projects. You can find those by simply going to spring initializr, setting Java 17, Spring Boot 3.x and clicking Add Dependencies button. It will show disabled projects. Sometimes such functionality offered in one project may be ported to another project (for example tracing was built into Spring Cloud Sleuth initially and now it is moved to Micrometer Tracing project). You have to do some digging and check whether your specific dependency is moved somewhere else if not abandoned already.

If the internal library/libraries built by your organization contain these depreciated codes and are added as dependencies in the component, then you have to update them beforehand. I will be talking about this extensively in the design challenges article since for most organizations, internal dependencies are the ones that bring headaches when migrating.

2.1 Migrating database connectors

<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc11</artifactId>
    <version>21.7.0.0</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.5.1</version>
</dependency>

I have tested the above versions, both of which are stable with Java 17 and are currently in production servers. There will be some refactoring needed for postgresql if you use custom dialects in your codebase. It is a slight change but you may need to get the major (10) and minor (0) versions right.

// Java-8
public class ExtendedPostgreSQL10Dialect extends PostgreSQL10Dialect {
    public ExtendedPostgreSQL10Dialect() {
        super();
        registerKeyword("WITHIN");
        registerKeyword("ROWNUM");
        registerKeyword("SYSDATE");
    }
}

// Java-17
public class ExtendedPostgreSQL10Dialect extends PostgreSQLDialect {
    public ExtendedPostgreSQL10Dialect() {
        super(DatabaseVersion.make(10, 0));
        registerKeyword("WITHIN");
        registerKeyword("ROWNUM");
        registerKeyword("SYSDATE");
    }
}

3. javax -> jakarta namespace change

This is the biggest change that gives developers a run for their money when migrating, not because it is difficult to implement, but because it is the most time-consuming. There are tools and command line executables available (e.g.: OpenRewrite) to scan a java class and make necessary adjustments. However, the ones I have tested always failed when the codebase gets bigger. In this case, your best friend is the IDE you are most familiar with.

JPA packages now start with jakarta instead of javax. However, not all javax is changed to jakarta. For example: javax.persistence.ValidationMode is now jakarta.persistence.ValidationMode. However, javax.sql.DataSource remains the same since it belongs to the standard java library. So make sure to roll back certain import statements in the aftermath.

4. Spring Security

Spring security is completely revamped and the old ways of authentication and authorization will not work anymore. Following is an example of utilizing annotation-based security.

// Spring Boot 2.x
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        if (Boolean.parseBoolean(env.getProperty("security.enabled", "true"))) {
            http.cors()
                    .and()
                    .authorizeRequests(authorize -> authorize.anyRequest().authenticated())
                    .oauth2ResourceServer().opaqueToken()
                    .introspector(new UserInfoTokenIntrospector(
                            env.getProperty("security.introspection_uri"),
                            env.getProperty("security.user_info_uri"),
                            env.getProperty("security.introspection_client_id"),
                            env.getProperty("security.introspection_client_secret")
                    ));
        } else {
            http.cors()
                    .and()
                    .authorizeRequests(authorize -> authorize.anyRequest().permitAll());
        }
    }
}
// Spring Boot 3.0
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        if (Boolean.parseBoolean(env.getProperty("security.enabled", "true"))) {
            http.cors()
                    .and()
                    .authorizeHttpRequests().requestMatchers(new AntPathRequestMatcher("/**")).permitAll()
                    .and()
                    .authorizeHttpRequests().requestMatchers(HttpMethod.GET, ".*username=.*password=.*").permitAll()
                    .and()
                    .authorizeHttpRequests(authManager -> authManager.requestMatchers("**").hasAnyAuthority("admin", "my-user").anyRequest().authenticated())
                    .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
                    .and()
                    .oauth2ResourceServer().opaqueToken()
                    .introspector(new UserInfoTokenIntrospector(
                            env.getProperty("security.introspection_uri"),
                            env.getProperty("security.user_info_uri"),
                            env.getProperty("security.introspection_client_id"),
                            env.getProperty("security.introspection_client_secret")
                    ));
        } else {
            http.cors()
                    .and()
                    .authorizeHttpRequests(authManager -> authManager.anyRequest().permitAll());
        }
        return http.build();
    }
}

Notice that class-based invocation is depreciated and annotation-based invocation is preferred. There is specific 3rd party documentation available on other security-related migrations.

Here is another example of how in-memory authentication is implemented in both Spring Boot 2.x and 3.0. As of now, no 3rd party guides are available to my knowledge that discuss migrating in-memory authentication. However, official spring documentation is more than enough.

// Spring Boot 2.x
@EnableWebSecurity
public class SecurityConfigurations extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password(encoder.encode("iAmAdmin"))
                .roles("ADMIN");
    }
}
// Spring Boot 3.0
@EnableWebSecurity
public class SecurityConfigurations {

    public UserDetailsService adminDetails() {
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        UserDetails admin = User.builder()
                .username("admin")
                .password(encoder.encode("iAmAdmin"))
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(admin);
    }

    @Bean
    public DaoAuthenticationProvider daoAuthProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(adminDetails());
        authProvider.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
        return authProvider;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().and()
                .authenticationProvider(daoAuthProvider())
                .httpBasic();
        return http.build();
    }
}

Again emphasis on the usage of beans and an annotation-driven approach instead of overriding base class is preferred. The method adminDetails() is wired into a method that outputs the authentication provider bean and that again is fed into a method that ultimately results in the familiar filter chain.

5. JVM Logging

JVM Logging (Xlog) has been changed to a more unified logging framework. Java 17 no longer recognizes the old way of Xlog. Hence you may have to replace your old commands with new keywords. Oracle itself provides an extensive guide on this. In the following example, you can see that separate flags are now bundled into a single flag.

# Java-8
JVM_LOG="-Xloggc:./logs/myComponent-GC.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=2 -XX:GCLogFileSize=5M"

# Java-17
JVM_LOG="-Xlog:gc*,gc+age*=trace:file=./logs/myComponent-GC.log:tags,time,uptime,level:filecount=2,filesize=5M"

6. GraalVM

Spring Boot 3 has first-party support for GraalVM. However, I would advise caution especially if you load custom certs into the java component. I had problems with it one of my components being corrupted when the custom certificate is loaded. Other components worked fine though. It is best to deploy a modified jar file in a compatible OpenJDK build, do stress and smoke testing and then finally deploy in GraalVM if you are into that. Otherwise, you will waste time fixing your jar which is not broken in the first place.

References

  1. https://spring.io/blog/2022/11/24/spring-boot-3-0-goes-ga

  2. https://www.unhcr.org/asylum-and-migration.html (cover image)

  3. https://advancedweb.hu/a-categorized-list-of-all-java-and-jvm-features-since-jdk-8-to-18/#deprecation-and-removal

  4. https://www.jesperdj.com/2018/09/30/jaxb-on-java-9-10-11-and-beyond/

  5. https://blogs.oracle.com/post/about-sunmiscunsafe

  6. https://www.baeldung.com/java-enterprise-evolution

  7. https://medium.com/javarevisited/how-to-upgrade-from-java-8-to-java-17-eb58f4554c6e

  8. https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/in-memory.html

  9. https://www.bezkoder.com/websecurityconfigureradapter-deprecated-spring-boot/

  10. https://docs.oracle.com/javase/9/tools/java.htm#GUID-BE93ABDC-999C-4CB5-A88B-1994AAAC74D5__CONVERTGCLOGGINGFLAGSTOXLOG-A5046BD1