NoClassDefFoundError: jakarta/servlet/Filter (root cause + fix)

Publish date: 20 Dec 2025
Tags: spring boot kotlin

TL;DR (répare en 60 secondes) #

Si tu vois :

➡️ Verdict : ta classe @SpringBootApplication est dans le default package (pas de package ...).
➡️ Fix : déclare un vrai package + place le fichier dans le dossier qui correspond (et aligne tes tests).


Diagnostic express (copie-colle → verdict) #

Tu as l’un de ces messages ? (ou plusieurs) #

** WARNING ** : Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package.
Failed to introspect Class [org.springframework.boot.web.servlet.support.ErrorPageFilterConfiguration]
Caused by: java.lang.NoClassDefFoundError: jakarta/servlet/Filter
Error processing condition on org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration...

Verdict quasi certain : ta classe @SpringBootApplication est dans le default package.


Pourquoi cette erreur est trompeuse (et pourquoi tu vas perdre du temps) #

Le dernier message te pousse vers la fausse piste :

NoClassDefFoundError: jakarta/servlet/Filter

Réflexe classique : « OK, j’ajoute spring-boot-starter-web ».

Mauvaise réponse dans la majorité des cas.

Ton problème n’est pas “il manque servlet”. Ton problème est : Spring scanne trop large, parce que ton application est en default package.


Cause racine : le default package fait exploser le périmètre du scan #

Le mécanisme (simple) #

@SpringBootApplication déclenche notamment un @ComponentScan.

Résultat : Spring peut se mettre à inspecter des classes dans tes dépendances, y compris des autoconfig Servlet (ex: ErrorPageFilterConfiguration). Et comme tu n’as pas jakarta.servlet sur le classpath (normal sans web), tu prends :

NoClassDefFoundError: jakarta/servlet/Filter

👉 jakarta.servlet.Filter est un symptôme secondaire. La cause est le package.


Reproduire le bug (pour comprendre, pas pour souffrir) #

1) Dépendances (pas de web) #

build.gradle.kts exemple :

plugins {
    id("org.springframework.boot") version "4.0.1"
    id("io.spring.dependency-management") version "1.1.7"
    kotlin("jvm") version "2.2.21"
    kotlin("plugin.spring") version "2.2.21"
}

group = "fr.behaska.labs.kotlin"
version = "0.0.1-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.boot:spring-boot-starter-jackson")
    implementation("tools.jackson.module:jackson-module-kotlin")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

kotlin {
    jvmToolchain(23)
}

tasks.test {
    useJUnitPlatform()
}

2) Classe main dans le default package (l’erreur) #

src/main/kotlin/DemoApplication.kt sans ligne package ... :

import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

/**
 * This code application does not excute properly because it is missing the package declaration.
 * As a result, you have to declare the package at the top of the file to make it work.
 * Please refer to file 'kotlin-spring-no-default-package/src/main/kotlin/fr/behaska/labs/kotlinspringnodefaultpackage/DemoApplicationWithDefaultPackage.kt' for a working example.
 */
@SpringBootApplication
class DemoApplication {
    @Bean
    fun runner() = CommandLineRunner {
        println("Application started ✅")
    }
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

3) Résultat attendu #


La correction propre (évite cette classe d’effets de bord / évite la dérive de scan) #

Étape 1 — Mets un vrai package (obligatoire) #

Déplace ton fichier dans un dossier cohérent, par exemple :

src/main/kotlin/fr/behaska/labs/kotlinspringnodefaultpackage/DemoApplication.kt

Et ajoute un package en haut du fichier :

package fr.behaska.labs.kotlinspringnodefaultpackage // <== this package declaration was added to fix the issue

import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

@SpringBootApplication
class DemoApplication {
    @Bean
    fun runner() = CommandLineRunner {
        println("Application started ✅")
    }
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

Étape 2 — Mets tout le reste sous ce package (ou en sous-packages) #

Étape 3 — Aligne tes tests (sinon tu te prends l’autre classique) #

Si tes tests ne sont pas dans le même arbre de packages, tu peux obtenir :

Unable to find a @SpringBootConfiguration by searching packages upwards from the test

Exemple:

import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import java.io.ByteArrayOutputStream
import java.io.PrintStream

/**
 * This test throws up an IllegalStateException if package declaration is not the same as Tested class.
 * Please refer to file 'kotlin-spring-no-default-package/src/test/kotlin/fr/behaska/labs/kotlinspringnodefaultpackage/DemoApplicationTest.kt' for a working example.
 */
@SpringBootTest
class DemoApplicationTest {

    @Test
    fun `runner prints startup message`() {
        val app = DemoApplication()
        val runner = app.runner()

        val originalOut = System.out
        val buffer = ByteArrayOutputStream()
        System.setOut(PrintStream(buffer))

        try {
            runner.run() // exécute le CommandLineRunner
        } finally {
            System.setOut(originalOut)
        }

        val output = buffer.toString(Charsets.UTF_8)
        assertTrue(output.contains("Application started ✅"), "Output was: $output")
    }
}

Fix : place les tests dans fr.behaska.labs.kotlinspringnodefaultpackage (ou sous-package) :

src/test/kotlin/fr/behaska/labs/kotlinspringnodefaultpackage/...

package fr.behaska.labs.kotlinspringnodefaultpackage

import DemoApplication
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import java.io.ByteArrayOutputStream
import java.io.PrintStream

@SpringBootTest
class DemoApplicationTest {

    @Test
    fun `runner prints startup message`() {
        val app = DemoApplication()
        val runner = app.runner()

        val originalOut = System.out
        val buffer = ByteArrayOutputStream()
        System.setOut(PrintStream(buffer))

        try {
            runner.run() // exécute le CommandLineRunner
        } finally {
            System.setOut(originalOut)
        }

        val output = buffer.toString(Charsets.UTF_8)
        assertTrue(output.contains("Application started ✅"), "Output was: $output")
    }

}

“Package directive does not match the file location” : normal (et tu dois écouter l’IDE) #

Kotlin (le langage) autorise un package qui ne correspond pas au dossier disque.

Mais ton IDE (IntelliJ par exemple) te met ce warning parce que :

Pour un projet (et a fortiori un exemple de blog) : package = dossier. Point.


Checklist de debug (rapide, pragmatique) #

Checklist “je veux comprendre en 30 secondes” #

Exemple : fr.behaska.labs.demosrc/main/kotlin/fr/behaska/labs/demo/
Si oui : arrête tout, corrige le package.

Checklist “je veux verrouiller que c’est une app non-web” #

Si ton app est CLI/batch, tu peux verrouiller l’intention :

src/main/resources/application.properties :

spring.main.web-application-type=none

Ça ne remplace pas le fix du package, mais ça évite les surprises si quelqu’un ajoute une dépendance web plus tard.


FAQ (les questions qui reviennent) #

“Pourquoi Spring essaie de charger des trucs Servlet alors que je n’ai pas web ?” #

Parce que ton scan est hors contrôle : il inspecte des classes d’auto-configuration qu’il n’aurait jamais dû toucher dans une app correctement packagée.

“Est-ce que je peux régler ça en ajoutant spring-boot-starter-web ?” #

Tu peux… mais tu masques le problème et tu changes ton appli. Tu viens d’ajouter un serveur web juste pour faire taire un symptôme.

“Et si je ne veux vraiment pas déplacer mes packages ?” #

Tu peux forcer le périmètre :

@SpringBootApplication(scanBasePackages = ["fr.behaska.labs.kotlinspringnodefaultpackage"])
class DemoApplication

Mais ça reste une mauvaise idée : tu gardes un projet qui se comporte différemment des conventions Spring, et tu vas le payer en friction.


Sources (officielles / reconnues) #

Spring Boot Reference — Structuring your code / default package https://docs.spring.io/spring-boot/reference/using/structuring-your-code.html

Stack Overflow — réponse liée à ce warning (maintainer Spring Boot) https://stackoverflow.com/questions/28211049/spring-boot-gs-componentscan-and-classnotfoundexception-for-connectionfactory