Skip to content

Class Loader Issue in Spring AI Preventing Proper JavaTimeModule Loading #2921

Open
@luwanglin

Description

@luwanglin

Issue Description

When using Spring AI, I encountered a serialization failure for Java 8 date/time types (such as java.time.Duration). Despite correctly including the jackson-datatype-jsr310 dependency in my project, the following error occurs at runtime:

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.Duration` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: org.springframework.ai.chat.model.ChatResponse["metadata"]->org.springframework.ai.chat.metadata.ChatResponseMetadata["rateLimit"]->org.springframework.ai.chat.metadata.EmptyRateLimit["tokensReset"])

Root Cause

Through debugging, I discovered that the issue lies in the org.springframework.ai.util.JacksonUtils class's instantiateAvailableModules() method. This method uses ClassUtils.forName() to attempt loading the JavaTimeModule class, but fails due to class loader isolation issues.

The specific reasons are:

  1. Spring Boot applications use multiple class loaders:

    • jdk.internal.loader.ClassLoaders$AppClassLoader - JDK's default application class loader
    • org.springframework.boot.loader.LaunchedURLClassLoader - Spring Boot's class loader for application classes and dependencies
  2. ClassUtils.getDefaultClassLoader() returns the thread context class loader, which is AppClassLoader, not the LaunchedURLClassLoader that can see the application dependencies.

  3. Due to class loader visibility rules (parent class loaders cannot see classes loaded by child class loaders), AppClassLoader cannot see the JavaTimeModule class loaded by LaunchedURLClassLoader.

  4. Verification tests:

    • Class.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule") successfully loads the class because it uses the caller's class loader (LaunchedURLClassLoader)
    • ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null) fails to load the class because it uses AppClassLoader
    • Class.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", false, ClassUtils.getDefaultClassLoader()) also fails to load the class

Steps to Reproduce

  1. Create a Spring Boot application with Spring AI dependencies
  2. Ensure jackson-datatype-jsr310 dependency is added
  3. Use Spring AI's ChatClient to send requests and handle responses
  4. Attempt to serialize a ChatResponse object containing java.time.Duration fields

Temporary Workaround

Modify the ModelOptionsUtils.OBJECT_MAPPER static field via reflection to directly register JavaTimeModule:

@Configuration
public class JacksonConfig {
    
    @PostConstruct
    public void fixModelOptionsUtilsObjectMapper() {
        try {
            // Get ModelOptionsUtils class
            Class<?> modelOptionsUtilsClass = Class.forName("org.springframework.ai.model.ModelOptionsUtils");
            
            // Get OBJECT_MAPPER field
            Field objectMapperField = modelOptionsUtilsClass.getDeclaredField("OBJECT_MAPPER");
            objectMapperField.setAccessible(true);
            
            // Get current ObjectMapper instance
            ObjectMapper currentMapper = (ObjectMapper) objectMapperField.get(null);
            
            // Create JavaTimeModule instance directly, avoiding class loader issues
            JavaTimeModule javaTimeModule = new JavaTimeModule();
            
            // Register module
            currentMapper.registerModule(javaTimeModule);
            
            System.out.println("Successfully registered JavaTimeModule to ModelOptionsUtils.OBJECT_MAPPER");
        } catch (Exception e) {
            System.err.println("Failed to modify ModelOptionsUtils.OBJECT_MAPPER: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Suggested Fix

I recommend modifying the JacksonUtils.instantiateAvailableModules() method to use Class.forName() instead of ClassUtils.forName(), or explicitly specify the current class's class loader:

try {
    Class<? extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass = 
        (Class<? extends Module>) Class.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule");
    com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass);
    modules.add(javaTimeModule);
}
catch (ClassNotFoundException ex) {
    // jackson-datatype-jsr310 not available
}

Or alternatively:

try {
    ClassLoader currentClassLoader = JacksonUtils.class.getClassLoader();
    Class<? extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass = 
        (Class<? extends Module>) Class.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", 
                                               true, 
                                               currentClassLoader);
    com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass);
    modules.add(javaTimeModule);
}
catch (ClassNotFoundException ex) {
    // jackson-datatype-jsr310 not available
}

Environment Information

  • Spring AI version: 1.0.0-M7.MT2
  • Spring Boot version: 2.7.18
  • Java version: jdk17
  • Jackson version: 2.18.3

Related Code

The issue appears in the following classes:

  1. org.springframework.ai.util.JacksonUtils - Responsible for loading Jackson modules
  2. org.springframework.ai.model.ModelOptionsUtils - Contains the static OBJECT_MAPPER field
  3. org.springframework.util.ClassUtils - Provides class loader utility methods

Additional Information

This issue is caused by a mismatch between Spring Boot's class loader hierarchy and Spring Framework's class loader usage patterns. In Spring Boot applications, application classes and dependencies are loaded by LaunchedURLClassLoader, while Spring Framework's utility class ClassUtils defaults to using the thread context class loader (AppClassLoader), resulting in an inability to find classes in application dependencies.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions