Skip to content

Commit

Permalink
Support compression for reactive routes and resteasy reactive
Browse files Browse the repository at this point in the history
- resolves #16425
  • Loading branch information
mkouba committed Mar 29, 2022
1 parent f919dcd commit 670a205
Show file tree
Hide file tree
Showing 27 changed files with 695 additions and 23 deletions.
20 changes: 20 additions & 0 deletions docs/src/main/asciidoc/reactive-routes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,26 @@ public class MyFilters {
<1> The `RouteFilter#value()` defines the priority used to sort the filters - filters with higher priority are called first.
<2> The filter is likely required to call the `next()` method to continue the chain.
== HTTP Compression
The body of an HTTP response is not compressed by default.
You can enable the HTTP compression support by means of `quarkus.http.enable-compression=true`.
If compression support is enabled then the response body is compressed if:
- the route method is annotated with `@io.quarkus.vertx.http.Compressed`, or
- the `Content-Type` header is set and the value is a compressed media type as configured via `quarkus.http.compress-media-types`.
The response body is never compressed if:
- the route method is annotated with `@io.quarkus.vertx.http.Uncompressed`, or
- the `Content-Type` header is not set.
TIP: By default, the following list of media types is compressed: `text/html`, `text/plain`, `text/xml`, `text/css`, `text/javascript` and `application/javascript`.
NOTE: If the client does not support HTTP compression then the response body is not compressed.
== Adding OpenAPI and Swagger UI
You can add support for link:https://www.openapis.org/[OpenAPI] and link:https://swagger.io/tools/swagger-ui/[Swagger UI] by using the `quarkus-smallrye-openapi` extension.
Expand Down
20 changes: 20 additions & 0 deletions docs/src/main/asciidoc/resteasy-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2177,6 +2177,26 @@ Or plain text:
< {"name":"roquefort"}
----

=== HTTP Compression

The body of an HTTP response is not compressed by default.
You can enable the HTTP compression support by means of `quarkus.http.enable-compression=true`.

If compression support is enabled then the response body is compressed if:

- the resource method is annotated with `@io.quarkus.vertx.http.Compressed`, or
- the `Content-Type` header is set and the value is a compressed media type as configured via `quarkus.http.compress-media-types`.

The response body is never compressed if:

- the resource method is annotated with `@io.quarkus.vertx.http.Uncompressed`, or
- the `Content-Type` header is not set.

TIP: By default, the following list of media types is compressed: `text/html`, `text/plain`, `text/xml`, `text/css`, `text/javascript` and `application/javascript`.

NOTE: If the client does not support HTTP compression then the response body is not compressed.


== Include/Exclude JAX-RS classes with build time conditions

Quarkus enables the inclusion or exclusion of JAX-RS Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans.
Expand Down
3 changes: 1 addition & 2 deletions docs/src/main/asciidoc/resteasy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -615,8 +615,7 @@ This configuration option would recognize strings in this format (shown as a reg

Once GZip support has been enabled you can use it on an endpoint by adding the `@org.jboss.resteasy.annotations.GZIP` annotation to your endpoint method.

If you want to compress everything then we recommended that you use the `quarkus.http.enable-compression=true` setting instead to globally enable
compression support.
NOTE: The configuration property `quarkus.http.enable-compression` has no effect on compression support of RESTEasy Classic endpoints.

== Multipart Support

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,31 @@

import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.vertx.http.runtime.HttpCompression;

public final class AnnotatedRouteHandlerBuildItem extends MultiBuildItem {

private final BeanInfo bean;
private final List<AnnotationInstance> routes;
private final AnnotationInstance routeBase;
private final MethodInfo method;
private final boolean blocking;
private final HttpCompression compression;

public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List<AnnotationInstance> routes,
AnnotationInstance routeBase) {
this(bean, method, routes, routeBase, false, HttpCompression.UNDEFINED);
}

public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List<AnnotationInstance> routes,
AnnotationInstance routeBase, boolean blocking, HttpCompression compression) {
super();
this.bean = bean;
this.method = method;
this.routes = routes;
this.routeBase = routeBase;
this.method = method;
this.blocking = blocking;
this.compression = compression;
}

public BeanInfo getBean() {
Expand All @@ -39,4 +50,12 @@ public AnnotationInstance getRouteBase() {
return routeBase;
}

public boolean isBlocking() {
return blocking;
}

public HttpCompression getCompression() {
return compression;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import org.jboss.jandex.DotName;

import io.quarkus.vertx.http.Compressed;
import io.quarkus.vertx.http.Uncompressed;
import io.quarkus.vertx.web.Body;
import io.quarkus.vertx.web.Header;
import io.quarkus.vertx.web.Param;
Expand Down Expand Up @@ -50,5 +52,7 @@ final class DotNames {
static final DotName THROWABLE = DotName.createSimple(Throwable.class.getName());
static final DotName BLOCKING = DotName.createSimple(Blocking.class.getName());
static final DotName COMPLETION_STAGE = DotName.createSimple(CompletionStage.class.getName());
static final DotName COMPRESSED = DotName.createSimple(Compressed.class.getName());
static final DotName UNCOMPRESSED = DotName.createSimple(Uncompressed.class.getName());

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -101,6 +102,7 @@
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
import io.quarkus.vertx.http.deployment.devmode.RouteDescriptionBuildItem;
import io.quarkus.vertx.http.runtime.HandlerType;
import io.quarkus.vertx.http.runtime.HttpCompression;
import io.quarkus.vertx.web.Param;
import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.Route.HttpMethod;
Expand Down Expand Up @@ -145,12 +147,12 @@ FeatureBuildItem feature() {
@BuildStep
void unremovableBeans(BuildProducer<UnremovableBeanBuildItem> unremovableBeans) {
unremovableBeans
.produce(UnremovableBeanBuildItem.beanClassAnnotation(io.quarkus.vertx.web.deployment.DotNames.ROUTE));
.produce(UnremovableBeanBuildItem.beanClassAnnotation(DotNames.ROUTE));
unremovableBeans
.produce(UnremovableBeanBuildItem.beanClassAnnotation(io.quarkus.vertx.web.deployment.DotNames.ROUTES));
.produce(UnremovableBeanBuildItem.beanClassAnnotation(DotNames.ROUTES));
unremovableBeans
.produce(UnremovableBeanBuildItem
.beanClassAnnotation(io.quarkus.vertx.web.deployment.DotNames.ROUTE_FILTER));
.beanClassAnnotation(DotNames.ROUTE_FILTER));
}

@BuildStep
Expand All @@ -168,33 +170,54 @@ void validateBeanDeployment(
// NOTE: inherited business methods are not taken into account
ClassInfo beanClass = bean.getTarget().get().asClass();
AnnotationInstance routeBaseAnnotation = beanClass
.classAnnotation(io.quarkus.vertx.web.deployment.DotNames.ROUTE_BASE);
.classAnnotation(DotNames.ROUTE_BASE);
for (MethodInfo method : beanClass.methods()) {
if (method.isSynthetic() || Modifier.isStatic(method.flags()) || method.name().equals("<init>")) {
continue;
}

List<AnnotationInstance> routes = new LinkedList<>();
AnnotationInstance routeAnnotation = annotationStore.getAnnotation(method,
io.quarkus.vertx.web.deployment.DotNames.ROUTE);
DotNames.ROUTE);
if (routeAnnotation != null) {
validateRouteMethod(bean, method, transformedAnnotations, beanArchive.getIndex(), routeAnnotation);
routes.add(routeAnnotation);
}
if (routes.isEmpty()) {
AnnotationInstance routesAnnotation = annotationStore.getAnnotation(method,
io.quarkus.vertx.web.deployment.DotNames.ROUTES);
DotNames.ROUTES);
if (routesAnnotation != null) {
for (AnnotationInstance annotation : routesAnnotation.value().asNestedArray()) {
validateRouteMethod(bean, method, transformedAnnotations, beanArchive.getIndex(), annotation);
routes.add(annotation);
}
}
}

if (!routes.isEmpty()) {
LOGGER.debugf("Found route handler business method %s declared on %s", method, bean);

HttpCompression compression = HttpCompression.UNDEFINED;
if (annotationStore.hasAnnotation(method, DotNames.COMPRESSED)) {
compression = HttpCompression.ON;
}
if (annotationStore.hasAnnotation(method, DotNames.UNCOMPRESSED)) {
if (compression == HttpCompression.ON) {
errors.produce(new ValidationErrorBuildItem(new IllegalStateException(
String.format(
"@Compressed and @Uncompressed cannot be both declared on business method %s declared on %s",
method, bean))));
} else {
compression = HttpCompression.OFF;
}
}
routeHandlerBusinessMethods
.produce(new AnnotatedRouteHandlerBuildItem(bean, method, routes, routeBaseAnnotation));
.produce(new AnnotatedRouteHandlerBuildItem(bean, method, routes, routeBaseAnnotation,
annotationStore.hasAnnotation(method, DotNames.BLOCKING), compression));
}
//
AnnotationInstance filterAnnotation = annotationStore.getAnnotation(method,
io.quarkus.vertx.web.deployment.DotNames.ROUTE_FILTER);
DotNames.ROUTE_FILTER);
if (filterAnnotation != null) {
if (!routes.isEmpty()) {
errors.produce(new ValidationErrorBuildItem(new IllegalStateException(
Expand Down Expand Up @@ -367,7 +390,7 @@ public boolean test(String name) {
}
}

if (businessMethod.getMethod().annotation(DotNames.BLOCKING) != null) {
if (businessMethod.isBlocking()) {
if (handlerType == HandlerType.NORMAL) {
handlerType = HandlerType.BLOCKING;
} else if (handlerType == HandlerType.FAILURE) {
Expand All @@ -389,6 +412,10 @@ public boolean test(String name) {
routeHandlers.put(routeString, routeHandler);
}

// Wrap the route handler if necessary
// Note that route annotations with the same values share a single handler implementation
routeHandler = recorder.compressRouteHandler(routeHandler, businessMethod.getCompression());

RouteMatcher matcher = new RouteMatcher(path, regex, produces, consumes, methods, order);
matchers.put(matcher, businessMethod.getMethod());
Function<Router, io.vertx.ext.web.Route> routeFunction = recorder.createRouteFunction(matcher,
Expand Down Expand Up @@ -453,9 +480,9 @@ void routeNotFound(Capabilities capabilities, ResourceNotFoundRecorder recorder,
@BuildStep
AutoAddScopeBuildItem autoAddScope() {
return AutoAddScopeBuildItem.builder()
.containsAnnotations(io.quarkus.vertx.web.deployment.DotNames.ROUTE,
io.quarkus.vertx.web.deployment.DotNames.ROUTES,
io.quarkus.vertx.web.deployment.DotNames.ROUTE_FILTER)
.containsAnnotations(DotNames.ROUTE,
DotNames.ROUTES,
DotNames.ROUTE_FILTER)
.defaultScope(BuiltinScope.SINGLETON)
.reason("Found route handler business methods").build();
}
Expand All @@ -467,10 +494,10 @@ private void validateRouteFilterMethod(BeanInfo bean, MethodInfo method) {
}
List<Type> params = method.parameters();
if (params.size() != 1 || !params.get(0).name()
.equals(io.quarkus.vertx.web.deployment.DotNames.ROUTING_CONTEXT)) {
.equals(DotNames.ROUTING_CONTEXT)) {
throw new IllegalStateException(String.format(
"Route filter method must accept exactly one parameter of type %s: %s [method: %s, bean: %s]",
io.quarkus.vertx.web.deployment.DotNames.ROUTING_CONTEXT, params, method, bean));
DotNames.ROUTING_CONTEXT, params, method, bean));
}
}

Expand Down Expand Up @@ -1252,7 +1279,7 @@ static List<ParameterInjector> initParamInjectors() {
List<ParameterInjector> injectors = new ArrayList<>();

injectors.add(
ParameterInjector.builder().canEndResponse().matchType(io.quarkus.vertx.web.deployment.DotNames.ROUTING_CONTEXT)
ParameterInjector.builder().canEndResponse().matchType(DotNames.ROUTING_CONTEXT)
.resultHandleProvider(new ResultHandleProvider() {
@Override
public ResultHandle get(MethodInfo method, Type paramType, Set<AnnotationInstance> annotations,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.quarkus.vertx.web.compress;

import static io.restassured.RestAssured.get;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import javax.enterprise.context.ApplicationScoped;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.Compressed;
import io.quarkus.vertx.http.Uncompressed;
import io.quarkus.vertx.web.Route;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import io.vertx.ext.web.RoutingContext;

public class CompressionTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(MyRoutes.class)
.addAsManifestResource(new StringAsset(MyRoutes.MESSAGE), "resources/file.txt")
.addAsManifestResource(new StringAsset(MyRoutes.MESSAGE), "resources/my.doc"))
.overrideConfigKey("quarkus.http.enable-compression", "true");

@Test
public void testRoutes() {
assertCompressed("/compressed");
assertUncompressed("/uncompressed");
assertCompressed("/compressed-content-type");
assertUncompressed("/uncompressed-content-type");
assertCompressed("/content-type-implicitly-compressed");
assertCompressed("/content-type-with-param-implicitly-compressed");
assertUncompressed("/content-type-implicitly-uncompressed");
assertCompressed("/compression-disabled-manually");
assertCompressed("/file.txt");
assertUncompressed("/my.doc");
}

private void assertCompressed(String path) {
String bodyStr = get(path).then().statusCode(200).header("Content-Encoding", "gzip").extract().asString();
assertEquals("Hello compression!", bodyStr);
}

private void assertUncompressed(String path) {
ExtractableResponse<Response> response = get(path)
.then().statusCode(200).extract();
assertTrue(response.header("Content-Encoding") == null, response.headers().toString());
assertEquals(MyRoutes.MESSAGE, response.asString());
}

@ApplicationScoped
public static class MyRoutes {

static String MESSAGE = "Hello compression!";

@Compressed
@Route
String compressed() {
return MESSAGE;
}

@Uncompressed
@Route
String uncompressed() {
return MESSAGE;
}

@Uncompressed
@Route
void uncompressedContentType(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "text/plain").end(MESSAGE);
}

@Compressed
@Route
void compressedContentType(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "foo/bar").end(MESSAGE);
}

@Route
void contentTypeImplicitlyCompressed(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "text/plain").end(MESSAGE);
}

@Route
void contentTypeWithParamImplicitlyCompressed(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "text/plain;charset=UTF-8").end(MESSAGE);
}

@Route
void contentTypeImplicitlyUncompressed(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "foo/bar").end(MESSAGE);
}

@Route
void compressionDisabledManually(RoutingContext context) {
context.response().headers().remove("Content-Encoding");
context.response().setStatusCode(200).putHeader("Content-type", "text/plain").end(MESSAGE);
}

}

}
Loading

0 comments on commit 670a205

Please sign in to comment.