Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support compression for reactive routes and resteasy reactive #24558

Merged
merged 1 commit into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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