Skip to content

Commit

Permalink
Fail at build time when there are model conflicts in the JAXBContext.
Browse files Browse the repository at this point in the history
When having these two classes:

```java
package io.quarkus.resteasy.reactive.jaxb.deployment.test.one;

import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Model {
    private String name;
    // ...
}
```

And:
```java
package io.quarkus.resteasy.reactive.jaxb.deployment.test.two;

import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Model {
    private String name;
    // ...
}
```

If users use the `quarkus-jaxb` extension and they inject the JAXBContext, for example:

```java
@Inject
JAXBContext context;
```

This will fail at runtime because JAXB sees the Model classes as one because of the name. 

These changes address this issue by failing at build time, so users can properly address it by fixing the JAXB model. 

Moreover, users can now exclude any of these classes by using the new property `quarkus.jaxb.exclude-classes`, for example:

```
quarkus.jaxb.exclude-classes=io.quarkus.jaxb.deployment.two.Model
```

Plus, in resteasy reactive, if users exclude a class that is required to serialize/deserialize using JAXB, it will not cause any harm because internally it will recreate a cached JAXBContext including the excluded cache.
  • Loading branch information
Sgitario committed Feb 22, 2023
1 parent 394f1eb commit 46f348c
Show file tree
Hide file tree
Showing 21 changed files with 4,230 additions and 37 deletions.
9 changes: 9 additions & 0 deletions docs/src/main/asciidoc/resteasy-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,15 @@ Importing this module will allow HTTP message bodies to be read from XML
and serialised to XML, for <<resource-types,all the types not already registered with a more specific
serialisation>>.

The JAXB Resteasy Reactive extension will automatically detect the classes that are used in the resources and require JAXB serialization. Then, it will register these classes into the default `JAXBContext` which is internally used by the JAXB message reader and writer.

However, in some situations, these classes cause the `JAXBContext` to fail: for example, when you're using the same class name in different java packages. In these cases, the application will fail at build time and print the JAXB exception that caused the issue, so you can properly fix it. Alternatively, you can also exclude the classes that cause the issue by using the property `quarkus.jaxb.exclude-classes`. When excluding classes that are required by any resource, the JAXB resteasy reactive extension will create and cache a custom `JAXBContext` that will include the excluded class, causing a minimal performance degradance.

[NOTE]
====
The property `quarkus.jaxb.exclude-classes` accepts a comma separated list of fully qualified class names, for example: `quarkus.jaxb.exclude-classes=org.acme.one.Model,org.acme.two.Model`.
====

==== Advanced JAXB-specific features

When using the `quarkus-resteasy-reactive-jaxb` extension there are some advanced features that RESTEasy Reactive supports.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package io.quarkus.jaxb.deployment;

import static io.quarkus.jaxb.deployment.utils.JaxbType.isValidType;

import java.io.IOError;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.annotation.XmlAccessOrder;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlAnyAttribute;
Expand Down Expand Up @@ -49,6 +55,7 @@
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.deployment.ApplicationArchive;
Expand All @@ -66,10 +73,13 @@
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.jaxb.runtime.JaxbConfig;
import io.quarkus.jaxb.runtime.JaxbContextConfigRecorder;
import io.quarkus.jaxb.runtime.JaxbContextProducer;

class JaxbProcessor {
public class JaxbProcessor {

private static Logger LOG = Logger.getLogger(JaxbProcessor.class);

private static final List<Class<? extends Annotation>> JAXB_ANNOTATIONS = List.of(
XmlAccessorType.class,
Expand Down Expand Up @@ -288,11 +298,25 @@ void registerClasses(

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void setupJaxbContextConfig(List<JaxbClassesToBeBoundBuildItem> classesToBeBoundBuildItems,
void setupJaxbContextConfig(JaxbConfig config,
List<JaxbClassesToBeBoundBuildItem> classesToBeBoundBuildItems,
JaxbContextConfigRecorder jaxbContextConfig) {
Set<String> classNamesToBeBound = new HashSet<>();
for (JaxbClassesToBeBoundBuildItem classesToBeBoundBuildItem : classesToBeBoundBuildItems) {
jaxbContextConfig.addClassesToBeBound(classesToBeBoundBuildItem.getClasses());
classNamesToBeBound.addAll(classesToBeBoundBuildItem.getClasses());
}

// remove classes that have been excluded by users
if (config.excludeClasses.isPresent()) {
classNamesToBeBound.removeAll(config.excludeClasses.get());
}

// parse class names to class
Set<Class<?>> classes = getAllClassesFromClassNames(classNamesToBeBound);
// validate the context to fail at build time if it's not valid
validateContext(classes);
// register the classes to be used at runtime
jaxbContextConfig.addClassesToBeBound(classes);
}

@BuildStep
Expand Down Expand Up @@ -360,4 +384,32 @@ private void addReflectiveClass(BuildProducer<ReflectiveClassBuildItem> reflecti
private void addResourceBundle(BuildProducer<NativeImageResourceBundleBuildItem> resourceBundle, String bundle) {
resourceBundle.produce(new NativeImageResourceBundleBuildItem(bundle));
}

private void validateContext(Set<Class<?>> classes) {
try {
JAXBContext.newInstance(classes.toArray(new Class[0]));
} catch (JAXBException e) {
throw new IllegalStateException("Failed to configure JAXB context", e);
}
}

private Set<Class<?>> getAllClassesFromClassNames(Collection<String> classNames) {
Set<Class<?>> classes = new HashSet<>();
for (String className : classNames) {
Class<?> clazz = getClassByName(className);
if (isValidType(clazz)) {
classes.add(clazz);
}
}

return classes;
}

private Class<?> getClassByName(String name) {
try {
return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.quarkus.jaxb.deployment.utils;

import java.util.Locale;
import java.util.Set;

import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;

public class JaxbType {

private static final String DEFAULT_JAXB_ANNOTATION_VALUE = "##default";

private final String modelName;
private final Class<?> clazz;

public JaxbType(Class<?> clazz) {
this.modelName = findModelNameFromType(clazz);
this.clazz = clazz;
}

public String getModelName() {
return modelName;
}

public Class<?> getType() {
return clazz;
}

private String findModelNameFromType(Class<?> clazz) {
String nameFromAnnotation = DEFAULT_JAXB_ANNOTATION_VALUE;
String namespaceFromAnnotation = DEFAULT_JAXB_ANNOTATION_VALUE;
XmlType xmlType = clazz.getAnnotation(XmlType.class);
if (xmlType != null) {
nameFromAnnotation = xmlType.name();
namespaceFromAnnotation = xmlType.namespace();
} else {
XmlRootElement rootElement = clazz.getAnnotation(XmlRootElement.class);
if (rootElement != null) {
nameFromAnnotation = rootElement.name();
namespaceFromAnnotation = rootElement.namespace();
}
}

String modelName = nameFromAnnotation;
if (DEFAULT_JAXB_ANNOTATION_VALUE.equals(nameFromAnnotation)) {
modelName = clazz.getSimpleName().toLowerCase(Locale.ROOT);
}

if (!DEFAULT_JAXB_ANNOTATION_VALUE.equals(namespaceFromAnnotation)) {
modelName += "." + namespaceFromAnnotation;
}

return modelName;
}

public static boolean isValidType(Class<?> clazz) {
return clazz != null && !clazz.isPrimitive() && !clazz.isArray();
}

public static JaxbType findExistingType(Set<JaxbType> dictionary, JaxbType jaxbType) {
for (JaxbType existing : dictionary) {
if (existing.modelName.equals(jaxbType.modelName)) {
return existing;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.jaxb.deployment.one;

import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Model {
private String name1;

public String getName1() {
return name1;
}

public void setName1(String name1) {
this.name1 = name1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.jaxb.deployment.two;

import jakarta.xml.bind.annotation.XmlRootElement;

/**
* This class would make the JAXBContext to fail because there is an existing class with same name and model in the
* package `one`.
* To address this failure, we are excluding this class using the property `quarkus.jaxb.exclude-classes` in the
* `application.properties` file.
*/
@XmlRootElement
public class Model {
private String name2;

public String getName2() {
return name2;
}

public void setName2(String name2) {
this.name2 = name2;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
quarkus.jaxb.exclude-classes=io.quarkus.jaxb.deployment.two.Model
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.jaxb.runtime;

import java.util.List;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED, name = "jaxb")
public class JaxbConfig {
/**
* Exclude classes to automatically be bound to the default JAXB context.
*/
@ConfigItem
public Optional<List<String>> excludeClasses;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
package io.quarkus.jaxb.runtime;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import io.quarkus.runtime.annotations.Recorder;

@Recorder
public class JaxbContextConfigRecorder {
private volatile static Set<String> classesToBeBound = new HashSet<>();
private volatile static Set<Class<?>> classesToBeBound = new HashSet<>();

public void addClassesToBeBound(Collection<String> additionalClassesToBeBound) {
this.classesToBeBound.addAll(additionalClassesToBeBound);
public void addClassesToBeBound(Collection<Class<?>> classes) {
this.classesToBeBound.addAll(classes);
}

public static String[] getClassesToBeBound() {
return classesToBeBound.toArray(new String[0]);
public static Set<Class<?>> getClassesToBeBound() {
return Collections.unmodifiableSet(classesToBeBound);
}

public static boolean isClassBound(Class<?> clazz) {
return classesToBeBound.contains(clazz.getName());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.jaxb.runtime;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -20,29 +21,12 @@

@ApplicationScoped
public class JaxbContextProducer {

@DefaultBean
@Singleton
@Produces
public JAXBContext jaxbContext(Instance<JaxbContextCustomizer> customizers) {
try {
Map<String, Object> properties = new HashMap<>();
List<JaxbContextCustomizer> sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers);
for (JaxbContextCustomizer customizer : sortedCustomizers) {
customizer.customizeContextProperties(properties);
}

String[] classNamesToBeBounded = JaxbContextConfigRecorder.getClassesToBeBound();
List<Class<?>> classes = new ArrayList<>();
for (int i = 0; i < classNamesToBeBounded.length; i++) {
Class<?> clazz = getClassByName(classNamesToBeBounded[i]);
if (!clazz.isPrimitive()) {
classes.add(clazz);
}
}
return JAXBContext.newInstance(classes.toArray(new Class[0]), properties);
} catch (JAXBException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
return createJAXBContext(customizers);
}

@DefaultBean
Expand Down Expand Up @@ -79,6 +63,24 @@ public Unmarshaller unmarshaller(JAXBContext jaxbContext, Instance<JaxbContextCu
}
}

public JAXBContext createJAXBContext(Instance<JaxbContextCustomizer> customizers, Class... extraClasses) {
try {
Map<String, Object> properties = new HashMap<>();
List<JaxbContextCustomizer> sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers);
for (JaxbContextCustomizer customizer : sortedCustomizers) {
customizer.customizeContextProperties(properties);
}

List<Class> classes = new ArrayList<>();
classes.addAll(Arrays.asList(extraClasses));
classes.addAll(JaxbContextConfigRecorder.getClassesToBeBound());

return JAXBContext.newInstance(classes.toArray(new Class[0]), properties);
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}

private List<JaxbContextCustomizer> sortCustomizersInDescendingPriorityOrder(Instance<JaxbContextCustomizer> customizers) {
List<JaxbContextCustomizer> sortedCustomizers = new ArrayList<>();
for (JaxbContextCustomizer customizer : customizers) {
Expand All @@ -87,8 +89,4 @@ private List<JaxbContextCustomizer> sortCustomizersInDescendingPriorityOrder(Ins
Collections.sort(sortedCustomizers);
return sortedCustomizers;
}

private Class<?> getClassByName(String name) throws ClassNotFoundException {
return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
}
}
Loading

0 comments on commit 46f348c

Please sign in to comment.