Skip to content

Commit

Permalink
Support blocking exception mappers in REST Client Reactive
Browse files Browse the repository at this point in the history
Fix #30312
  • Loading branch information
Sgitario committed Apr 20, 2023
1 parent f842487 commit 55a49cc
Show file tree
Hide file tree
Showing 12 changed files with 557 additions and 21 deletions.
62 changes: 62 additions & 0 deletions docs/src/main/asciidoc/rest-client-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,68 @@ Naturally this handling is per REST Client. `@ClientExceptionMapper` uses the de

NOTE: Methods annotated with `@ClientExceptionMapper` can also take a `java.lang.reflect.Method` parameter which is useful if the exception mapping code needs to know the REST Client method that was invoked and caused the exception mapping code to engage.

=== Using @Blocking annotation in exception mappers

In cases that warrant using `InputStream` as the return type of REST Client method (such as when large amounts of data need to be read):

[source, java]
----
@Path("/echo")
@RegisterRestClient
public interface EchoClient {
@GET
InputStream get();
}
----

This will work as expected, but if you try to read this InputStream object in a custom exception mapper, you will receive a `BlockingNotAllowedException` exception. This is because `ResponseExceptionMapper` classes are run on the Event Loop thread executor by default - which does not allow to perform IO operations.

To make your exception mapper blocking, you can annotate the exception mapper with the `@Blocking` annotation:

[source, java]
----
@Provider
@Blocking <1>
public class MyResponseExceptionMapper implements ResponseExceptionMapper<RuntimeException> {
@Override
public RuntimeException toThrowable(Response response) {
if (response.getStatus() == 500) {
response.readEntity(String.class); <2>
return new RuntimeException("The remote service responded with HTTP 500");
}
return null;
}
}
----

<1> With the `@Blocking` annotation, the MyResponseExceptionMapper exception mapper will be executed in the worker thread pool.
<2> Reading the entity is now allowed because we're executing the mapper on the worker thread pool.

Note that you can also use the `@Blocking` annotation when using @ClientExceptionMapper:

[source, java]
----
@Path("/echo")
@RegisterRestClient
public interface EchoClient {
@GET
InputStream get();
@ClientExceptionMapper
@Blocking
static RuntimeException toException(Response response) {
if (response.getStatus() == 500) {
response.readEntity(String.class);
return new RuntimeException("The remote service responded with HTTP 500");
}
return null;
}
}
----

[[multipart]]
== Multipart Form support

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,14 @@ GeneratedClassResult generateResponseExceptionMapper(AnnotationInstance instance
throw new IllegalStateException(message);
}

StringBuilder sigBuilder = new StringBuilder();
sigBuilder.append(targetMethod.name()).append("_").append(targetMethod.returnType().name().toString());
for (Type i : targetMethod.parameterTypes()) {
sigBuilder.append(i.name().toString());
}

int priority = Priorities.USER;
AnnotationValue priorityAnnotationValue = instance.value("priority");
if (priorityAnnotationValue != null) {
priority = priorityAnnotationValue.asInt();
}

ClassInfo restClientInterfaceClassInfo = targetMethod.declaringClass();
String generatedClassName = restClientInterfaceClassInfo.name().toString() + "_" + targetMethod.name() + "_"
+ "ResponseExceptionMapper" + "_" + HashUtil.sha1(sigBuilder.toString());
String generatedClassName = getGeneratedClassName(targetMethod);
try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput).className(generatedClassName)
.interfaces(ResteasyReactiveResponseExceptionMapper.class).build()) {
MethodCreator toThrowable = cc.getMethodCreator("toThrowable", Throwable.class, Response.class,
Expand Down Expand Up @@ -143,6 +136,17 @@ GeneratedClassResult generateResponseExceptionMapper(AnnotationInstance instance
return new GeneratedClassResult(restClientInterfaceClassInfo.name().toString(), generatedClassName, priority);
}

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() + "_"
+ "ResponseExceptionMapper" + "_" + 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.annotation.RegisterProviders;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import org.jboss.jandex.DotName;

import io.quarkus.rest.client.reactive.ClientExceptionMapper;
Expand All @@ -32,6 +33,8 @@ public class DotNames {
public static final DotName CLIENT_EXCEPTION_MAPPER = DotName.createSimple(ClientExceptionMapper.class.getName());
public static final DotName CLIENT_REDIRECT_HANDLER = DotName.createSimple(ClientRedirectHandler.class.getName());

public static final DotName RESPONSE_EXCEPTION_MAPPER = DotName.createSimple(ResponseExceptionMapper.class.getName());

static final DotName METHOD = DotName.createSimple(Method.class.getName());

private DotNames() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_CLIENT_HEADERS;
import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDER;
import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDERS;
import static io.quarkus.rest.client.reactive.deployment.DotNames.RESPONSE_EXCEPTION_MAPPER;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.*;
import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.CDI_WRAPPER_SUFFIX;
import static org.jboss.resteasy.reactive.common.processor.JandexUtil.isImplementorOf;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.BLOCKING;
import static org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner.BUILTIN_HTTP_ANNOTATIONS_TO_METHOD;

import java.lang.annotation.RetentionPolicy;
Expand Down Expand Up @@ -497,6 +500,24 @@ void addRestClientBeans(Capabilities capabilities,
}
}
}

Set<String> blockingClassNames = new HashSet<>();
Set<AnnotationInstance> registerBlockingClasses = new HashSet<>(index.getAnnotations(BLOCKING));
for (AnnotationInstance registerBlockingClass : registerBlockingClasses) {
AnnotationTarget target = registerBlockingClass.target();
if (target.kind() == AnnotationTarget.Kind.CLASS
&& isImplementorOf(index, target.asClass(), RESPONSE_EXCEPTION_MAPPER)) {
// Watch for @Blocking annotations in classes that implements ResponseExceptionMapper:
blockingClassNames.add(target.asClass().toString());
} else if (target.kind() == AnnotationTarget.Kind.METHOD
&& target.asMethod().annotation(CLIENT_EXCEPTION_MAPPER) != null) {
// Watch for @Blocking annotations in methods that are also annotated with @ClientExceptionMapper:
blockingClassNames.add(ClientExceptionMapperHandler.getGeneratedClassName(target.asMethod()));
}
}

recorder.setBlockingClassNames(blockingClassNames);

if (LaunchMode.current() == LaunchMode.DEVELOPMENT) {
recorder.setConfigKeys(configKeys);
}
Expand Down

0 comments on commit 55a49cc

Please sign in to comment.