Skip to content

Commit

Permalink
Merge pull request #4212 from geoand/jsonb-bean
Browse files Browse the repository at this point in the history
Sanitize and homogenize RESTEasy + Json handling
  • Loading branch information
geoand authored Oct 3, 2019
2 parents c36f8c4 + 749dd90 commit 87fc842
Show file tree
Hide file tree
Showing 29 changed files with 700 additions and 251 deletions.
10 changes: 10 additions & 0 deletions bom/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,21 @@
<artifactId>quarkus-jackson-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson-spi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb-spi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonp-deployment</artifactId>
Expand Down
56 changes: 44 additions & 12 deletions docs/src/main/asciidoc/rest-json-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -186,28 +186,60 @@ They are analyzed at build time and Quarkus restricts the number of JAX-RS provi
It allows to reduce the size of the native executable.
====

=== More on our Jackson support
=== Configuring JSON support

==== JSON-B

Quarkus makes it very easy to configure various JSON-B settings via CDI beans. The simplest (and suggested) approach is to define a CDI bean of type `io.quarkus.jsonb.JsonbConfigCustomizer`
inside of which any JSON-B configuration can be applied.

If for example a custom serializer named `FooSerializer` for type `com.example.Foo` needs to be registered with JSON-B, the addition of a bean like the following would suffice:

[source,java]
----
import io.quarkus.jsonb.JsonbConfigCustomizer;
import javax.inject.Singleton;
import javax.json.bind.JsonbConfig;
import javax.json.bind.serializer.JsonbSerializer;
@Singleton
public class FooSerializerRegistrationCustomizer implements JsonbConfigCustomizer {
public void customize(JsonbConfig config) {
config.withSerializers(new FooSerializer());
}
}
----

A more advanced option would be to directly provide a bean of `javax.json.bind.JsonbConfig` or in the extreme case to provide a bean of type `javax.json.bind.Jsonb`.
If the latter approach is leveraged it is very important to manually inject and apply all `io.quarkus.jsonb.JsonbConfigCustomizer` beans in the CDI producer that produces `javax.json.bind.Jsonb`.
Failure to do so will prevent JSON-B specific customizations provided by various extensions from being applied.

==== Jackson

As stated above, Quarkus provides the option of using https://github.com/FasterXML/jackson[Jackson] instead of JSON-B via the use of the `quarkus-resteasy-jackson` extension.

When using Jackson, Quarkus allows users to customize the `ObjectMapper` very easily - all that needs to be done is to configure a CDI producer bean like so:
Following the same approach as described in the previous section, Jackson's `ObjectMapper` can be configured using a `io.quarkus.jackson.ObjectMapperCustomizer` bean.
An example where a custom module needs to be registered would like so:

[source,java]
----
@ApplicationScoped
public class CustomObjectMapperConfig {
@Singleton
@Produces
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// perform configuration
return objectMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.jackson.ObjectMapperCustomizer;
import javax.inject.Singleton;
@Singleton
public class RegisterCustomModuleCustomizer implements ObjectMapperCustomizer {
public void customize(ObjectMapper mapper) {
mapper.registerModule(new CustomModule());
}
}
----

If no such bean is provided, a default `ObjectMapper` is used.
Users can even provide their own `ObjectMapper` bean if they so choose.
If this is done, it is very important to manually inject and apply all `io.quarkus.jackson.ObjectMapperCustomizer` beans in the CDI producer that produces `ObjectMapper`.
Failure to do so will prevent Jackson specific customizations provided by various extensions from being applied.

== Creating a frontend

Expand Down
8 changes: 8 additions & 0 deletions extensions/jackson/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson-spi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
Expand All @@ -14,13 +15,28 @@
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Type;

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.module.SimpleModule;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.substrate.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.substrate.ReflectiveHierarchyBuildItem;
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.ObjectMapperCustomizer;
import io.quarkus.jackson.ObjectMapperProducer;
import io.quarkus.jackson.spi.JacksonModuleBuildItem;

public class JacksonProcessor {

Expand All @@ -33,6 +49,9 @@ public class JacksonProcessor {
@Inject
BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchyClass;

@Inject
BuildProducer<AdditionalBeanBuildItem> additionalBeans;

@Inject
CombinedIndexBuildItem combinedIndexBuildItem;

Expand Down Expand Up @@ -71,6 +90,9 @@ void register() {
}
}
}

// 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) {
Expand All @@ -81,4 +103,80 @@ private void addReflectiveHierarchyClass(DotName className) {
private void addReflectiveClass(boolean methods, boolean fields, String... className) {
reflectiveClass.produce(new ReflectiveClassBuildItem(methods, fields, className));
}

// Generate a ObjectMapperCustomizer bean that registers each serializer / deserializer with ObjectMapper
@BuildStep
void generateCustomizer(BuildProducer<GeneratedBeanBuildItem> generatedBeans,
List<JacksonModuleBuildItem> jacksonModules) {

if (jacksonModules.isEmpty()) {
return;
}

ClassOutput classOutput = new ClassOutput() {
@Override
public void write(String name, byte[] data) {
generatedBeans.produce(new GeneratedBeanBuildItem(name, data));
}
};

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.loadClass(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);
}

customize.returnValue(null);
}
}
}
}
1 change: 1 addition & 0 deletions extensions/jackson/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<modules>
<module>deployment</module>
<module>runtime</module>
<module>spi</module>
</modules>

</project>
4 changes: 4 additions & 0 deletions extensions/jackson/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.jackson;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
* Meant to be implemented by a CDI bean that provides arbitrary customization for the default {@link ObjectMapper}.
* <p>
* All implementations (that are registered as CDI beans) are taken into account when producing the default
* {@link ObjectMapper}.
* <p>
* See also {@link ObjectMapperProducer#objectMapper}.
*/
public interface ObjectMapperCustomizer {

void customize(ObjectMapper objectMapper);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.jackson;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.Produces;
import javax.inject.Singleton;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.arc.DefaultBean;

@ApplicationScoped
public class ObjectMapperProducer {

@DefaultBean
@Singleton
@Produces
public ObjectMapper objectMapper(Instance<ObjectMapperCustomizer> customizers) {
ObjectMapper objectMapper = new ObjectMapper();
for (ObjectMapperCustomizer customizer : customizers) {
customizer.customize(objectMapper);
}
return objectMapper;
}
}
22 changes: 22 additions & 0 deletions extensions/jackson/spi/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-jackson-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>quarkus-jackson-spi</artifactId>
<name>Quarkus - Jackson - SPI</name>
<description>Artifact that provides BuildItems specific to Jackson</description>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
</dependencies>
</project>
Loading

0 comments on commit 87fc842

Please sign in to comment.