Migrate Your Spring Boot App To Quarkus: Java, Kotlin & Gradle
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.
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
Soufiane Sakhi is an AWS Certified Solutions Architect – Associate and a professional full stack developer.