Migrate Your Spring Boot App To Quarkus: Java, Kotlin & Gradle

Migrating to Quarkus from Spring Boot with Gradle, Java & Kotlin - Tips, Configuration & Conversion Table
How to migrate your Spring Boot web app to Quarkus with class and annotation equivalence tables, common code snippets and Gradle configuration example.

Introduction

After recently migrating a Spring Boot microservice app to Quarkus, I decided to write a tutorial to share some common code adaptations that I used. This will certainly not cover all the use cases but it's enough to get started.

This guide is applicable for both Java and Kotlin web apps. If your project uses Maven instead of Gradle, you can check out this tutorial to get started with the Maven configuration part.

This tutorial will not cover Spring Data JPA, Spring Security and Spring Cloud Config, but if you are interested you can follow these guides:

Gradle adaptations

To find the required Gradle dependencies and configuration for Quarkus, I created an empty Quarkus project by using the online starter and selecting the spring dependencies the current project uses.

code.quarkus.io

After that I adapted the project's gradle configuration files by:

  • Deleting all the spring dependencies and plugins (You can search the file for any spring keywords)
  • Adding the new dependencies and plugin configuration

The resulting build.gradle should look like this:

plugins {
    // To remove if the project uses Kotlin
    id 'java'
    // Required if the project uses Kotlin
    // id 'org.jetbrains.kotlin.jvm' version "1.3.72"
    // id "org.jetbrains.kotlin.plugin.allopen" version "1.3.72"
    id 'io.quarkus'
}

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation 'io.quarkus:quarkus-spring-boot-properties' // required when using annotations such as @ConfigurationProperties
    implementation 'io.quarkus:quarkus-spring-di' // required when using annotations such as @Service, @Configuration, ...
    implementation 'io.quarkus:quarkus-spring-web' // required when using annotations such as @RestController, @GetMapping, ...
    // implementation 'io.quarkus:quarkus-kotlin' // required when using Kotlin
    implementation 'io.quarkus:quarkus-vertx'
    // implementation 'io.quarkus:quarkus-undertow'// if the project should not be using a reactive server (and use servlets instead)
    implementation 'io.quarkus:quarkus-resteasy'
    implementation 'io.quarkus:quarkus-resteasy-jackson' // Optional, for Jackson support
    implementation 'io.quarkus:quarkus-resteasy-mutiny' // Optional, for reactive support
    implementation 'io.smallrye.reactive:mutiny-rxjava' // Optional, for reactive conversion for RxJava
    implementation 'io.smallrye.reactive:mutiny-reactor' // Optional, for reactive conversion for Project React
    implementation 'io.smallrye.reactive:smallrye-mutiny-vertx-web-client' // Optional, for reactive web client support
    implementation 'io.quarkus:quarkus-scheduler' // Optional, for scheduling periodic tasks
    implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
    // implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' // required when using Kotlin

    // implementation("ch.qos.logback:logback-classic:1.2.3") // when using Logback

    testImplementation 'io.quarkus:quarkus-junit5'
    testImplementation 'io.rest-assured:kotlin-extensions'
}

group 'org.acme'
version '1.0.0-SNAPSHOT'

quarkus {
    // Required if the project uses Kotlin
    // setOutputDirectory("$projectDir/build/classes/kotlin/main")
    setWorkingDir("$projectDir") // Added to have a similar behavior as Spring Boot
}

quarkusDev {
    // Required if the project uses Kotlin
    // setSourceDir("$projectDir/src/main/kotlin")

    // The following jvm args are required to support Logback
    // jvmArgs = [
    //         "-Dorg.jboss.logging.provider=slf4j",
    //         "-Dlogback.configurationFile=$projectDir/src/main/resources/logback.xml"
    // ]
}

// Required if the project uses Kotlin
// allOpen {
//     annotation("javax.ws.rs.Path")
//     annotation("javax.enterprise.context.ApplicationScoped")
//     annotation("io.quarkus.test.junit.QuarkusTest")
// }

// To remove when using Kotlin
compileJava {
    options.encoding = 'UTF-8'
    options.compilerArgs << '-parameters'
}

// To remove when using Kotlin
compileTestJava {
    options.encoding = 'UTF-8'
}

// Required if the project uses Kotlin
// compileKotlin {
//     kotlinOptions.jvmTarget = JavaVersion.VERSION_11
//     kotlinOptions.javaParameters = true
// }

// Required if the project uses Kotlin
// compileTestKotlin {
//     kotlinOptions.jvmTarget = JavaVersion.VERSION_11
// }

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

test {
    systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager"
}

If you're using Gradle with Kotlin DSL:

build.gradle.kts

// Required if the project uses Kotlin
//import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    // To remove if the project uses Kotlin
    id("java")
    // Required if the project uses Kotlin
    // kotlin("jvm") version "1.3.72"
    // kotlin("plugin.allopen") version "1.3.72"
    id("io.quarkus")
}

repositories {
    mavenLocal()
    mavenCentral()
}

val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project

dependencies {
    implementation("io.quarkus:quarkus-spring-boot-properties") // required when using annotations such as @ConfigurationProperties
    implementation("io.quarkus:quarkus-spring-di") // required when using annotations such as @Service, @Configuration, ...
    implementation("io.quarkus:quarkus-spring-web") // required when using annotations such as @RestController, @GetMapping, ...
    // implementation("io.quarkus:quarkus-kotlin") // required when using Kotlin
    implementation("io.quarkus:quarkus-vertx")
    // implementation("io.quarkus:quarkus-undertow")// if the project should not be using a reactive server (and use servlets instead)
    implementation("io.quarkus:quarkus-resteasy")
    implementation("io.quarkus:quarkus-resteasy-jackson") // Optional, for Jackson support
    implementation("io.quarkus:quarkus-resteasy-mutiny") // Optional, for reactive support
    implementation("io.smallrye.reactive:mutiny-rxjava") // Optional, for reactive conversion for RxJava
    implementation("io.smallrye.reactive:mutiny-reactor") // Optional, for reactive conversion for Project React
    implementation("io.smallrye.reactive:smallrye-mutiny-vertx-web-client") // Optional, for reactive web client support
    implementation("io.quarkus:quarkus-scheduler") // Optional, for scheduling periodic tasks
    implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
    // implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") // required when using Kotlin

    // implementation("ch.qos.logback:logback-classic:1.2.3") // when using Logback

    testImplementation("io.quarkus:quarkus-junit5")
    testImplementation("io.rest-assured:kotlin-extensions")
}

group = "org.acme"
version = "1.0.0-SNAPSHOT"

quarkus {
    // Required if the project uses Kotlin
    // setOutputDirectory("$projectDir/build/classes/kotlin/main")
    setWorkingDir("$projectDir") // Added to have a similar behavior as Spring Boot
}

tasks.quarkusDev {
    // Required if the project uses Kotlin
    // setSourceDir("$projectDir/src/main/kotlin")

    // The following jvm args are required to support Logback
    // jvmArgs = listOf(
    //         "-Dorg.jboss.logging.provider=slf4j",
    //         "-Dlogback.configurationFile=$projectDir/src/main/resources/logback.xml"
    // )
}

// Required if the project uses Kotlin
// allOpen {
//     annotation("javax.ws.rs.Path")
//     annotation("javax.enterprise.context.ApplicationScoped")
//     annotation("io.quarkus.test.junit.QuarkusTest")
// }

tasks.withType<JavaCompile> {
    options.encoding = "UTF-8"
    options.compilerArgs.add("-parameters")
}

// Required if the project uses Kotlin
//tasks.withType<KotlinCompile>().configureEach {
//    kotlinOptions {
//        jvmTarget = javaVersion.toString()
//        javaParameters = true
//    }
//}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks.test {
    systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
}

settings.gradle:

pluginManagement {
    repositories {
        mavenLocal()
        mavenCentral()
        gradlePluginPortal()
    }
    plugins {
      id 'io.quarkus' version "${quarkusPluginVersion}"
    }
}
rootProject.name='project-name'

gradle.properties:

quarkusPluginVersion=1.4.2.Final
quarkusPlatformArtifactId=quarkus-universe-bom
quarkusPlatformGroupId=io.quarkus
quarkusPlatformVersion=1.4.2.Final

To run the project in development mode, the quarkusDev gradle task can be used.

To build the project, the quarkusBuild gradle task can be used.

A more comprehensive guide for working with gradle in Quarkus is available here.


Class and Annotation Equivalence Between Spring Boot and Quarkus

Class Equivalence

Spring Boot Quarkus
HttpServletRequest javax.servlet.http.HttpServletRequest
HttpServletResponse javax.servlet.http.HttpServletResponse
HttpRequest, ServerHttpRequest org.jboss.resteasy.spi.HttpRequest, HttpServerRequest
ServerHttpResponse HttpServerResponse
ResponseEntity javax.ws.rs.core.Response
RestTemplate, Reactive WebClient Vert.x WebClient
Filter, WebFilter, AbstractRequestLoggingFilter ContainerRequestFilter or ContainerResponseFilter
org.springframework.scheduling.annotation.Scheduled io.quarkus.scheduler.Scheduled
Mono, Flux Uni, Multi
UriComponentsBuilder javax.ws.rs.core.UriBuilder

Annotations Equivalence

Spring Boot Quarkus
@SpringBootApplication @QuarkusMain
@Autowired, @Qualifier, @Bean @Inject, @Named, @Produces
@Component, @Service, @Repository, @Configuration @Singleton, @ApplicationScoped
@Controller, @RestController, @RequestMapping @Path
@GetMapping, @PostMapping, ... @GET, @POST, ...
@xxxMapping(consumes=.., produces=..) @Consumes, @Produces
@RequestParam, @PathVariable @QueryParam, @PathParam
@ConfigurationProperties @ConfigProperties, @ConfigProperty

Common configuration equivalence Between Spring Boot and Quarkus

Global Exception Handling

Spring Boot

Kotlin
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.http.ResponseEntity

@ControllerAdvice
class GlobalExceptionHandlers {
  //...
  @ExceptionHandler(Exception::class)
  fun handleException(ex: Exception): ResponseEntity<Any> {
    logger.error("Uncaught exception", ex)
    return ResponseEntity.badRequest().build()
  }
}
Java
@ControllerAdvice
public class GlobalExceptionHandlers {
    //...
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Void> handleException(Exception ex) {
        logger.error("Uncaught exception", ex);
        return ResponseEntity.badRequest().build();
    }
}

Quarkus

Kotlin
import javax.ws.rs.WebApplicationException
import javax.ws.rs.core.Response
import javax.ws.rs.ext.ExceptionMapper
import javax.ws.rs.ext.Provider

@Provider
class UncaughtExceptionMapper : ExceptionMapper<Exception> {
  //...
  override fun toResponse(ex: Exception): Response {
    return when(ex) {
      is WebApplicationException -> ex.response
      else -> Response.status(Response.Status.BAD_REQUEST).build().also {
        logger.error("Uncaught exception", ex)
      }
    }
  }
}
Java
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class UncaughtExceptionMapper implements ExceptionMapper<Exception> {
    //...
    @Override
    public Response toResponse(Exception ex) {
        if (ex instanceof WebApplicationException) {
            return ((WebApplicationException)ex).getResponse();
        }
        logger.error("Uncaught exception", ex);
        return Response.status(Response.Status.BAD_REQUEST).build();
    }
}

Jackson configuration

Spring Boot

application.properties

spring.jackson.default-property-inclusion=non_null

Quarkus

Kotlin
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import io.quarkus.jackson.ObjectMapperCustomizer
import javax.inject.Singleton

@Singleton
class JsonConfig : ObjectMapperCustomizer {
  override fun customize(mapper: ObjectMapper) {
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
  }
}
Java
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.jackson.ObjectMapperCustomizer;
import javax.inject.Singleton;

@Singleton
public class JsonConfig implements ObjectMapperCustomizer {
    @Override
    public void customize(ObjectMapper mapper) {
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }
}

CORS configuration

Spring Boot

Can be configured either:

  • In individual controllers with the @CrossOrigin annotation (class or method level)
  • Globally using a WebMvcConfigure Bean

Quarkus

Configured globally in application.properties


Conclusion

Migrating from Spring Boot to Quarkus was not a very complicated task.

I did encounter some errors when mixing Quarkus annotations with Spring compatibility extension ones, for example:

  • declares multiple scope type annotations: javax.inject.Singleton, javax.inject.Singleton: I had a Quarkus @Scheduled method inside a Spring component. I resolved it by extracting the method to an external class.
  • RESTEASY003400: Illegal to inject a non-interface type into a singleton: Resolved by keeping the Spring DI annotations.

There are of course many behavioral changes between the two frameworks, for example:

  • @PathVariable parameters are not url decoded.
  • when using the spring-boot-properties extension, dash-separated properties are not automatically matched to camelcase fields.

Tests have to be done to ensure that the existing functionality of your app remains intact.

I have compared many JVM performance metrics before and after the migration, which will be presented in this article: https://simply-how.com/quarkus-vs-spring-boot-production-performance