-
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 providing contextual objects per client in REST Client Reactive
The REST Client Reactive supports adding contextual beans to the Client using the annotation `@ClientProvider` that will be available as part of the `jakarta.ws.rs.ext.Providers` interface in filters/handlers/message readers and writers. 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); @ClientProvider <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 `@ClientProvider`. <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. Now, we can resolve the object we provided using the `jakarta.ws.rs.ext.Providers` interface, for example: ```java @Provider public class TestClientRequestFilter implements ResteasyReactiveClientRequestFilter { @OverRide public void filter(ResteasyReactiveClientRequestFilter requestContext) { Providers providers = requestContext.getProviders(); ObjectMapper objectMapper = providers.getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON).getContext(null); // ... } } ``` NOTE: When providing a custom ObjectMapper, the REST Client Reactive Jackson extension will automatically check the client context to use it. Fix #23979
- Loading branch information
Showing
7 changed files
with
299 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
167 changes: 167 additions & 0 deletions
167
...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,167 @@ | ||
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.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 ClassOutput classOutput; | ||
|
||
ClientContextResolverHandler(ClassOutput classOutput) { | ||
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 (!DotNames.CLIENT_PROVIDER.equals(instance.name())) { | ||
throw new IllegalArgumentException( | ||
"'clientExceptionMapperInstance' must be an instance of " + DotNames.CLIENT_PROVIDER); | ||
} | ||
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); | ||
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 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 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(DotNames.CLIENT_PROVIDER | ||
+ " 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 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"); | ||
} | ||
} |
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
38 changes: 38 additions & 0 deletions
38
...client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/ClientProvider.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,38 @@ | ||
package io.quarkus.rest.client.reactive; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
import jakarta.ws.rs.Priorities; | ||
|
||
/** | ||
* Used to easily define a custom provider 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 | ||
* @ClientProvider | ||
* static Bean context() { | ||
* return ... | ||
* } | ||
* | ||
* } | ||
* </pre> | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
public @interface ClientProvider { | ||
|
||
/** | ||
* The priority with which the exception mapper will be executed | ||
*/ | ||
int priority() default Priorities.USER; | ||
} |
Oops, something went wrong.