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 63ab09fd708..349b07ad19d 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 @@ -247,14 +247,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/jmh/java/org/springframework/http/codec/json/Jackson2JsonEncoderBenchmark.java b/spring-web/src/jmh/java/org/springframework/http/codec/json/Jackson2JsonEncoderBenchmark.java new file mode 100644 index 00000000000..2a6602af60e --- /dev/null +++ b/spring-web/src/jmh/java/org/springframework/http/codec/json/Jackson2JsonEncoderBenchmark.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec.json; + +import java.util.Collections; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +/** + * Benchmarks for encoding POJOs to JSON using Jackson. + * + * @author Brian Clozel + * @see AbstractJackson2Encoder + */ +@BenchmarkMode(Mode.Throughput) +public class Jackson2JsonEncoderBenchmark { + + + /** + * Benchmark data holding {@link Project} to be serialized by the JSON Encoder. + * A {@code projectCount} parameter can be used to grow the size of the object graph to serialize. + */ + @State(Scope.Benchmark) + public static class EncodeSingleData { + + @Param({"0", "50", "500"}) + int projectCount; + + Jackson2JsonEncoder jsonEncoder; + + DataBufferFactory bufferFactory; + + ResolvableType resolvableType; + + Project project; + + @Setup + public void setup() { + final Jackson2ObjectMapperBuilder mapperBuilder = new Jackson2ObjectMapperBuilder(); + ObjectMapper objectMapper = mapperBuilder.build(); + this.bufferFactory = new DefaultDataBufferFactory(); + this.jsonEncoder = new Jackson2JsonEncoder(objectMapper); + this.resolvableType = ResolvableType.forClass(Project.class); + this.project = new Project("spring", this.projectCount); + } + + } + + @Benchmark + public DataBuffer encodeValue(EncodeSingleData data) { + return data.jsonEncoder.encodeValue(data.project, data.bufferFactory, data.resolvableType, MediaType.APPLICATION_JSON, Collections.emptyMap()); + } + + /** + * Benchmark data holding {@link Project} to be serialized by the JSON Encoder. + * A {@code projectCount} parameter can be used to grow the size of the object graph to serialize. + */ + @State(Scope.Benchmark) + public static class EncodeData extends EncodeSingleData { + + @Param({"1", "50", "500"}) + int streamSize; + + } + + @Benchmark + public void encode(Blackhole bh, EncodeData data) { + Flux projects = Flux.generate(sink -> sink.next(data.project)).take(data.streamSize).cast(Project.class); + data.jsonEncoder.encode(projects, data.bufferFactory, data.resolvableType, MediaType.APPLICATION_JSON, Collections.emptyMap()) + .doOnNext(bh::consume) + .then().block(); + } + +} diff --git a/spring-web/src/jmh/java/org/springframework/http/codec/json/Project.java b/spring-web/src/jmh/java/org/springframework/http/codec/json/Project.java new file mode 100644 index 00000000000..60a922dd611 --- /dev/null +++ b/spring-web/src/jmh/java/org/springframework/http/codec/json/Project.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec.json; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Sample Pojo for JSON encoder benchmarks. + * @author Brian Clozel + */ +public class Project { + + private String name; + + private String url; + + private List subProjects = Collections.emptyList(); + + public Project() { + } + + public Project(String name) { + this.name = name; + this.url = "https://spring.io/projects/" + name; + } + + public Project(String name, int subProjectsCount) { + this(name); + this.subProjects = new ArrayList<>(subProjectsCount); + for (int i = 0; i < subProjectsCount; i++) { + this.subProjects.add(new Project(name + i)); + } + } + + public void setName(String name) { + this.name = name; + } + + public void setUrl(String url) { + this.url = url; + } + + public List getSubProjects() { + return this.subProjects; + } + + public void setSubProjects(List subProjects) { + this.subProjects = subProjects; + } + + public String getName() { + return this.name; + } + + public String getUrl() { + return this.url; + } + +} 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 07bb9581e74..70d9df4c7ed 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 @@ -140,7 +140,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); @@ -163,30 +172,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 5e900595762..d550a8accc6 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 b24f766eae9..9d116dc8ca0 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(); }