-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allows customizing the ObjectMapper in REST Client Reactive Jackson
The REST Client Reactive supports adding a custom ObjectMapper to be used only the Client using the annotation `@ClientObjectMapper`. A simple example is to provide a custom ObjectMapper to the REST Client Reactive Jackson extension by doing: ```java @path("/extensions") @RegisterRestClient public interface ExtensionsService { @get Set<Extension> getById(@QueryParam("id") String id); @ClientObjectMapper <1> static ObjectMapper objectMapper(ObjectMapper defaultObjectMapper) { <2> return defaultObjectMapper.copy() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(DeserializationFeature.UNWRAP_ROOT_VALUE); } } ``` <1> The method must be annotated with `@ClientObjectMapper`. <2> It's must be a static method. Also, the parameter `defaultObjectMapper` will be resolved via CDI. If not found, it will throw an exception at runtime. Fix #23979
- Loading branch information
Showing
8 changed files
with
330 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
...son/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/ClientObjectMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package io.quarkus.rest.client.reactive.jackson; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* Used to easily define a custom object mapper for the specific REST Client on which it's used. | ||
* | ||
* The annotation MUST be placed on a method of the REST Client interface that meets the following criteria: | ||
* <ul> | ||
* <li>Is a {@code static} method</li> | ||
* </ul> | ||
* | ||
* An example method could look like the following: | ||
* | ||
* <pre> | ||
* {@code | ||
* @ClientObjectMapper | ||
* static ObjectMapper objectMapper() { | ||
* return new ObjectMapper(); | ||
* } | ||
* | ||
* } | ||
* </pre> | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
public @interface ClientObjectMapper { | ||
} |
27 changes: 27 additions & 0 deletions
27
...arkus/rest/client/reactive/deployment/AnnotationToRegisterIntoClientContextBuildItem.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package io.quarkus.rest.client.reactive.deployment; | ||
|
||
import org.jboss.jandex.DotName; | ||
|
||
import io.quarkus.builder.item.MultiBuildItem; | ||
|
||
/** | ||
* A Build Item that is used to register annotations that are used by the client to register services into the client context. | ||
*/ | ||
public final class AnnotationToRegisterIntoClientContextBuildItem extends MultiBuildItem { | ||
|
||
private final DotName annotation; | ||
private final Class<?> expectedReturnType; | ||
|
||
public AnnotationToRegisterIntoClientContextBuildItem(DotName annotation, Class<?> expectedReturnType) { | ||
this.annotation = annotation; | ||
this.expectedReturnType = expectedReturnType; | ||
} | ||
|
||
public DotName getAnnotation() { | ||
return annotation; | ||
} | ||
|
||
public Class<?> getExpectedReturnType() { | ||
return expectedReturnType; | ||
} | ||
} |
180 changes: 180 additions & 0 deletions
180
...rc/main/java/io/quarkus/rest/client/reactive/deployment/ClientContextResolverHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
package io.quarkus.rest.client.reactive.deployment; | ||
|
||
import java.lang.annotation.Annotation; | ||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Modifier; | ||
import java.util.LinkedHashMap; | ||
|
||
import jakarta.ws.rs.Priorities; | ||
|
||
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.MethodInfo; | ||
import org.jboss.jandex.Type; | ||
import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; | ||
|
||
import io.quarkus.arc.Arc; | ||
import io.quarkus.arc.ArcContainer; | ||
import io.quarkus.arc.InstanceHandle; | ||
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.gizmo.SignatureBuilder; | ||
import io.quarkus.rest.client.reactive.runtime.ResteasyReactiveContextResolver; | ||
import io.quarkus.runtime.util.HashUtil; | ||
|
||
/** | ||
* Generates an implementation of {@link ResteasyReactiveContextResolver} | ||
* from an instance of {@link jakarta.ws.rs.ext.Provider} | ||
*/ | ||
class ClientContextResolverHandler { | ||
|
||
private static final String[] EMPTY_STRING_ARRAY = new String[0]; | ||
private static final ResultHandle[] EMPTY_RESULT_HANDLES_ARRAY = new ResultHandle[0]; | ||
private static final MethodDescriptor GET_INVOKED_METHOD = | ||
MethodDescriptor.ofMethod(RestClientRequestContext.class, "getInvokedMethod", Method.class); | ||
|
||
private final DotName annotation; | ||
private final Class<?> expectedReturnType; | ||
private final ClassOutput classOutput; | ||
|
||
ClientContextResolverHandler(DotName annotation, Class<?> expectedReturnType, ClassOutput classOutput) { | ||
this.annotation = annotation; | ||
this.expectedReturnType = expectedReturnType; | ||
this.classOutput = classOutput; | ||
} | ||
|
||
/** | ||
* Generates an implementation of {@link ResteasyReactiveContextResolver} that looks something like: | ||
* | ||
* <pre> | ||
* {@code | ||
* public class SomeService_map_ContextResolver_a8fb70beeef2a54b80151484d109618eed381626 | ||
* implements ResteasyReactiveContextResolver<T> { | ||
* | ||
* public T getContext(Class<?> type) { | ||
* // simply call the static method of interface | ||
* return SomeService.map(var1); | ||
* } | ||
* | ||
* } | ||
* </pre> | ||
*/ | ||
GeneratedClassResult generateContextResolver(AnnotationInstance instance) { | ||
if (!annotation.equals(instance.name())) { | ||
throw new IllegalArgumentException( | ||
"'clientContextResolverInstance' must be an instance of " + annotation); | ||
} | ||
MethodInfo targetMethod = findTargetMethod(instance); | ||
if (targetMethod == null) { | ||
return null; | ||
} | ||
|
||
int priority = Priorities.USER; | ||
AnnotationValue priorityAnnotationValue = instance.value("priority"); | ||
if (priorityAnnotationValue != null) { | ||
priority = priorityAnnotationValue.asInt(); | ||
} | ||
|
||
Class<?> returnTypeClassName = lookupReturnClass(targetMethod); | ||
if (!expectedReturnType.isAssignableFrom(returnTypeClassName)) { | ||
throw new IllegalStateException(annotation | ||
+ " is only supported on static methods of REST Client interfaces that return '" + expectedReturnType + "'." | ||
+ " Offending instance is '" + targetMethod.declaringClass().name().toString() + "#" | ||
+ targetMethod.name() + "'"); | ||
} | ||
|
||
ClassInfo restClientInterfaceClassInfo = targetMethod.declaringClass(); | ||
String generatedClassName = getGeneratedClassName(targetMethod); | ||
try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput).className(generatedClassName) | ||
.signature(SignatureBuilder.forClass().addInterface(io.quarkus.gizmo.Type.parameterizedType(io.quarkus.gizmo.Type.classType(ResteasyReactiveContextResolver.class), io.quarkus.gizmo.Type.classType(returnTypeClassName)))) | ||
.build()) { | ||
MethodCreator getContext = cc.getMethodCreator("getContext", Object.class, Class.class); | ||
LinkedHashMap<String, ResultHandle> targetMethodParams = new LinkedHashMap<>(); | ||
for (Type paramType : targetMethod.parameterTypes()) { | ||
ResultHandle targetMethodParamHandle; | ||
if (paramType.name().equals(DotNames.METHOD)) { | ||
targetMethodParamHandle = getContext.invokeVirtualMethod(GET_INVOKED_METHOD, getContext.getMethodParam(1)); | ||
} else { | ||
targetMethodParamHandle = getFromCDI(getContext, targetMethod.returnType().name().toString()); | ||
} | ||
|
||
targetMethodParams.put(paramType.name().toString(), targetMethodParamHandle); | ||
} | ||
|
||
ResultHandle resultHandle = getContext.invokeStaticInterfaceMethod( | ||
MethodDescriptor.ofMethod( | ||
restClientInterfaceClassInfo.name().toString(), | ||
targetMethod.name(), | ||
targetMethod.returnType().name().toString(), | ||
targetMethodParams.keySet().toArray(EMPTY_STRING_ARRAY)), | ||
targetMethodParams.values().toArray(EMPTY_RESULT_HANDLES_ARRAY)); | ||
getContext.returnValue(resultHandle); | ||
} | ||
|
||
return new GeneratedClassResult(restClientInterfaceClassInfo.name().toString(), generatedClassName, priority); | ||
} | ||
|
||
private MethodInfo findTargetMethod(AnnotationInstance instance) { | ||
MethodInfo targetMethod = null; | ||
if (instance.target().kind() == AnnotationTarget.Kind.METHOD) { | ||
targetMethod = instance.target().asMethod(); | ||
if (ignoreAnnotation(targetMethod)) { | ||
return null; | ||
} | ||
if ((targetMethod.flags() & Modifier.STATIC) != 0) { | ||
if (targetMethod.returnType().kind() == Type.Kind.VOID) { | ||
throw new IllegalStateException(annotation | ||
+ " is only supported on static methods of REST Client interfaces that return an object." | ||
+ " Offending instance is '" + targetMethod.declaringClass().name().toString() + "#" | ||
+ targetMethod.name() + "'"); | ||
} | ||
|
||
|
||
} | ||
} | ||
|
||
return targetMethod; | ||
} | ||
|
||
private static Class<?> lookupReturnClass(MethodInfo targetMethod) { | ||
Class<?> returnTypeClassName = null; | ||
try { | ||
returnTypeClassName = Class.forName(targetMethod.returnType().name().toString(), false, Thread.currentThread().getContextClassLoader()); | ||
} catch (ClassNotFoundException ignored) { | ||
|
||
} | ||
return returnTypeClassName; | ||
} | ||
|
||
private static ResultHandle getFromCDI(MethodCreator getContext, String className) { | ||
ResultHandle containerHandle = getContext | ||
.invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); | ||
ResultHandle instanceHandle = getContext.invokeInterfaceMethod(MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), | ||
containerHandle, getContext.loadClassFromTCCL(className), | ||
getContext.newArray(Annotation.class, 0)); | ||
return getContext.invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); | ||
} | ||
|
||
public static String getGeneratedClassName(MethodInfo methodInfo) { | ||
StringBuilder sigBuilder = new StringBuilder(); | ||
sigBuilder.append(methodInfo.name()).append("_").append(methodInfo.returnType().name().toString()); | ||
for (Type i : methodInfo.parameterTypes()) { | ||
sigBuilder.append(i.name().toString()); | ||
} | ||
|
||
return methodInfo.declaringClass().name().toString() + "_" + methodInfo.name() + "_" | ||
+ "ContextResolver" + "_" + HashUtil.sha1(sigBuilder.toString()); | ||
} | ||
|
||
private static boolean ignoreAnnotation(MethodInfo methodInfo) { | ||
// ignore the annotation if it's placed on a Kotlin companion class | ||
// this is not a problem since the Kotlin compiler will also place the annotation the static method interface method | ||
return methodInfo.declaringClass().name().toString().contains("$Companion"); | ||
} | ||
} |
Oops, something went wrong.