Merge branch '7.0.x'

This commit is contained in:
Brian Clozel
2026-07-16 19:10:36 +02:00
3 changed files with 85 additions and 4 deletions
@@ -32,6 +32,7 @@ import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonEncoding;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.exc.JacksonIOException;
import tools.jackson.core.util.BufferRecycler;
import tools.jackson.core.util.ByteArrayBuilder;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ObjectMapper;
@@ -159,7 +160,8 @@ public abstract class AbstractJacksonEncoder<T extends ObjectMapper> extends Jac
}
ObjectWriter writer = createObjectWriter(mapper, elementType, mimeType, hintsToUse);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.generatorFactory()._getBufferRecycler());
BufferRecycler recycler = writer.generatorFactory()._getBufferRecycler();
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(recycler);
JsonEncoding encoding = getJsonEncoding(mimeType);
JsonGenerator generator = mapper.createGenerator(byteBuilder, encoding);
SequenceWriter sequenceWriter = writer.writeValues(generator);
@@ -204,6 +206,11 @@ public abstract class AbstractJacksonEncoder<T extends ObjectMapper> extends Jac
catch (JacksonIOException ex) {
logger.error("Could not close Encoder resources", ex);
}
finally {
// Release strictly after the generator and builder are done: the
// recycler returns to a concurrent pool and may be reused at once.
recycler.releaseToPool();
}
});
}
catch (JacksonIOException ex) {
@@ -223,7 +230,8 @@ public abstract class AbstractJacksonEncoder<T extends ObjectMapper> extends Jac
ObjectWriter writer = createObjectWriter(mapper, valueType, mimeType, hints);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.generatorFactory()._getBufferRecycler());
BufferRecycler recycler = writer.generatorFactory()._getBufferRecycler();
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(recycler);
try {
JsonEncoding encoding = getJsonEncoding(mimeType);
@@ -249,6 +257,7 @@ public abstract class AbstractJacksonEncoder<T extends ObjectMapper> extends Jac
}
finally {
byteBuilder.release();
recycler.releaseToPool();
}
}
@@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.BufferRecycler;
import com.fasterxml.jackson.core.util.ByteArrayBuilder;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -171,7 +172,8 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
}
ObjectWriter writer = createObjectWriter(mapper, elementType, mimeType, null, hintsToUse);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
BufferRecycler recycler = writer.getFactory()._getBufferRecycler();
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(recycler);
JsonEncoding encoding = getJsonEncoding(mimeType);
JsonGenerator generator = mapper.getFactory().createGenerator(byteBuilder, encoding);
SequenceWriter sequenceWriter = writer.writeValues(generator);
@@ -216,6 +218,11 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
catch (IOException ex) {
logger.error("Could not close Encoder resources", ex);
}
finally {
// Release strictly after the generator and builder are done: the
// recycler returns to a concurrent pool and may be reused at once.
recycler.releaseToPool();
}
});
}
catch (IOException ex) {
@@ -247,7 +254,8 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
writer = writer.with(filters);
}
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
BufferRecycler recycler = writer.getFactory()._getBufferRecycler();
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(recycler);
try {
JsonEncoding encoding = getJsonEncoding(mimeType);
@@ -276,6 +284,7 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
}
finally {
byteBuilder.release();
recycler.releaseToPool();
}
}
@@ -21,6 +21,7 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@@ -29,6 +30,10 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.util.BufferRecycler;
import tools.jackson.core.util.JsonRecyclerPools;
import tools.jackson.core.util.RecyclerPool;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.ser.FilterProvider;
@@ -37,6 +42,7 @@ import tools.jackson.databind.ser.std.SimpleFilterProvider;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.testfixture.codec.AbstractEncoderTests;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1;
@@ -305,6 +311,41 @@ class JacksonJsonEncoderTests extends AbstractEncoderTests<JacksonJsonEncoder> {
);
}
@Test
void releasesBufferRecyclerToPool() {
CountingRecyclerPool pool = new CountingRecyclerPool();
JsonFactory factory = JsonFactory.builder().recyclerPool(pool).build();
JacksonJsonEncoder encoder = new JacksonJsonEncoder(JsonMapper.builder(factory).build());
Pojo pojo = new Pojo("foo", "bar");
ResolvableType type = ResolvableType.forClass(Pojo.class);
// encodeValue (Mono path)
drain(encoder.encode(Mono.just(pojo), this.bufferFactory, type, APPLICATION_JSON, null));
// Flux joined into a JSON array (non-streaming path)
drain(encoder.encode(Flux.just(pojo, pojo), this.bufferFactory, type, APPLICATION_JSON, null));
// Flux with a streaming media type (NDJSON separator path)
drain(encoder.encode(Flux.just(pojo, pojo), this.bufferFactory, type, APPLICATION_NDJSON, null));
// Error termination of the streaming path must still release via doAfterTerminate
encoder.encode(Flux.error(new IllegalStateException("boom")), this.bufferFactory, type, APPLICATION_NDJSON, null)
.as(StepVerifier::create)
.expectError(IllegalStateException.class)
.verify();
assertThat(pool.acquired).hasPositiveValue();
assertThat(pool.released).hasValue(pool.acquired.get());
}
private static void drain(Flux<DataBuffer> output) {
output.as(StepVerifier::create)
.thenConsumeWhile(buffer -> {
DataBufferUtils.release(buffer);
return true;
})
.verifyComplete();
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
private static class ParentClass {
@@ -322,4 +363,26 @@ class JacksonJsonEncoderTests extends AbstractEncoderTests<JacksonJsonEncoder> {
record JacksonFilteredBean(String property1, String property2) {
}
@SuppressWarnings("serial")
private static final class CountingRecyclerPool implements RecyclerPool<BufferRecycler> {
final AtomicInteger acquired = new AtomicInteger();
final AtomicInteger released = new AtomicInteger();
private final RecyclerPool<BufferRecycler> delegate = JsonRecyclerPools.newConcurrentDequePool();
@Override
public BufferRecycler acquirePooled() {
this.acquired.incrementAndGet();
return this.delegate.acquirePooled();
}
@Override
public void releasePooled(BufferRecycler recycler) {
this.released.incrementAndGet();
this.delegate.releasePooled(recycler);
}
}
}