Minimal Apache CXF setup using Gradle, Kotlin and SpringBoot3
I spend a lot of time recently trying to figure out how to properly configure Apache CXF with Gradle, Kotlin and SpringBoot3 (so using Jakarta instead of JaxWS).
I was in the process of modernizing an old project that somebody else had written. Refactoring the code from Java to Kotlin was pretty straight forward but at some point the app stopped working (which I didn’t realize in time to know where exactly I introduced the breaking change), so that’s when I started to look for information on how CXF is meant to be used.
Unfortunately all the tutorials I found weren’t really that good of a fit, since apart from the official documentation, all were many years old and using outdated versions of the libraries involved.
a minimal project
the Spring configuration:
@SpringBootApplication
class HelloApp
fun main() {
runApplication<HelloApp>()
}
@Configuration
class Config {
@Bean fun getServer(
bus: Bus,
helloService: HelloService,
): Server = JAXRSServerFactoryBean().apply {
setBus(bus)
address = "/rs"
setServiceBeans(listOf(helloService))
setProvider(
JacksonJsonProvider().apply {
setMapper(jacksonObjectMapper())
}
)
}.create()
}
the endpoint definition:
@Consumes("application/json")
@Produces("application/json")
interface HelloService {
@GET
@Path("/hello")
fun hello(): String
}
@Component
class HelloServiceImpl: HelloService {
override fun hello(): String = """{"msg": "hello back"}"""
}
and the gradle configuration:
plugins {
id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
val kotlinVersion = "2.0.0"
kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
kotlin {
// uses org.gradle.java.installations.auto-download=false in gradle.properties to disable auto provisioning of JDK
jvmToolchain(21)
}
repositories {
mavenCentral()
}
dependencies {
val jacksonVersion = "2.17.1"
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:$jacksonVersion")
val cxfVersion = "4.0.4"
implementation("org.apache.cxf:cxf-spring-boot-starter-jaxrs:$cxfVersion")
}
The whole project is available on GitHub.
the final puzzle piece
I actually had a functional version of this configuration pretty fast, I just didn’t recognize it since I
made the mistake of assume that given the hello-function is annotated like this @GET @Path("/hello")
and we set the address to /rs
that the complete local url would look like this:
http://localhost:8080/rs/hello
but there is some hidden configuration going on and the url is actually:
http://localhost:8080/services/rs/hello
The autogenerated client to our actual project (not this one) actually additionally inserts a /json
into the url
directly after the /rs
and I haven’t figured out under which conditions this happens. For this project a url without
an additional `/json’ part works fine, but I wanted to add it for completeness.
other tutorials: