A Spring Shell 4 starter application gives a JLine History ClassNotFound

Summary

The application crashes during Spring Boot startup with a ClassNotFoundException for org.jline.reader.History. This occurs because Spring Shell 4 (SS4) and Spring Boot 3 require JLine 3, but the spring-shell-starter dependency in version 4.0.0 is transitive and does not include JLine by default. Additionally, the application code is mixing outdated Spring Shell 1.x annotations with Spring Shell 4.x architecture, leading to compilation warnings and runtime ambiguity.

Root Cause

The issue is caused by a missing dependency in the project configuration. Spring Shell 4 is built on top of Spring Boot 3 and JLine 3, which provides the underlying terminal interaction capabilities.

  1. Missing JLine Dependency: While spring-shell-starter pulls in the core framework, the actual JLine library is an optional dependency. If not explicitly added to the pom.xml, the JVM cannot find the org.jline.reader.History class at runtime.
  2. Annotation Mismatch: The provided code uses org.springframework.shell.core.command.annotation.Command. This is a legacy annotation from Spring Shell 1.x. Spring Shell 4 uses org.springframework.shell.command.annotation.Command.

Why This Happens in Real Systems

In modern Spring Boot and Spring Shell development, the framework architecture relies on dependency management rather than fat JARs containing every library.

  • Optional Dependencies: Frameworks often mark heavy UI or terminal libraries (like JLine) as “optional” or “provided” to keep applications lightweight if they only use programmatic APIs.
  • Autowiring on Class Presence: Spring Boot auto-configurations (like JLineShellAutoConfiguration) often use @ConditionalOnClass annotations. When the starter attempts to configure JLine features, it checks for the presence of org.jline.reader.History. If the JAR is missing, the application fails immediately.
  • Strict Versioning: Spring Shell 4 strictly requires JLine 3. If a developer manually adds an older JLine version (e.g., JLine 2.x), version conflicts and NoSuchMethodError exceptions will occur, though the specific error here is a ClassNotFoundException.

Real-World Impact

  • Application Startup Failure: The application crashes immediately; no shell becomes available.
  • Confusing Stack Traces: The stack trace points to ConfigurationClassBeanDefinitionReader, which obscures the root cause. It looks like a Spring context configuration error rather than a missing dependency.
  • Legacy Code Integration Risk: Developers migrating from Spring Shell 1.x or 2.x to 4.x often leave old annotations in place. While @EnableCommand might suppress some errors, it prevents the newer annotation processors from scanning the commands correctly, leading to the “hi” method not being executed even if the classpath issue is resolved.

Example or Code

The Fix (pom.xml)

To resolve the ClassNotFoundException, you must explicitly add the JLine dependency.


    17
    4.0.0



    
    
        org.springframework.shell
        spring-shell-starter
        ${spring-shell.version}
    

    
    
        org.jline
        jline
        3.26.2 
    

    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    



    
        
            org.springframework.shell
            spring-shell-dependencies
            ${spring-shell.version}
            pom
            import
        
    

The Corrected Code (Spring Shell 4 Syntax)

We must update the imports to use the Spring Shell 4 package structure and remove the obsolete @EnableCommand (or use it correctly if static commands are required).

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.shell.command.annotation.Command;

@SpringBootApplication
public class Web2Application {

    public static void main(String[] args) {
        SpringApplication.run(Web2Application.class, args);
    }

    @Command
    public void hi() {
        System.out.println("Hello world!");
    }
}

How Senior Engineers Fix It

Senior engineers approach this issue by validating the dependency graph before debugging code logic.

  1. Audit the Dependency Tree: Run mvn dependency:tree (or gradle dependencies) to inspect resolved versions. Seniors look specifically for org.jline:jline in the list. If it’s absent, they immediately identify the missing transitive dependency.
  2. Align Library Versions: Since Spring Shell 4 depends on Spring Boot 3, the senior ensures that external libraries (like JLine) are compatible with the Spring Boot 3.x baseline (typically JLine 3.x).
  3. Modernize Annotations: They remove legacy annotations (org.springframework.shell.core...) and ensure imports align with the current Spring Shell major version. This prevents “silent failures” where commands compile but don’t register.
  4. Verify Auto-Configuration: They enable debug=true in application.properties to check the AutoConfigurationReport and verify that JLineShellAutoConfiguration is conditionally matching successfully once dependencies are added.

Why Juniors Miss It

Junior engineers often struggle with this specific error due to the “Black Box” nature of Spring Boot Starters.

  1. Trust in Starters: Juniors often assume that adding spring-shell-starter provides everything needed for a shell application. They do not realize that starters are curated lists that often exclude optional or heavy graphical libraries.
  2. Misinterpreting Stack Traces: The error java.lang.IllegalStateException: Failed to generate bean name looks like a Spring bean configuration logic error. Juniors often spend hours checking @Component scans or @Bean methods rather than looking at the Caused by: java.lang.ClassNotFoundException.
  3. Copy-Paste Legacy: The user’s code demonstrates copying examples from older documentation (Spring Shell 1.x). Juniors often don’t check the version tags on documentation, leading to incompatible annotation usage that confuses the compiler and runtime.
  4. Overlooking dependency:tree: Many juniors are not yet proficient with Maven/Gradle diagnostics and rely on checking the pom.xml visually for the presence of a dependency, rather than verifying if it has actually been downloaded and resolved into the classpath.