Skip to content

Commit d50b51e

Browse files
committed
Fix ordering of releasing resources in JSON Encoder
Prior to this commit, the Jackson 2.x encoders, in case of encoding a stream of data, would first release the `ByteArrayBuilder` and then the `JsonGenerator`. This order is inconsistent with the single value variant (see `o.s.h.codec.json.AbstractJackson2Encoder#encodeValue`) and invalid since the `JsonGenerator` uses internally the `ByteArrayBuilder`. In case of a CSV Encoder, the codec can buffer data to write the column names of the CSV file. Writing an empty Flux with this Encoder would not fail but still log a NullPointerException ignored by the reactive pipeline. This commit fixes the order and avoid such issues at runtime. Fixes gh-30493
1 parent 03b9edc commit d50b51e

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

spring-web/spring-web.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ dependencies {
7878
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
7979
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin")
8080
testImplementation("com.fasterxml.jackson.module:jackson-module-parameter-names")
81+
testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv")
8182
testImplementation("com.squareup.okhttp3:mockwebserver")
8283
testImplementation("io.micrometer:micrometer-observation-test")
8384
testImplementation("io.projectreactor:reactor-test")

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory buffe
205205
.doOnNext(dataBuffer -> Hints.touchDataBuffer(dataBuffer, hintsToUse, logger))
206206
.doAfterTerminate(() -> {
207207
try {
208-
byteBuilder.release();
209208
generator.close();
209+
byteBuilder.release();
210210
}
211211
catch (IOException ex) {
212212
logger.error("Could not close Encoder resources", ex);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.codec.json;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import com.fasterxml.jackson.databind.ObjectWriter;
24+
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
25+
import org.junit.jupiter.api.Test;
26+
import reactor.core.publisher.Flux;
27+
28+
import org.springframework.core.ResolvableType;
29+
import org.springframework.core.testfixture.codec.AbstractEncoderTests;
30+
import org.springframework.http.MediaType;
31+
import org.springframework.util.Assert;
32+
import org.springframework.util.MimeType;
33+
import org.springframework.web.testfixture.xml.Pojo;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
37+
/**
38+
* Tests for {@link AbstractJackson2Encoder} for the CSV variant and how resources are managed.
39+
* @author Brian Clozel
40+
*/
41+
class JacksonCsvEncoderTests extends AbstractEncoderTests<org.springframework.http.codec.json.JacksonCsvEncoderTests.JacksonCsvEncoder> {
42+
43+
public JacksonCsvEncoderTests() {
44+
super(new JacksonCsvEncoder());
45+
}
46+
47+
@Test
48+
@Override
49+
public void canEncode() throws Exception {
50+
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
51+
assertThat(this.encoder.canEncode(pojoType, JacksonCsvEncoder.TEXT_CSV)).isTrue();
52+
}
53+
54+
@Test
55+
@Override
56+
public void encode() throws Exception {
57+
Flux<Object> input = Flux.just(new Pojo("spring", "framework"),
58+
new Pojo("spring", "data"),
59+
new Pojo("spring", "boot"));
60+
61+
testEncode(input, Pojo.class, step -> step
62+
.consumeNextWith(expectString("bar,foo\nframework,spring\n"))
63+
.consumeNextWith(expectString("data,spring\n"))
64+
.consumeNextWith(expectString("boot,spring\n"))
65+
.verifyComplete());
66+
}
67+
68+
@Test
69+
// See gh-30493
70+
// this test did not fail directly but logged a NullPointerException dropped by the reactive pipeline
71+
void encodeEmptyFlux() {
72+
Flux<Object> input = Flux.empty();
73+
testEncode(input, Pojo.class, step -> step.verifyComplete());
74+
}
75+
76+
static class JacksonCsvEncoder extends AbstractJackson2Encoder {
77+
public static final MediaType TEXT_CSV = new MediaType("text", "csv");
78+
79+
public JacksonCsvEncoder() {
80+
this(CsvMapper.builder().build(), TEXT_CSV);
81+
}
82+
83+
@Override
84+
protected byte[] getStreamingMediaTypeSeparator(MimeType mimeType) {
85+
// CsvMapper emits newlines
86+
return new byte[0];
87+
}
88+
89+
public JacksonCsvEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
90+
super(mapper, mimeTypes);
91+
Assert.isInstanceOf(CsvMapper.class, mapper);
92+
setStreamingMediaTypes(List.of(TEXT_CSV));
93+
}
94+
95+
@Override
96+
protected ObjectWriter customizeWriter(ObjectWriter writer, MimeType mimeType, ResolvableType elementType, Map<String, Object> hints) {
97+
var mapper = (CsvMapper) getObjectMapper();
98+
return writer.with(mapper.schemaFor(elementType.toClass()).withHeader());
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)