Skip to content

Commit 08fa89e

Browse files
committed
Support virtual threads for Jackson pool.
1 parent 1130ec5 commit 08fa89e

File tree

4 files changed

+65
-4
lines changed

4 files changed

+65
-4
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/JacksonHttpMessageConvertersConfiguration.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@
1616

1717
package org.springframework.boot.autoconfigure.http;
1818

19+
import com.fasterxml.jackson.core.util.JsonRecyclerPools;
1920
import com.fasterxml.jackson.databind.ObjectMapper;
2021
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
2122

2223
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2324
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2425
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2526
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
27+
import org.springframework.boot.autoconfigure.thread.Threading;
2628
import org.springframework.context.annotation.Bean;
2729
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.core.env.Environment;
2831
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
2932
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
3033
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
@@ -63,8 +66,13 @@ protected static class MappingJackson2XmlHttpMessageConverterConfiguration {
6366
@Bean
6467
@ConditionalOnMissingBean
6568
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter(
66-
Jackson2ObjectMapperBuilder builder) {
67-
return new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build());
69+
Jackson2ObjectMapperBuilder builder, Environment environment) {
70+
ObjectMapper om = builder.createXmlMapper(true).build();
71+
if (Threading.VIRTUAL.isActive(environment)) {
72+
om.getFactory().setRecyclerPool(JsonRecyclerPools.sharedLockFreePool());
73+
}
74+
75+
return new MappingJackson2XmlHttpMessageConverter(om);
6876
}
6977

7078
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.fasterxml.jackson.annotation.JsonAutoDetect;
3232
import com.fasterxml.jackson.annotation.JsonCreator;
3333
import com.fasterxml.jackson.annotation.PropertyAccessor;
34+
import com.fasterxml.jackson.core.util.JsonRecyclerPools;
3435
import com.fasterxml.jackson.databind.Module;
3536
import com.fasterxml.jackson.databind.ObjectMapper;
3637
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
@@ -49,6 +50,7 @@
4950
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
5051
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
5152
import org.springframework.boot.autoconfigure.jackson.JacksonProperties.ConstructorDetectorStrategy;
53+
import org.springframework.boot.autoconfigure.thread.Threading;
5254
import org.springframework.boot.context.properties.EnableConfigurationProperties;
5355
import org.springframework.boot.jackson.JsonComponentModule;
5456
import org.springframework.boot.jackson.JsonMixinModule;
@@ -59,6 +61,7 @@
5961
import org.springframework.context.annotation.Primary;
6062
import org.springframework.context.annotation.Scope;
6163
import org.springframework.core.Ordered;
64+
import org.springframework.core.env.Environment;
6265
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
6366
import org.springframework.util.Assert;
6467
import org.springframework.util.ClassUtils;
@@ -127,8 +130,13 @@ static class JacksonObjectMapperConfiguration {
127130
@Bean
128131
@Primary
129132
@ConditionalOnMissingBean
130-
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
131-
return builder.createXmlMapper(false).build();
133+
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder, Environment environment) {
134+
ObjectMapper om = builder.createXmlMapper(false).build();
135+
if (Threading.VIRTUAL.isActive(environment)) {
136+
om.getFactory().setRecyclerPool(JsonRecyclerPools.sharedLockFreePool());
137+
}
138+
139+
return om;
132140
}
133141

134142
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfigurationTests.java

+19
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818

1919
import java.nio.charset.StandardCharsets;
2020

21+
import com.fasterxml.jackson.core.JsonFactory;
22+
import com.fasterxml.jackson.core.util.JsonRecyclerPools.LockFreePool;
2123
import com.fasterxml.jackson.databind.ObjectMapper;
2224
import com.google.gson.Gson;
2325
import jakarta.json.bind.Jsonb;
2426
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.condition.EnabledOnJre;
28+
import org.junit.jupiter.api.condition.JRE;
2529

2630
import org.springframework.aot.hint.RuntimeHints;
2731
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
@@ -292,6 +296,21 @@ void shouldRegisterHints() {
292296
assertThat(RuntimeHintsPredicates.reflection().onMethod(Encoding.class, "shouldForce")).rejects(hints);
293297
}
294298

299+
@Test
300+
@EnabledOnJre(JRE.JAVA_21)
301+
void shouldUseVirtualThreads() {
302+
this.contextRunner.withUserConfiguration(JacksonObjectMapperBuilderConfig.class)
303+
.withPropertyValues("spring.threads.virtual.enabled:true")
304+
.run((context) -> {
305+
MappingJackson2XmlHttpMessageConverter converter = context
306+
.getBean("mappingJackson2XmlHttpMessageConverter", MappingJackson2XmlHttpMessageConverter.class);
307+
assertThat(converter).extracting(MappingJackson2XmlHttpMessageConverter::getObjectMapper)
308+
.extracting(ObjectMapper::getFactory)
309+
.extracting(JsonFactory::_getRecyclerPool)
310+
.isInstanceOf(LockFreePool.class);
311+
});
312+
}
313+
295314
private ApplicationContextRunner allOptionsRunner() {
296315
return this.contextRunner.withConfiguration(AutoConfigurations.of(GsonAutoConfiguration.class,
297316
JacksonAutoConfiguration.class, JsonbAutoConfiguration.class));

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java

+26
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
3030
import com.fasterxml.jackson.annotation.JsonFormat;
3131
import com.fasterxml.jackson.annotation.JsonInclude;
32+
import com.fasterxml.jackson.core.JsonFactory;
3233
import com.fasterxml.jackson.core.JsonGenerator;
3334
import com.fasterxml.jackson.core.JsonParser;
3435
import com.fasterxml.jackson.core.ObjectCodec;
36+
import com.fasterxml.jackson.core.util.JsonRecyclerPools.LockFreePool;
37+
import com.fasterxml.jackson.core.util.JsonRecyclerPools.ThreadLocalPool;
3538
import com.fasterxml.jackson.databind.AnnotationIntrospector;
3639
import com.fasterxml.jackson.databind.DeserializationConfig;
3740
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -53,6 +56,8 @@
5356
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
5457
import org.assertj.core.api.InstanceOfAssertFactories;
5558
import org.junit.jupiter.api.Test;
59+
import org.junit.jupiter.api.condition.EnabledOnJre;
60+
import org.junit.jupiter.api.condition.JRE;
5661

5762
import org.springframework.aot.hint.RuntimeHints;
5863
import org.springframework.aot.hint.predicate.ReflectionHintsPredicates;
@@ -491,6 +496,27 @@ void shouldRegisterPropertyNamingStrategyHints() {
491496
"UPPER_CAMEL_CASE", "SNAKE_CASE", "UPPER_SNAKE_CASE", "LOWER_CASE", "KEBAB_CASE", "LOWER_DOT_CASE");
492497
}
493498

499+
@Test
500+
void shouldUseThreadLocalPool() {
501+
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled:false").run((context) -> {
502+
ObjectMapper mapper = context.getBean(ObjectMapper.class);
503+
assertThat(mapper).extracting(ObjectMapper::getFactory)
504+
.extracting(JsonFactory::_getRecyclerPool)
505+
.isInstanceOf(ThreadLocalPool.class);
506+
});
507+
}
508+
509+
@Test
510+
@EnabledOnJre(JRE.JAVA_21)
511+
void shouldUseVirtualThread() {
512+
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled:true").run((context) -> {
513+
ObjectMapper mapper = context.getBean(ObjectMapper.class);
514+
assertThat(mapper).extracting(ObjectMapper::getFactory)
515+
.extracting(JsonFactory::_getRecyclerPool)
516+
.isInstanceOf(LockFreePool.class);
517+
});
518+
}
519+
494520
private void shouldRegisterPropertyNamingStrategyHints(Class<?> type, String... fieldNames) {
495521
RuntimeHints hints = new RuntimeHints();
496522
new JacksonAutoConfigurationRuntimeHints().registerHints(hints, getClass().getClassLoader());

0 commit comments

Comments
 (0)