Chapitre API RESTful avec Quarkus
Utilisation des intercepteurs et des filtres HTTP
Le but de cet exercice est d'apprendre l'utilité des filtres HTTP et des intercepteurs Quarkus.
Quarkus fournit les possibilités suivantes :
- les intercepteurs
- les décorateurs
- les filtres HTTP
Prérequis
Avant de commencer cet exercice, assurez-vous d'avoir les éléments suivants installés sur votre système :
- JDK 11 ou supérieur
- Apache Maven
- Un éditeur de texte ou une IDE Java (comme Eclipse, IntelliJ IDEA, ou Visual Studio Code)
Nous allons explorer les différentes fonctionnalités de Quarkus pour les intercepteurs et les filtres HTTP.
Utiliser les intercepteurs
Dans cet exercice, nous allons utiliser les intercepteurs Quarkus pour intercepter les appels à certaines méthodes et en profiter pour afficher les informations des paramètres de la méthode dans les logs.
Pour cela nous allons commencer par créer une annotation @Logged
dans le microservice qui sert de backend.
package com.byoskill.utils;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.interceptor.InterceptorBinding;
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Inherited
public @interface Logged {
}
Si vous avez un problème avec les importations, vérifiez que vous avez bien importé jakarta.interceptor.InterceptorBinding
et l'extension Quarkus :
quarkus ext add io.quarkus:quarkus-arc
Nous allons ensuite annoter les méthodes de notre composant AdoptionMemoryRepository.java
.
Par exemple :
@Logged
@Override
public Multi<Monster> getAllMonsters() {
return Multi.createFrom().iterable(monsters);
}
Il est désormais temps d'ajouter le code de notre intercepteur.
package com.byoskill.utils;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
import org.jboss.logging.Logger;
import java.lang.reflect.Method;
@Logged
@Priority(2020)
@Interceptor
public class LoggingInterceptor {
@Inject
Logger logger;
@AroundInvoke
Object logInvocation(InvocationContext context) throws Exception {
Method method = context.getMethod();
var params = method.getParameters();
logger.info("Calling " + method.getName() + " with " + params.length + " parameters");
for (int i = 0; i < params.length; i++) {
logger.info("Parameter " + i + " is " + params[i].getName() + " of type " + params[i].getType() + " and value " + context.getParameters()[i]);
}
Object ret = context.proceed();
return ret;
}
}
Les décorateurs
Dans Quarkus, les décorateurs sont utilisés pour surcharger le comportement d'un composant qui expose un contrat (interface).
Nous pouvons par exemple surcharger l'interface AdoptionRepository
afin d'ajouter un contrôle supplémentaire quand une adoption est ajoutée.
Nous allons d'abord créer une interface qui définit un contrat particulier quand un développeur veut créer une méthode pour changer le nom d'une entité.
Nous allons créer une interface qui définit le comportement attendu pour une entité qui peut changer de nom.
package com.byoskill.common.model;
public interface HasName {
void setName(String name);
}
Nous allons créer le contrat pour les repository qui offrent la possibilité de changer le nom d'une entité.
package com.byoskill.common.model;
public interface ChangeNameOperation<T extends HasName> {
T changeName(T _newEntity, String newName);
}
Cette interface sera ajoutée à l'interface AdoptionRepository
afin d'offrir ce comportement.
Nous allons modifier l'interface AdoptionRepository
afin que l'interface définisse ce contrat.
import com.byoskill.common.model.ChangeNameOperation;
public interface AdoptionRepository extends ChangeNameOperation<Monster> {
}
Nous ajoutons également l'interface HasName
à la classe Monster
.
public class Monster implements HasName {
}
Créons enfin notre décorateur :
package com.byoskill.domain.common.model;
import io.smallrye.mutiny.Uni;
import jakarta.annotation.Priority;
import jakarta.decorator.Decorator;
import jakarta.decorator.Delegate;
import jakarta.enterprise.inject.Any;
import jakarta.inject.Inject;
import jakarta.ws.rs.WebApplicationException;
@Priority(10)
@Decorator
public class ChangeNameOperationDecorator implements ChangeNameOperation<HasName> {
@Inject
@Any
@Delegate
ChangeNameOperation<HasName> delegate;
@Override
public Uni<HasName> changeName(final HasName entityToBeUpdated, final String newName) {
if (3 > newName.length()) throw new WebApplicationException("Name must be at least 3 characters long", 400);
if (newName.contains(" ")) throw new WebApplicationException("Name must not contain spaces", 400);
return delegate.changeName(entityToBeUpdated, newName);
}
}
Tâche optionnelle
Créez un endpoint REST offrant la possibilité de changer le nom d'une entité.
Vous pouvez utiliser par exemple le path /monsters/{id}/name
.
Exemple de solution:
@POST
@Path("/{id}/name")
public Uni<Monster> changeName(@PathParam("id") final String id, @QueryParam("name") final String name) {
return adoptionRepository.getMonsterByUuid(id)
.log("findByUuid")
.onItem().ifNull().failWith(new WebApplicationException("Monster not found", 404))
.call(monster -> adoptionRepository.changeName(monster, name));
}
Les filtres HTTP
Nous allons regarder dans cette dernière section comment implémenter un filtre HTTP et notamment implémenter un filtre CORS très simple avec Quarkus.
Les filtres HTTP sous Quarkus sont des classes qui implémentent l'interface io.quarkus.vertx.http.runtime.filters.Filter
.
Voici différents types de filtres comme exemples pour jouer avec :
Un filtre très simple qui ajoute un header HTTP :
@Provider
public class JaxFilter implements ContainerResponseFilter {
@Override
public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-Content-Type-Options", "nosniff");
}
}
Un filtre d'API qui bloque si le HTTP Header est manquant :
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ApiFilter {
}
@ApiFilter
@Provider
public class BasicApiFilter implements ContainerResponseFilter {
@Override
public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext)
throws IOException {
// Detect if the HTTP Header containing the API Key is missing
if (null == requestContext.getHeaderString("x-api-key")) {
throw new WebApplicationException("Missing API Key", 403);
}
}
}
Voici un autre exemple qui utilise une syntaxe plus moderne :
package com.byoskill.frontend.security;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
import java.util.Optional;
class Filters {
@ServerRequestFilter
public Optional<Response> getFilter(final ContainerRequestContext ctx) {
// only allow GET methods for now
if (ctx.getMethod().equals(HttpMethod.GET)) {
ctx.abortWith(Response.status(Response.Status.METHOD_NOT_ALLOWED).build());
return Optional.of(Response.status(Response.Status.METHOD_NOT_ALLOWED).build());
}
return Optional.empty();
}
}
Plus d'informations sur les filtres :
- https://quarkus.io/guides/writing-custom-filters
- https://quarkus.io/guides/security-cors#cors-filteruserguide/html/Interceptors.html
- https://quarkus.io/guides/http-reference#cors-filter