Skip to content

Commit 9d9d539

Browse files
committed
Do not use the Thread Context ClassLoader to load jackson modules.
Fixes #2921 Signed-off-by: Eric Bottard <[email protected]>
1 parent cdf5643 commit 9d9d539

File tree

3 files changed

+70
-10
lines changed

3 files changed

+70
-10
lines changed

spring-ai-commons/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@
106106
<artifactId>jackson-module-kotlin</artifactId>
107107
<scope>test</scope>
108108
</dependency>
109+
<dependency>
110+
<groupId>com.fasterxml.jackson.datatype</groupId>
111+
<artifactId>jackson-datatype-jsr310</artifactId>
112+
<scope>test</scope>
113+
</dependency>
109114

110115
</dependencies>
111116

spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@
2323

2424
import org.springframework.beans.BeanUtils;
2525
import org.springframework.core.KotlinDetector;
26-
import org.springframework.util.ClassUtils;
2726

2827
/**
2928
* Utility methods for Jackson.
@@ -43,8 +42,8 @@ public abstract class JacksonUtils {
4342
public static List<Module> instantiateAvailableModules() {
4443
List<Module> modules = new ArrayList<>();
4544
try {
46-
Class<? extends com.fasterxml.jackson.databind.Module> jdk8ModuleClass = (Class<? extends Module>) ClassUtils
47-
.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", null);
45+
Class<? extends com.fasterxml.jackson.databind.Module> jdk8ModuleClass = (Class<? extends Module>) Class
46+
.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module");
4847
com.fasterxml.jackson.databind.Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass);
4948
modules.add(jdk8Module);
5049
}
@@ -53,8 +52,8 @@ public static List<Module> instantiateAvailableModules() {
5352
}
5453

5554
try {
56-
Class<? extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass = (Class<? extends Module>) ClassUtils
57-
.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null);
55+
Class<? extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass = (Class<? extends Module>) Class
56+
.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule");
5857
com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass);
5958
modules.add(javaTimeModule);
6059
}
@@ -63,8 +62,8 @@ public static List<Module> instantiateAvailableModules() {
6362
}
6463

6564
try {
66-
Class<? extends com.fasterxml.jackson.databind.Module> parameterNamesModuleClass = (Class<? extends Module>) ClassUtils
67-
.forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule", null);
65+
Class<? extends com.fasterxml.jackson.databind.Module> parameterNamesModuleClass = (Class<? extends Module>) Class
66+
.forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule");
6867
com.fasterxml.jackson.databind.Module parameterNamesModule = BeanUtils
6968
.instantiateClass(parameterNamesModuleClass);
7069
modules.add(parameterNamesModule);
@@ -76,8 +75,8 @@ public static List<Module> instantiateAvailableModules() {
7675
// Kotlin present?
7776
if (KotlinDetector.isKotlinPresent()) {
7877
try {
79-
Class<? extends com.fasterxml.jackson.databind.Module> kotlinModuleClass = (Class<? extends Module>) ClassUtils
80-
.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", null);
78+
Class<? extends com.fasterxml.jackson.databind.Module> kotlinModuleClass = (Class<? extends Module>) Class
79+
.forName("com.fasterxml.jackson.module.kotlin.KotlinModule");
8180
Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass);
8281
modules.add(kotlinModule);
8382
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.util;
18+
19+
import java.time.Duration;
20+
import java.time.temporal.ChronoUnit;
21+
22+
import com.fasterxml.jackson.core.JsonProcessingException;
23+
import com.fasterxml.jackson.databind.json.JsonMapper;
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
class JacksonUtilsTests {
29+
30+
/*
31+
* Make sure that JacksonUtils use the correct classloader to load modules. See
32+
* https://github.com/spring-projects/spring-ai/issues/2921
33+
*/
34+
@Test
35+
void usesCorrectClassLoader() throws JsonProcessingException, ClassNotFoundException {
36+
ClassLoader previousLoader = Thread.currentThread().getContextClassLoader();
37+
try {
38+
// This parent CL cannot see the clazz class below. But this shouldn't matter.
39+
Thread.currentThread().setContextClassLoader(getClass().getClassLoader().getParent());
40+
// Should work whatever the current Thread context CL is
41+
var jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
42+
Class<?> clazz = getClass().getClassLoader().loadClass(getClass().getName() + "$Cell");
43+
var output = jsonMapper.readValue("{\"name\":\"Amoeba\",\"lifespan\":\"PT42S\"}", clazz);
44+
assertThat(output).isEqualTo(new Cell("Amoeba", Duration.of(42L, ChronoUnit.SECONDS)));
45+
46+
}
47+
finally {
48+
Thread.currentThread().setContextClassLoader(previousLoader);
49+
}
50+
51+
}
52+
53+
record Cell(String name, Duration lifespan) {
54+
}
55+
56+
}

0 commit comments

Comments
 (0)