Skip to content

Commit 07ba5e6

Browse files
committed
Lazily access ObjectMapper in Quarkus REST Jackson module
This is done because the server MessageBodyReader and MessageBodyWriter classes are created at static init, which means two things: * The Jackson bean should not be accessed directly as it can still be configured by user provided code (via ObjectMapperCustomizer). * Accessing the Jackson bean directly imposes a rather large hit on startup time (on my machine, it's over 50ms), regardless if MessageBodyReader / MessageBodyWriter are ever used in the application
1 parent 66b2630 commit 07ba5e6

File tree

5 files changed

+138
-18
lines changed

5 files changed

+138
-18
lines changed

extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MessageBodyReaderTests.java

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
import java.io.IOException;
88
import java.io.InputStream;
99
import java.io.OutputStream;
10+
import java.lang.annotation.Annotation;
1011
import java.nio.charset.StandardCharsets;
1112
import java.util.ArrayList;
1213
import java.util.Collections;
14+
import java.util.Iterator;
1315
import java.util.List;
1416
import java.util.Objects;
1517

18+
import jakarta.enterprise.inject.Instance;
19+
import jakarta.enterprise.util.TypeLiteral;
1620
import jakarta.ws.rs.WebApplicationException;
1721
import jakarta.ws.rs.container.CompletionCallback;
1822
import jakarta.ws.rs.container.ConnectionCallback;
@@ -119,7 +123,8 @@ void shouldThrowInvalidDefinitionException() {
119123
@Nested
120124
@DisplayName("ServerJacksonMessageBodyReader")
121125
class ServerJacksonMessageBodyReaderTests {
122-
private final CommonReaderTests tests = new CommonReaderTests(new ServerJacksonMessageBodyReader(new ObjectMapper()));
126+
private final CommonReaderTests tests = new CommonReaderTests(
127+
new ServerJacksonMessageBodyReader(new NewObjectMapperInstance()));
123128

124129
@Test
125130
void shouldThrowWebExceptionWithStreamReadExceptionCause() {
@@ -146,7 +151,7 @@ void shouldThrowInvalidDefinitionException() {
146151

147152
@Test
148153
void shouldThrowWebExceptionWithValueInstantiationExceptionCauseUsingServerRequestContext() throws IOException {
149-
var reader = new ServerJacksonMessageBodyReader(new ObjectMapper());
154+
var reader = new ServerJacksonMessageBodyReader(new NewObjectMapperInstance());
150155
// missing non-nullable property
151156
var stream = new ByteArrayInputStream("{\"cost\": 2}".getBytes(StandardCharsets.UTF_8));
152157
var context = new MockServerRequestContext(stream);
@@ -279,4 +284,56 @@ public void abortWith(Response response) {
279284

280285
}
281286
}
287+
288+
private static class NewObjectMapperInstance implements Instance<ObjectMapper> {
289+
@Override
290+
public Instance<ObjectMapper> select(Annotation... qualifiers) {
291+
throw new IllegalStateException("Should never be called");
292+
}
293+
294+
@Override
295+
public <U extends ObjectMapper> Instance<U> select(Class<U> subtype, Annotation... qualifiers) {
296+
throw new IllegalStateException("Should never be called");
297+
}
298+
299+
@Override
300+
public <U extends ObjectMapper> Instance<U> select(TypeLiteral<U> subtype, Annotation... qualifiers) {
301+
throw new IllegalStateException("Should never be called");
302+
}
303+
304+
@Override
305+
public boolean isUnsatisfied() {
306+
throw new IllegalStateException("Should never be called");
307+
}
308+
309+
@Override
310+
public boolean isAmbiguous() {
311+
throw new IllegalStateException("Should never be called");
312+
}
313+
314+
@Override
315+
public void destroy(ObjectMapper instance) {
316+
throw new IllegalStateException("Should never be called");
317+
}
318+
319+
@Override
320+
public Handle<ObjectMapper> getHandle() {
321+
throw new IllegalStateException("Should never be called");
322+
}
323+
324+
@Override
325+
public Iterable<? extends Handle<ObjectMapper>> handles() {
326+
throw new IllegalStateException("Should never be called");
327+
}
328+
329+
@Override
330+
public ObjectMapper get() {
331+
return new ObjectMapper();
332+
}
333+
334+
@Override
335+
public Iterator<ObjectMapper> iterator() {
336+
throw new IllegalStateException("Should never be called");
337+
}
338+
}
282339
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.quarkus.resteasy.reactive.jackson.runtime.serialisers;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.lang.annotation.Annotation;
6+
import java.lang.reflect.Type;
7+
import java.util.function.Supplier;
8+
9+
import jakarta.enterprise.inject.Instance;
10+
import jakarta.ws.rs.WebApplicationException;
11+
import jakarta.ws.rs.core.MediaType;
12+
import jakarta.ws.rs.core.MultivaluedMap;
13+
14+
import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader;
15+
import org.jboss.resteasy.reactive.common.util.EmptyInputStream;
16+
17+
import com.fasterxml.jackson.databind.ObjectMapper;
18+
import com.fasterxml.jackson.databind.ObjectReader;
19+
20+
import io.quarkus.arc.impl.LazyValue;
21+
22+
public abstract class AbstractServerJacksonMessageBodyReader extends AbstractJsonMessageBodyReader {
23+
24+
protected final LazyValue<ObjectReader> defaultReader;
25+
26+
public AbstractServerJacksonMessageBodyReader(Instance<ObjectMapper> mapper) {
27+
this.defaultReader = new LazyValue<>(new Supplier<>() {
28+
@Override
29+
public ObjectReader get() {
30+
return mapper.get().reader();
31+
}
32+
});
33+
}
34+
35+
@Override
36+
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
37+
MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException,
38+
WebApplicationException {
39+
return doReadFrom(type, genericType, entityStream);
40+
}
41+
42+
protected ObjectReader getEffectiveReader() {
43+
return defaultReader.get();
44+
}
45+
46+
private Object doReadFrom(Class<Object> type, Type genericType, InputStream entityStream) throws IOException {
47+
if (entityStream instanceof EmptyInputStream) {
48+
return null;
49+
}
50+
ObjectReader reader = getEffectiveReader();
51+
return reader.forType(reader.getTypeFactory().constructType(genericType != null ? genericType : type))
52+
.readValue(entityStream);
53+
}
54+
}

extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import java.util.Map;
1212
import java.util.concurrent.ConcurrentHashMap;
1313
import java.util.function.Function;
14+
import java.util.function.Supplier;
1415

16+
import jakarta.enterprise.inject.Instance;
1517
import jakarta.inject.Inject;
1618
import jakarta.ws.rs.WebApplicationException;
1719
import jakarta.ws.rs.core.MediaType;
@@ -24,22 +26,28 @@
2426
import com.fasterxml.jackson.databind.ObjectMapper;
2527
import com.fasterxml.jackson.databind.ObjectWriter;
2628

29+
import io.quarkus.arc.impl.LazyValue;
2730
import io.quarkus.resteasy.reactive.jackson.runtime.mappers.JacksonMapperUtil;
2831

2932
public class BasicServerJacksonMessageBodyWriter extends ServerMessageBodyWriter.AllWriteableMessageBodyWriter {
3033

31-
private final ObjectWriter defaultWriter;
34+
private final LazyValue<ObjectWriter> defaultWriter;
3235
private final Map<JavaType, ObjectWriter> genericWriters = new ConcurrentHashMap<>();
3336

3437
@Inject
35-
public BasicServerJacksonMessageBodyWriter(ObjectMapper mapper) {
36-
this.defaultWriter = createDefaultWriter(mapper);
38+
public BasicServerJacksonMessageBodyWriter(Instance<ObjectMapper> mapper) {
39+
this.defaultWriter = new LazyValue<>(new Supplier<>() {
40+
@Override
41+
public ObjectWriter get() {
42+
return createDefaultWriter(mapper.get());
43+
}
44+
});
3745
}
3846

3947
private ObjectWriter getWriter(Type genericType, Object value) {
4048
// make sure we properly handle polymorphism in generic collections
4149
if (value != null && genericType != null) {
42-
JavaType rootType = JacksonMapperUtil.getGenericRootType(genericType, defaultWriter);
50+
JavaType rootType = JacksonMapperUtil.getGenericRootType(genericType, defaultWriter.get());
4351
// Check that the determined root type is really assignable from the given entity.
4452
// A mismatch can happen, if a ServerResponseFilter replaces the response entity with another object
4553
// that does not match the original signature of the method (see HalServerResponseFilter for an example)
@@ -50,7 +58,7 @@ private ObjectWriter getWriter(Type genericType, Object value) {
5058
writer = genericWriters.computeIfAbsent(rootType, new Function<>() {
5159
@Override
5260
public ObjectWriter apply(JavaType type) {
53-
return defaultWriter.forType(type);
61+
return defaultWriter.get().forType(type);
5462
}
5563
});
5664
}
@@ -59,7 +67,7 @@ public ObjectWriter apply(JavaType type) {
5967
}
6068

6169
// no generic type given, or the generic type is just a class. Use the default writer.
62-
return this.defaultWriter;
70+
return this.defaultWriter.get();
6371
}
6472

6573
@Override

extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.function.BiFunction;
1313
import java.util.function.Function;
1414

15+
import jakarta.enterprise.inject.Instance;
1516
import jakarta.inject.Inject;
1617
import jakarta.ws.rs.WebApplicationException;
1718
import jakarta.ws.rs.core.MediaType;
@@ -22,7 +23,6 @@
2223

2324
import org.jboss.resteasy.reactive.common.util.StreamUtil;
2425
import org.jboss.resteasy.reactive.server.core.CurrentRequestManager;
25-
import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader;
2626
import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo;
2727
import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader;
2828
import org.jboss.resteasy.reactive.server.spi.ServerRequestContext;
@@ -36,18 +36,18 @@
3636

3737
import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder;
3838

39-
public class FullyFeaturedServerJacksonMessageBodyReader extends JacksonBasicMessageBodyReader
39+
public class FullyFeaturedServerJacksonMessageBodyReader extends AbstractServerJacksonMessageBodyReader
4040
implements ServerMessageBodyReader<Object> {
4141

42-
private final ObjectMapper originalMapper;
42+
private final Instance<ObjectMapper> originalMapper;
4343
private final Providers providers;
4444
private final ConcurrentMap<String, ObjectReader> perMethodReader = new ConcurrentHashMap<>();
4545
private final ConcurrentMap<String, ObjectReader> perTypeReader = new ConcurrentHashMap<>();
4646
private final ConcurrentMap<Class<?>, ObjectMapper> contextResolverMap = new ConcurrentHashMap<>();
4747
private final ConcurrentMap<ObjectMapper, ObjectReader> objectReaderMap = new ConcurrentHashMap<>();
4848

4949
@Inject
50-
public FullyFeaturedServerJacksonMessageBodyReader(ObjectMapper mapper, Providers providers) {
50+
public FullyFeaturedServerJacksonMessageBodyReader(Instance<ObjectMapper> mapper, Providers providers) {
5151
super(mapper);
5252
this.originalMapper = mapper;
5353
this.providers = providers;
@@ -152,7 +152,7 @@ private ObjectReader getObjectReaderFromAnnotations(ResteasyReactiveResourceInfo
152152

153153
private ObjectReader getEffectiveReader(Class<Object> type, Type genericType, MediaType responseMediaType) {
154154
ObjectMapper effectiveMapper = getEffectiveMapper(type, responseMediaType);
155-
ObjectReader effectiveReader = defaultReader;
155+
ObjectReader effectiveReader = defaultReader.get();
156156
if (effectiveMapper != originalMapper) {
157157
// Effective reader based on the context
158158
effectiveReader = objectReaderMap.computeIfAbsent(effectiveMapper, new Function<>() {
@@ -192,7 +192,7 @@ public ObjectReader apply(ObjectMapper objectMapper) {
192192

193193
private ObjectMapper getEffectiveMapper(Class<Object> type, MediaType responseMediaType) {
194194
if (providers == null) {
195-
return originalMapper;
195+
return originalMapper.get();
196196
}
197197

198198
ContextResolver<ObjectMapper> contextResolver = providers.getContextResolver(ObjectMapper.class,
@@ -214,7 +214,7 @@ public ObjectMapper apply(Class<?> aClass) {
214214
}
215215
}
216216

217-
return originalMapper;
217+
return originalMapper.get();
218218
}
219219

220220
private static class MethodObjectReaderFunction implements Function<String, ObjectReader> {

extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/ServerJacksonMessageBodyReader.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import java.lang.annotation.Annotation;
66
import java.lang.reflect.Type;
77

8+
import jakarta.enterprise.inject.Instance;
89
import jakarta.inject.Inject;
910
import jakarta.ws.rs.WebApplicationException;
1011
import jakarta.ws.rs.core.MediaType;
1112
import jakarta.ws.rs.core.MultivaluedMap;
1213
import jakarta.ws.rs.core.Response;
1314

1415
import org.jboss.resteasy.reactive.common.util.StreamUtil;
15-
import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader;
1616
import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo;
1717
import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader;
1818
import org.jboss.resteasy.reactive.server.spi.ServerRequestContext;
@@ -24,10 +24,11 @@
2424
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
2525
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
2626

27-
public class ServerJacksonMessageBodyReader extends JacksonBasicMessageBodyReader implements ServerMessageBodyReader<Object> {
27+
public class ServerJacksonMessageBodyReader extends AbstractServerJacksonMessageBodyReader
28+
implements ServerMessageBodyReader<Object> {
2829

2930
@Inject
30-
public ServerJacksonMessageBodyReader(ObjectMapper mapper) {
31+
public ServerJacksonMessageBodyReader(Instance<ObjectMapper> mapper) {
3132
super(mapper);
3233
}
3334

0 commit comments

Comments
 (0)