Browse Source

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
pull/26319/head
Brian Clozel 4 years ago
parent
commit
7bee3d1574
  1. 15
      spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java
  2. 55
      spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java
  3. 4
      spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java
  4. 2
      spring-web/src/test/resources/log4j2-test.xml
  5. 43
      spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java

15
spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java

@ -263,14 +263,15 @@ public class MappingJackson2MessageConverter extends AbstractMessageConverter { @@ -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);
}
else {
this.objectMapper.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();
}
payload = out.toByteArray();
}
else {
// Assuming a text-based target payload

55
spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java

@ -149,7 +149,16 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple @@ -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 @@ -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);
try {
JsonEncoding encoding = getJsonEncoding(mimeType);
logValue(hints, value);
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);
}
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);
byte[] bytes = byteBuilder.toByteArray();
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
return buffer;
}
finally {
byteBuilder.release();
}
}
private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFactory, @Nullable Map<String, Object> hints,

4
spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java

@ -307,8 +307,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener @@ -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;

2
spring-web/src/test/resources/log4j2-test.xml

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
</Console>
</Appenders>
<Loggers>
<Logger name="org.springframework.web" level="debug" />
<Logger name="org.springframework.web" level="warn" />
<Logger name="org.springframework.beans" level="warn" />
<Logger name="org.springframework.binding" level="warn" />
<Logger name="org.springframework.http" level="warn" />

43
spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java

@ -206,29 +206,30 @@ public abstract class AbstractJackson2View extends AbstractView { @@ -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);
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();
}
try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding)) {
writePrefix(generator, object);
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();
}
ObjectWriter objectWriter = (serializationView != null ?
this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
objectWriter.writeValue(generator, value);
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();
writeSuffix(generator, object);
generator.flush();
}
}

Loading…
Cancel
Save