Skip to content

Commit 7603085

Browse files
authored
Merge pull request #44373 from geoand/#44231
Ensure that custom Jackson modules work in dev-mode
2 parents 30d733f + 77747da commit 7603085

File tree

5 files changed

+201
-8
lines changed

5 files changed

+201
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package io.quarkus.resteasy.reactive.jackson.deployment.test;
2+
3+
import static io.restassured.RestAssured.given;
4+
import static org.hamcrest.Matchers.containsString;
5+
6+
import java.io.IOException;
7+
8+
import jakarta.inject.Singleton;
9+
import jakarta.ws.rs.GET;
10+
import jakarta.ws.rs.Path;
11+
import jakarta.ws.rs.Produces;
12+
import jakarta.ws.rs.core.MediaType;
13+
14+
import org.jboss.shrinkwrap.api.asset.StringAsset;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.RegisterExtension;
17+
18+
import com.fasterxml.jackson.core.JsonGenerator;
19+
import com.fasterxml.jackson.core.JsonParser;
20+
import com.fasterxml.jackson.core.JsonToken;
21+
import com.fasterxml.jackson.databind.DeserializationContext;
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import com.fasterxml.jackson.databind.SerializerProvider;
24+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
25+
import com.fasterxml.jackson.databind.module.SimpleModule;
26+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
27+
28+
import io.quarkus.jackson.ObjectMapperCustomizer;
29+
import io.quarkus.test.QuarkusDevModeTest;
30+
import io.vertx.core.json.JsonArray;
31+
32+
public class CustomModuleLiveReloadTest {
33+
34+
@RegisterExtension
35+
static final QuarkusDevModeTest TEST = new QuarkusDevModeTest()
36+
.withApplicationRoot((jar) -> jar
37+
.addClasses(Resource.class, StringAndInt.class, StringAndIntSerializer.class,
38+
StringAndIntDeserializer.class, Customizer.class)
39+
.addAsResource(new StringAsset("index content"), "META-INF/resources/index.html"));
40+
41+
@Test
42+
void test() {
43+
assertResponse();
44+
45+
// force reload
46+
TEST.addResourceFile("META-INF/resources/index.html", "html content");
47+
48+
assertResponse();
49+
}
50+
51+
private static void assertResponse() {
52+
given().accept("application/json").get("test/array")
53+
.then()
54+
.statusCode(200)
55+
.body(containsString("first:1"), containsString("second:2"));
56+
}
57+
58+
@Path("test")
59+
public static class Resource {
60+
61+
@Path("array")
62+
@GET
63+
@Produces(MediaType.APPLICATION_JSON)
64+
public JsonArray array() {
65+
var array = new JsonArray();
66+
array.add(new StringAndInt("first", 1));
67+
array.add(new StringAndInt("second", 2));
68+
return array;
69+
}
70+
}
71+
72+
public static class StringAndInt {
73+
private final String stringValue;
74+
private final int intValue;
75+
76+
public StringAndInt(String s, int i) {
77+
this.stringValue = s;
78+
this.intValue = i;
79+
}
80+
81+
public static StringAndInt parse(String value) {
82+
if (value == null) {
83+
return null;
84+
}
85+
int dot = value.indexOf(':');
86+
if (-1 == dot) {
87+
throw new IllegalArgumentException(value);
88+
}
89+
try {
90+
return new StringAndInt(value.substring(0, dot), Integer.parseInt(value.substring(dot + 1)));
91+
} catch (NumberFormatException e) {
92+
throw new IllegalArgumentException(value, e);
93+
}
94+
}
95+
96+
public String format() {
97+
return this.stringValue + ":" + intValue;
98+
}
99+
}
100+
101+
public static class StringAndIntSerializer extends StdSerializer<StringAndInt> {
102+
103+
public StringAndIntSerializer() {
104+
super(StringAndInt.class);
105+
}
106+
107+
@Override
108+
public void serialize(StringAndInt value, JsonGenerator gen, SerializerProvider provider) throws IOException {
109+
if (value == null)
110+
gen.writeNull();
111+
else {
112+
gen.writeString(value.format());
113+
}
114+
}
115+
}
116+
117+
public static class StringAndIntDeserializer extends StdDeserializer<StringAndInt> {
118+
119+
public StringAndIntDeserializer() {
120+
super(StringAndInt.class);
121+
}
122+
123+
@Override
124+
public StringAndInt deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
125+
if (p.currentToken() == JsonToken.VALUE_STRING) {
126+
return StringAndInt.parse(p.getText());
127+
} else if (p.currentToken() == JsonToken.VALUE_NULL) {
128+
return null;
129+
}
130+
return null;
131+
}
132+
}
133+
134+
@Singleton
135+
public static class Customizer implements ObjectMapperCustomizer {
136+
@Override
137+
public void customize(ObjectMapper objectMapper) {
138+
var m = new SimpleModule("test");
139+
m.addSerializer(StringAndInt.class, new StringAndIntSerializer());
140+
m.addDeserializer(StringAndInt.class, new StringAndIntDeserializer());
141+
objectMapper.registerModule(m);
142+
}
143+
}
144+
}

extensions/vertx/deployment/src/main/java/io/quarkus/vertx/core/deployment/VertxCoreProcessor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
3232
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
3333
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
34+
import io.quarkus.deployment.IsDevelopment;
3435
import io.quarkus.deployment.annotations.BuildProducer;
3536
import io.quarkus.deployment.annotations.BuildStep;
3637
import io.quarkus.deployment.annotations.ExecutionTime;
@@ -286,6 +287,12 @@ ContextHandlerBuildItem createVertxContextHandlers(VertxCoreRecorder recorder, V
286287
return new ContextHandlerBuildItem(recorder.executionContextHandler(buildConfig.customizeArcContext()));
287288
}
288289

290+
@BuildStep(onlyIf = IsDevelopment.class)
291+
@Record(ExecutionTime.RUNTIME_INIT)
292+
public void resetMapper(VertxCoreRecorder recorder, ShutdownContextBuildItem shutdown) {
293+
recorder.resetMapper(shutdown);
294+
}
295+
289296
private void handleBlockingWarningsInDevOrTestMode() {
290297
try {
291298
Filter debuggerFilter = createDebuggerFilter();

extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/VertxCoreRecorder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle;
5050
import io.quarkus.vertx.mdc.provider.LateBoundMDCProvider;
5151
import io.quarkus.vertx.runtime.VertxCurrentContextFactory;
52+
import io.quarkus.vertx.runtime.jackson.QuarkusJacksonFactory;
5253
import io.vertx.core.AsyncResult;
5354
import io.vertx.core.Context;
5455
import io.vertx.core.Handler;
@@ -583,6 +584,15 @@ public Thread newThread(Runnable runnable) {
583584
};
584585
}
585586

587+
public void resetMapper(ShutdownContext shutdown) {
588+
shutdown.addShutdownTask(new Runnable() {
589+
@Override
590+
public void run() {
591+
QuarkusJacksonFactory.reset();
592+
}
593+
});
594+
}
595+
586596
private static void setNewThreadTccl(VertxThread thread) {
587597
ClassLoader cl = VertxCoreRecorder.currentDevModeNewThreadCreationClassLoader;
588598
if (cl == null) {

extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonFactory.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.quarkus.vertx.runtime.jackson;
22

3+
import java.util.concurrent.atomic.AtomicInteger;
4+
35
import io.vertx.core.json.jackson.DatabindCodec;
46
import io.vertx.core.json.jackson.JacksonCodec;
57
import io.vertx.core.spi.JsonFactory;
@@ -10,6 +12,8 @@
1012
*/
1113
public class QuarkusJacksonFactory implements JsonFactory {
1214

15+
private static final AtomicInteger COUNTER = new AtomicInteger();
16+
1317
@Override
1418
public JsonCodec codec() {
1519
JsonCodec codec;
@@ -25,7 +29,15 @@ public JsonCodec codec() {
2529
codec = new JacksonCodec();
2630
}
2731
}
32+
COUNTER.incrementAndGet();
2833
return codec;
2934
}
3035

36+
public static void reset() {
37+
// if we blindly reset, we could get NCDFE because Jackson classes would not have been loaded
38+
if (COUNTER.get() > 0) {
39+
QuarkusJacksonJsonCodec.reset();
40+
}
41+
}
42+
3143
}

extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/QuarkusJacksonJsonCodec.java

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,31 @@
3030
*/
3131
class QuarkusJacksonJsonCodec implements JsonCodec {
3232

33-
private static final ObjectMapper mapper;
33+
private static volatile ObjectMapper mapper;
3434
// we don't want to create this unless it's absolutely necessary (and it rarely is)
3535
private static volatile ObjectMapper prettyMapper;
3636

3737
static {
38+
populateMapper();
39+
}
40+
41+
public static void reset() {
42+
mapper = null;
43+
prettyMapper = null;
44+
}
45+
46+
private static ObjectMapper mapper() {
47+
if (mapper == null) {
48+
synchronized (QuarkusJacksonJsonCodec.class) {
49+
if (mapper == null) {
50+
populateMapper();
51+
}
52+
}
53+
}
54+
return mapper;
55+
}
56+
57+
private static void populateMapper() {
3858
ArcContainer container = Arc.container();
3959
if (container == null) {
4060
// this can happen in QuarkusUnitTest
@@ -74,7 +94,7 @@ class QuarkusJacksonJsonCodec implements JsonCodec {
7494

7595
private ObjectMapper prettyMapper() {
7696
if (prettyMapper == null) {
77-
prettyMapper = mapper.copy();
97+
prettyMapper = mapper().copy();
7898
prettyMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
7999
}
80100
return prettyMapper;
@@ -83,7 +103,7 @@ private ObjectMapper prettyMapper() {
83103
@SuppressWarnings("unchecked")
84104
@Override
85105
public <T> T fromValue(Object json, Class<T> clazz) {
86-
T value = QuarkusJacksonJsonCodec.mapper.convertValue(json, clazz);
106+
T value = QuarkusJacksonJsonCodec.mapper().convertValue(json, clazz);
87107
if (clazz == Object.class) {
88108
value = (T) adapt(value);
89109
}
@@ -102,7 +122,7 @@ public <T> T fromBuffer(Buffer buf, Class<T> clazz) throws DecodeException {
102122

103123
public static JsonParser createParser(Buffer buf) {
104124
try {
105-
return QuarkusJacksonJsonCodec.mapper.getFactory()
125+
return QuarkusJacksonJsonCodec.mapper().getFactory()
106126
.createParser((InputStream) new ByteBufInputStream(buf.getByteBuf()));
107127
} catch (IOException e) {
108128
throw new DecodeException("Failed to decode:" + e.getMessage(), e);
@@ -111,7 +131,7 @@ public static JsonParser createParser(Buffer buf) {
111131

112132
public static JsonParser createParser(String str) {
113133
try {
114-
return QuarkusJacksonJsonCodec.mapper.getFactory().createParser(str);
134+
return QuarkusJacksonJsonCodec.mapper().getFactory().createParser(str);
115135
} catch (IOException e) {
116136
throw new DecodeException("Failed to decode:" + e.getMessage(), e);
117137
}
@@ -122,7 +142,7 @@ public static <T> T fromParser(JsonParser parser, Class<T> type) throws DecodeEx
122142
T value;
123143
JsonToken remaining;
124144
try {
125-
value = QuarkusJacksonJsonCodec.mapper.readValue(parser, type);
145+
value = QuarkusJacksonJsonCodec.mapper().readValue(parser, type);
126146
remaining = parser.nextToken();
127147
} catch (Exception e) {
128148
throw new DecodeException("Failed to decode:" + e.getMessage(), e);
@@ -141,7 +161,7 @@ public static <T> T fromParser(JsonParser parser, Class<T> type) throws DecodeEx
141161
@Override
142162
public String toString(Object object, boolean pretty) throws EncodeException {
143163
try {
144-
ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper;
164+
ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper();
145165
return mapper.writeValueAsString(object);
146166
} catch (Exception e) {
147167
throw new EncodeException("Failed to encode as JSON: " + e.getMessage(), e);
@@ -151,7 +171,7 @@ public String toString(Object object, boolean pretty) throws EncodeException {
151171
@Override
152172
public Buffer toBuffer(Object object, boolean pretty) throws EncodeException {
153173
try {
154-
ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper;
174+
ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper();
155175
return Buffer.buffer(mapper.writeValueAsBytes(object));
156176
} catch (Exception e) {
157177
throw new EncodeException("Failed to encode as JSON: " + e.getMessage(), e);

0 commit comments

Comments
 (0)