forked from quarkusio/quarkus
-
Notifications
You must be signed in to change notification settings - Fork 0
/
JacksonProcessor.java
398 lines (344 loc) · 20.9 KB
/
JacksonProcessor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
package io.quarkus.jackson.deployment;
import static org.jboss.jandex.AnnotationTarget.Kind.CLASS;
import static org.jboss.jandex.AnnotationTarget.Kind.METHOD;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Type;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.SimpleObjectIdResolver;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.jackson.JacksonMixin;
import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.jackson.runtime.MixinsRecorder;
import io.quarkus.jackson.runtime.ObjectMapperProducer;
import io.quarkus.jackson.spi.ClassPathJacksonModuleBuildItem;
import io.quarkus.jackson.spi.JacksonModuleBuildItem;
public class JacksonProcessor {
private static final DotName JSON_DESERIALIZE = DotName.createSimple(JsonDeserialize.class.getName());
private static final DotName JSON_SERIALIZE = DotName.createSimple(JsonSerialize.class.getName());
private static final DotName JSON_AUTO_DETECT = DotName.createSimple(JsonAutoDetect.class.getName());
private static final DotName JSON_TYPE_ID_RESOLVER = DotName.createSimple(JsonTypeIdResolver.class.getName());
private static final DotName JSON_CREATOR = DotName.createSimple("com.fasterxml.jackson.annotation.JsonCreator");
private static final DotName JSON_NAMING = DotName.createSimple("com.fasterxml.jackson.databind.annotation.JsonNaming");
private static final DotName JSON_IDENTITY_INFO = DotName.createSimple("com.fasterxml.jackson.annotation.JsonIdentityInfo");
private static final DotName BUILDER_VOID = DotName.createSimple(Void.class.getName());
private static final String TIME_MODULE = "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule";
private static final String JDK8_MODULE = "com.fasterxml.jackson.datatype.jdk8.Jdk8Module";
private static final String PARAMETER_NAMES_MODULE = "com.fasterxml.jackson.module.paramnames.ParameterNamesModule";
private static final DotName JACKSON_MIXIN = DotName.createSimple(JacksonMixin.class.getName());
// this list can probably be enriched with more modules
private static final List<String> MODULES_NAMES_TO_AUTO_REGISTER = Arrays.asList(TIME_MODULE, JDK8_MODULE,
PARAMETER_NAMES_MODULE);
@Inject
CombinedIndexBuildItem combinedIndexBuildItem;
@Inject
List<IgnoreJsonDeserializeClassBuildItem> ignoreJsonDeserializeClassBuildItems;
@BuildStep
void unremovable(Capabilities capabilities, BuildProducer<UnremovableBeanBuildItem> producer) {
if (capabilities.isPresent(Capability.VERTX_CORE)) {
producer.produce(UnremovableBeanBuildItem.beanTypes(ObjectMapper.class));
}
}
@BuildStep
void register(
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchyClass,
BuildProducer<ReflectiveMethodBuildItem> reflectiveMethod,
BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
reflectiveClass.produce(
new ReflectiveClassBuildItem(true, false, "com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector",
"com.fasterxml.jackson.databind.ser.std.SqlDateSerializer",
"com.fasterxml.jackson.databind.ser.std.SqlTimeSerializer",
"com.fasterxml.jackson.databind.deser.std.DateDeserializers$SqlDateDeserializer",
"com.fasterxml.jackson.databind.deser.std.DateDeserializers$TimestampDeserializer",
"com.fasterxml.jackson.annotation.SimpleObjectIdResolver"));
IndexView index = combinedIndexBuildItem.getIndex();
// TODO: @JsonDeserialize is only supported as a class annotation - we should support the others as well
Set<DotName> ignoredDotNames = new HashSet<>();
for (IgnoreJsonDeserializeClassBuildItem ignoreJsonDeserializeClassBuildItem : ignoreJsonDeserializeClassBuildItems) {
ignoredDotNames.add(ignoreJsonDeserializeClassBuildItem.getDotName());
}
// handle the various @JsonDeserialize cases
for (AnnotationInstance deserializeInstance : index.getAnnotations(JSON_DESERIALIZE)) {
AnnotationTarget annotationTarget = deserializeInstance.target();
if (CLASS.equals(annotationTarget.kind())) {
DotName dotName = annotationTarget.asClass().name();
if (!ignoredDotNames.contains(dotName)) {
addReflectiveHierarchyClass(dotName, reflectiveHierarchyClass);
}
AnnotationValue annotationValue = deserializeInstance.value("builder");
if (null != annotationValue && AnnotationValue.Kind.CLASS.equals(annotationValue.kind())) {
DotName builderClassName = annotationValue.asClass().name();
if (!BUILDER_VOID.equals(builderClassName)) {
addReflectiveHierarchyClass(builderClassName, reflectiveHierarchyClass);
}
}
}
AnnotationValue usingValue = deserializeInstance.value("using");
if (usingValue != null) {
// the Deserializers are constructed internally by Jackson using a no-args constructor
reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, usingValue.asClass().name().toString()));
}
AnnotationValue keyUsingValue = deserializeInstance.value("keyUsing");
if (keyUsingValue != null) {
// the Deserializers are constructed internally by Jackson using a no-args constructor
reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, keyUsingValue.asClass().name().toString()));
}
AnnotationValue contentUsingValue = deserializeInstance.value("contentUsing");
if (contentUsingValue != null) {
// the Deserializers are constructed internally by Jackson using a no-args constructor
reflectiveClass
.produce(new ReflectiveClassBuildItem(false, false, contentUsingValue.asClass().name().toString()));
}
}
// handle the various @JsonSerialize cases
for (AnnotationInstance serializeInstance : index.getAnnotations(JSON_SERIALIZE)) {
AnnotationValue usingValue = serializeInstance.value("using");
if (usingValue != null) {
// the Serializers are constructed internally by Jackson using a no-args constructor
reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, usingValue.asClass().name().toString()));
}
AnnotationValue keyUsingValue = serializeInstance.value("keyUsing");
if (keyUsingValue != null) {
// the Deserializers are constructed internally by Jackson using a no-args constructor
reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, keyUsingValue.asClass().name().toString()));
}
AnnotationValue contentUsingValue = serializeInstance.value("contentUsing");
if (contentUsingValue != null) {
// the Deserializers are constructed internally by Jackson using a no-args constructor
reflectiveClass
.produce(new ReflectiveClassBuildItem(false, false, contentUsingValue.asClass().name().toString()));
}
AnnotationValue nullsUsingValue = serializeInstance.value("nullsUsing");
if (nullsUsingValue != null) {
// the Deserializers are constructed internally by Jackson using a no-args constructor
reflectiveClass
.produce(new ReflectiveClassBuildItem(false, false, nullsUsingValue.asClass().name().toString()));
}
}
for (AnnotationInstance creatorInstance : index.getAnnotations(JSON_AUTO_DETECT)) {
if (creatorInstance.target().kind() == CLASS) {
reflectiveClass
.produce(
new ReflectiveClassBuildItem(true, true, creatorInstance.target().asClass().name().toString()));
}
}
// Register @JsonTypeIdResolver implementations for reflection.
// Note: @JsonTypeIdResolver is, simply speaking, the "dynamic version" of @JsonSubTypes, i.e. sub-types are
// dynamically identified by Jackson's `TypeIdResolver.typeFromId()`, which returns sub-types of the annotated
// class. Means: the referenced `TypeIdResolver` _and_ all sub-types of the annotated class must be registered
// for reflection.
for (AnnotationInstance resolverInstance : index.getAnnotations(JSON_TYPE_ID_RESOLVER)) {
AnnotationValue value = resolverInstance.value("value");
if (value != null) {
// Add the type-id-resolver class
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, value.asClass().name().toString()));
// Add the whole hierarchy of the annotated class
addReflectiveHierarchyClass(resolverInstance.target().asClass().name(), reflectiveHierarchyClass);
}
}
// make sure we register the constructors and methods marked with @JsonCreator for reflection
for (AnnotationInstance creatorInstance : index.getAnnotations(JSON_CREATOR)) {
if (METHOD == creatorInstance.target().kind()) {
reflectiveMethod.produce(new ReflectiveMethodBuildItem(creatorInstance.target().asMethod()));
}
}
// register @JsonNaming strategy implementations for reflection
for (AnnotationInstance jsonNamingInstance : index.getAnnotations(JSON_NAMING)) {
AnnotationValue strategyValue = jsonNamingInstance.value("value");
if (strategyValue != null) {
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, strategyValue.asClass().name().toString()));
}
}
// register @JsonIdentityInfo strategy implementations for reflection
for (AnnotationInstance jsonIdentityInfoInstance : index.getAnnotations(JSON_IDENTITY_INFO)) {
AnnotationValue generatorValue = jsonIdentityInfoInstance.value("generator");
AnnotationValue resolverValue = jsonIdentityInfoInstance.value("resolver");
if (generatorValue != null) {
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, generatorValue.asClass().name().toString()));
}
if (resolverValue != null) {
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, resolverValue.asClass().name().toString()));
} else {
// Registering since SimpleObjectIdResolver is the default value of @JsonIdentityInfo.resolver
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, SimpleObjectIdResolver.class));
}
}
// this needs to be registered manually since the runtime module is not indexed by Jandex
additionalBeans.produce(new AdditionalBeanBuildItem(ObjectMapperProducer.class));
}
private void addReflectiveHierarchyClass(DotName className,
BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchyClass) {
Type jandexType = Type.create(className, Type.Kind.CLASS);
reflectiveHierarchyClass.produce(new ReflectiveHierarchyBuildItem.Builder()
.type(jandexType)
.source(getClass().getSimpleName() + " > " + jandexType.name().toString())
.build());
}
@BuildStep
void autoRegisterModules(BuildProducer<ClassPathJacksonModuleBuildItem> classPathJacksonModules) {
for (String module : MODULES_NAMES_TO_AUTO_REGISTER) {
registerModuleIfOnClassPath(module, classPathJacksonModules);
}
}
private void registerModuleIfOnClassPath(String moduleClassName,
BuildProducer<ClassPathJacksonModuleBuildItem> classPathJacksonModules) {
if (QuarkusClassLoader.isClassPresentAtRuntime(moduleClassName)) {
classPathJacksonModules.produce(new ClassPathJacksonModuleBuildItem(moduleClassName));
}
}
// Generate a ObjectMapperCustomizer bean that registers each serializer / deserializer as well as detected modules with the ObjectMapper
@BuildStep
void generateCustomizer(BuildProducer<GeneratedBeanBuildItem> generatedBeans,
List<JacksonModuleBuildItem> jacksonModules, List<ClassPathJacksonModuleBuildItem> classPathJacksonModules) {
if (jacksonModules.isEmpty() && classPathJacksonModules.isEmpty()) {
return;
}
ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(generatedBeans);
try (ClassCreator classCreator = ClassCreator.builder().classOutput(classOutput)
.className("io.quarkus.jackson.customizer.RegisterSerializersAndDeserializersCustomizer")
.interfaces(ObjectMapperCustomizer.class.getName())
.build()) {
classCreator.addAnnotation(Singleton.class);
try (MethodCreator customize = classCreator.getMethodCreator("customize", void.class, ObjectMapper.class)) {
ResultHandle objectMapper = customize.getMethodParam(0);
for (JacksonModuleBuildItem jacksonModule : jacksonModules) {
if (jacksonModule.getItems().isEmpty()) {
continue;
}
/*
* Create code similar to the following:
*
* SimpleModule module = new SimpleModule("somename");
* module.addSerializer(Foo.class, new FooSerializer());
* module.addDeserializer(Foo.class, new FooDeserializer());
* objectMapper.registerModule(module);
*/
ResultHandle module = customize.newInstance(
MethodDescriptor.ofConstructor(SimpleModule.class, String.class),
customize.load(jacksonModule.getName()));
for (JacksonModuleBuildItem.Item item : jacksonModule.getItems()) {
ResultHandle targetClass = customize.loadClassFromTCCL(item.getTargetClassName());
String serializerClassName = item.getSerializerClassName();
if (serializerClassName != null && !serializerClassName.isEmpty()) {
ResultHandle serializer = customize.newInstance(
MethodDescriptor.ofConstructor(serializerClassName));
customize.invokeVirtualMethod(
MethodDescriptor.ofMethod(SimpleModule.class, "addSerializer", SimpleModule.class,
Class.class, JsonSerializer.class),
module, targetClass, serializer);
}
String deserializerClassName = item.getDeserializerClassName();
if (deserializerClassName != null && !deserializerClassName.isEmpty()) {
ResultHandle deserializer = customize.newInstance(
MethodDescriptor.ofConstructor(deserializerClassName));
customize.invokeVirtualMethod(
MethodDescriptor.ofMethod(SimpleModule.class, "addDeserializer", SimpleModule.class,
Class.class, JsonDeserializer.class),
module, targetClass, deserializer);
}
}
customize.invokeVirtualMethod(
MethodDescriptor.ofMethod(ObjectMapper.class, "registerModule", ObjectMapper.class, Module.class),
objectMapper, module);
}
for (ClassPathJacksonModuleBuildItem classPathJacksonModule : classPathJacksonModules) {
ResultHandle module = customize
.newInstance(MethodDescriptor.ofConstructor(classPathJacksonModule.getModuleClassName()));
customize.invokeVirtualMethod(
MethodDescriptor.ofMethod(ObjectMapper.class, "registerModule", ObjectMapper.class, Module.class),
objectMapper, module);
}
customize.returnValue(null);
}
// ensure that the things we auto-register have the lower priority - this ensures that user registered modules take priority
try (MethodCreator priority = classCreator.getMethodCreator("priority", int.class)) {
priority.returnValue(priority.load(ObjectMapperCustomizer.QUARKUS_CUSTOMIZER_PRIORITY));
}
}
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
public void supportMixins(MixinsRecorder recorder,
CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
IndexView index = combinedIndexBuildItem.getIndex();
Collection<AnnotationInstance> jacksonMixins = index.getAnnotations(JACKSON_MIXIN);
if (jacksonMixins.isEmpty()) {
return;
}
Map<Class<?>, Class<?>> mixinsMap = new HashMap<>();
for (AnnotationInstance instance : jacksonMixins) {
if (instance.target().kind() != CLASS) {
continue;
}
ClassInfo mixinClassInfo = instance.target().asClass();
String mixinClassName = mixinClassInfo.name().toString();
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, mixinClassName));
try {
Type[] targetTypes = instance.value().asClassArray();
if ((targetTypes == null) || targetTypes.length == 0) {
continue;
}
Class<?> mixinClass = Thread.currentThread().getContextClassLoader().loadClass(mixinClassName);
for (Type targetType : targetTypes) {
String targetClassName = targetType.name().toString();
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, targetClassName));
mixinsMap.put(Thread.currentThread().getContextClassLoader().loadClass(targetClassName),
mixinClass);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to determine Jackson mixin usage at build", e);
}
}
if (mixinsMap.isEmpty()) {
return;
}
syntheticBeans.produce(SyntheticBeanBuildItem.configure(ObjectMapperCustomizer.class)
.scope(Singleton.class)
.supplier(recorder.customizerSupplier(mixinsMap))
.done());
}
}