Minimal Apache CXF setup using Gradle, Kotlin and SpringBoot3

· Stephan Schröder

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:

class HelloApp

fun main() {

class Config {

    @Bean fun getServer(
        bus: Bus,
        helloService: HelloService,
    ): Server = JAXRSServerFactoryBean().apply {
        address = "/rs"
            JacksonJsonProvider().apply {

the endpoint definition:

interface HelloService {
    fun hello(): String

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

repositories {

dependencies {

    val jacksonVersion = "2.17.1"

    val cxfVersion = "4.0.4"

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:


but there is some hidden configuration going on and the url is actually:


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: