From 7bee3d157407c2408653e1c40608c0244be6be1d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 20 Oct 2020 15:36:23 +0200 Subject: [PATCH] Optimize Jackson resource management in codecs Prior to this commit, references to `JsonGenerator` and `ByteArrayBuilder` were not closed/released within codecs calls. This prevents Jackson from reusing more efficiently shared memory resources. This commit properly closes/releases Jackson resources in Spring MVC, Spring WebFlux and Spring Messaging codecs. A benchmark on WebFlux codecs (in both single value/streaming mode) shows significant throughput and allocation improvements for small payloads. Closes gh-25910 --- .../MappingJackson2MessageConverter.java | 15 ++--- .../codec/json/AbstractJackson2Encoder.java | 57 ++++++++++++------- .../AbstractJackson2HttpMessageConverter.java | 4 +- spring-web/src/test/resources/log4j2-test.xml | 2 +- .../view/json/AbstractJackson2View.java | 41 ++++++------- 5 files changed, 67 insertions(+), 52 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java index e0c0129cbd8..a1cc3fb0df5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java @@ -263,14 +263,15 @@ public class MappingJackson2MessageConverter extends AbstractMessageConverter { if (byte[].class == getSerializedPayloadClass()) { ByteArrayOutputStream out = new ByteArrayOutputStream(1024); JsonEncoding encoding = getJsonEncoding(getMimeType(headers)); - JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding); - if (view != null) { - this.objectMapper.writerWithView(view).writeValue(generator, payload); + try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding)) { + if (view != null) { + this.objectMapper.writerWithView(view).writeValue(generator, payload); + } + else { + this.objectMapper.writeValue(generator, payload); + } + payload = out.toByteArray(); } - else { - this.objectMapper.writeValue(generator, payload); - } - payload = out.toByteArray(); } else { // Assuming a text-based target payload diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index 53a90097817..0c34e1232d4 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -149,7 +149,16 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple return Flux.from(inputStream) .map(value -> encodeStreamingValue(value, bufferFactory, hints, sequenceWriter, byteBuilder, - separator)); + separator)) + .doAfterTerminate(() -> { + try { + byteBuilder.release(); + generator.close(); + } + catch (IOException ex) { + logger.error("Could not close Encoder resources", ex); + } + }); } catch (IOException ex) { return Flux.error(ex); @@ -172,30 +181,34 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple ObjectWriter writer = createObjectWriter(valueType, mimeType, hints); ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler()); - JsonEncoding encoding = getJsonEncoding(mimeType); - - logValue(hints, value); - try { - JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding); - writer.writeValue(generator, value); - generator.flush(); - } - catch (InvalidDefinitionException ex) { - throw new CodecException("Type definition error: " + ex.getType(), ex); - } - catch (JsonProcessingException ex) { - throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); - } - catch (IOException ex) { - throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex); - } + JsonEncoding encoding = getJsonEncoding(mimeType); - byte[] bytes = byteBuilder.toByteArray(); - DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length); - buffer.write(bytes); + logValue(hints, value); - return buffer; + try (JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding)) { + writer.writeValue(generator, value); + generator.flush(); + } + catch (InvalidDefinitionException ex) { + throw new CodecException("Type definition error: " + ex.getType(), ex); + } + catch (JsonProcessingException ex) { + throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); + } + catch (IOException ex) { + throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex); + } + + byte[] bytes = byteBuilder.toByteArray(); + DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + + return buffer; + } + finally { + byteBuilder.release(); + } } private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFactory, @Nullable Map hints, diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 7f3b59fee44..65ce5fae6c9 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -307,8 +307,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener MediaType contentType = outputMessage.getHeaders().getContentType(); JsonEncoding encoding = getJsonEncoding(contentType); - JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); - try { + + try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding)) { writePrefix(generator, object); Object value = object; diff --git a/spring-web/src/test/resources/log4j2-test.xml b/spring-web/src/test/resources/log4j2-test.xml index f37050e01ad..c8a85fff2d9 100644 --- a/spring-web/src/test/resources/log4j2-test.xml +++ b/spring-web/src/test/resources/log4j2-test.xml @@ -6,7 +6,7 @@ - + diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java index ffac9fb1478..6d3fea8f2b8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java @@ -206,29 +206,30 @@ public abstract class AbstractJackson2View extends AbstractView { * @throws IOException if writing failed */ protected void writeContent(OutputStream stream, Object object) throws IOException { - JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding); - writePrefix(generator, object); + try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding)) { + writePrefix(generator, object); - Object value = object; - Class serializationView = null; - FilterProvider filters = null; + Object value = object; + Class serializationView = null; + FilterProvider filters = null; - if (value instanceof MappingJacksonValue) { - MappingJacksonValue container = (MappingJacksonValue) value; - value = container.getValue(); - serializationView = container.getSerializationView(); - filters = container.getFilters(); + if (value instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) value; + value = container.getValue(); + serializationView = container.getSerializationView(); + filters = container.getFilters(); + } + + ObjectWriter objectWriter = (serializationView != null ? + this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer()); + if (filters != null) { + objectWriter = objectWriter.with(filters); + } + objectWriter.writeValue(generator, value); + + writeSuffix(generator, object); + generator.flush(); } - - ObjectWriter objectWriter = (serializationView != null ? - this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer()); - if (filters != null) { - objectWriter = objectWriter.with(filters); - } - objectWriter.writeValue(generator, value); - - writeSuffix(generator, object); - generator.flush(); }