Filtres et intercepteurs de Jersey

Filtres Jersey et intercepteurs

1. introduction

Dans cet article, nous allons expliquer le fonctionnement des filtres et des intercepteurs dans le framework Jersey, ainsi que les principales différences entre ceux-ci.

Nous utiliserons Jersey 2 ici et nous testerons notre application à l'aide d'un serveur Tomcat 9.

2. Configuration de l'application

Commençons par créer une ressource simple sur notre serveur:

@Path("/greetings")
public class Greetings {

    @GET
    public String getHelloGreeting() {
        return "hello";
    }
}

Créons également la configuration de serveur correspondante pour notre application:

@ApplicationPath("/*")
public class ServerConfig extends ResourceConfig {

    public ServerConfig() {
        packages("com.example.jersey.server");
    }
}

Si vous souhaitez approfondir la création d'une API avec Jersey, vous pouvez consulterthis article.

Vous pouvez également consulterour client-focused article et apprendre à créer un client Java avec Jersey.

3. Les filtres

Maintenant, commençons avec les filtres.

En termes simples,filters let us modify the properties of requests and responses - par exemple, les en-têtes HTTP. Les filtres peuvent être appliqués à la fois du côté serveur et côté client.

Gardez à l'esprit quefilters are always executed, regardless of whether the resource was found or not.

3.1. Implémentation d'un filtre de serveur de requêtes

Commençons par les filtres côté serveur et créons un filtre de demande.

Nous allons le faire en implémentant l'interfaceContainerRequestFilter et en l'enregistrant en tant queProvider sur notre serveur:

@Provider
public class RestrictedOperationsRequestFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext ctx) throws IOException {
        if (ctx.getLanguage() != null && "EN".equals(ctx.getLanguage()
          .getLanguage())) {

            ctx.abortWith(Response.status(Response.Status.FORBIDDEN)
              .entity("Cannot access")
              .build());
        }
    }
}

Ce filtre simple rejette simplement les requêtes avec la langue“EN” dans la requête en appelant la méthodeabortWith().

Comme le montre l'exemple, nous avons dû implémenter une seule méthode qui reçoit le contexte de la demande, que nous pouvons modifier à notre guise.

Gardons à l'esprit quethis filter is executed after the resource was matched.

Si nous voulons exécuter un filtre avant la correspondance des ressources,we can use a pre-matching filter by annotating our filter with the @PreMatching annotation:

@Provider
@PreMatching
public class PrematchingRequestFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext ctx) throws IOException {
        if (ctx.getMethod().equals("DELETE")) {
            LOG.info("\"Deleting request");
        }
    }
}

Si nous essayons d'accéder à notre ressource maintenant, nous pouvons vérifier que notre filtre de pré-correspondance est d'abord exécuté:

2018-02-25 16:07:27,800 [http-nio-8080-exec-3] INFO  c.b.j.s.f.PrematchingRequestFilter - prematching filter
2018-02-25 16:07:27,816 [http-nio-8080-exec-3] INFO  c.b.j.s.f.RestrictedOperationsRequestFilter - Restricted operations filter

3.2. Implémentation d'un filtre de serveur de réponse

Nous allons maintenant implémenter un filtre de réponse côté serveur qui ajoutera simplement un nouvel en-tête à la réponse.

Pour ce faire,our filter has to implement the ContainerResponseFilter interface et implémentez sa seule méthode:

@Provider
public class ResponseServerFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext,
      ContainerResponseContext responseContext) throws IOException {
        responseContext.getHeaders().add("X-Test", "Filter test");
    }
}

Notez que le paramètreContainerRequestContext est simplement utilisé en lecture seule - puisque nous traitons déjà la réponse.

2.3. Implémentation d'un filtre client

Nous allons maintenant travailler avec des filtres côté client. Ces filtres fonctionnent de la même manière que les filtres de serveur et les interfaces que nous devons implémenter sont très similaires à celles du côté serveur.

Voyons cela en action avec un filtre qui ajoute une propriété à la requête:

@Provider
public class RequestClientFilter implements ClientRequestFilter {

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        requestContext.setProperty("test", "test client request filter");
    }
}

Créons également un client Jersey pour tester ce filtre:

public class JerseyClient {

    private static String URI_GREETINGS = "http://localhost:8080/jersey/greetings";

    public static String getHelloGreeting() {
        return createClient().target(URI_GREETINGS)
          .request()
          .get(String.class);
    }

    private static Client createClient() {
        ClientConfig config = new ClientConfig();
        config.register(RequestClientFilter.class);

        return ClientBuilder.newClient(config);
    }
}

Notez que nous devons ajouter le filtre à la configuration du client pour l’enregistrer.

Enfin, nous allons également créer un filtre pour la réponse dans le client.

Cela fonctionne d'une manière très similaire à celle du serveur, mais en implémentant l'interfaceClientResponseFilter:

@Provider
public class ResponseClientFilter implements ClientResponseFilter {

    @Override
    public void filter(ClientRequestContext requestContext,
      ClientResponseContext responseContext) throws IOException {
        responseContext.getHeaders()
          .add("X-Test-Client", "Test response client filter");
    }

}

Encore une fois, leClientRequestContext est à des fins de lecture seule.

4. Intercepteurs

Les intercepteurs sont davantage liés au tri et à la dissémination des corps des messages HTTP contenus dans les demandes et les réponses. Ils peuvent être utilisés à la fois du côté serveur et côté client.

Gardez à l'esprit quethey’re executed after the filters and only if a message body is present.

Il existe deux types d'intercepteurs:ReaderInterceptor etWriterInterceptor, et ils sont identiques pour le serveur et le côté client.

Ensuite, nous allons créer une autre ressource sur notre serveur - qui est accessible via un POST et reçoit un paramètre dans le corps, donc les intercepteurs seront exécutés lors de l'accès:

@POST
@Path("/custom")
public Response getCustomGreeting(String name) {
    return Response.status(Status.OK.getStatusCode())
      .build();
}

Nous allons également ajouter une nouvelle méthode à notre client Jersey - pour tester cette nouvelle ressource:

public static Response getCustomGreeting() {
    return createClient().target(URI_GREETINGS + "/custom")
      .request()
      .post(Entity.text("custom"));
}

4.1. Implémentation d'unReaderInterceptor

Les intercepteurs de lecteurs nous permettent de manipuler les flux entrants afin que nous puissions les utiliser pour modifier la demande côté serveur ou la réponse côté client.

Créons un intercepteur côté serveur pour écrire un message personnalisé dans le corps de la requête interceptée:

@Provider
public class RequestServerReaderInterceptor implements ReaderInterceptor {

    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
      throws IOException, WebApplicationException {
        InputStream is = context.getInputStream();
        String body = new BufferedReader(new InputStreamReader(is)).lines()
          .collect(Collectors.joining("\n"));

        context.setInputStream(new ByteArrayInputStream(
          (body + " message added in server reader interceptor").getBytes()));

        return context.proceed();
    }
}

Notez quewe have to call the proceed() methodto call the next interceptor in the chain. Une fois que tous les intercepteurs sont exécutés, le lecteur de corps du message approprié sera appelé.

3.2. Implémentation d'unWriterInterceptor

Les intercepteurs Writer fonctionnent de manière très similaire aux intercepteurs lecteurs, mais ils manipulent les flux sortants afin que nous puissions les utiliser avec la requête côté client ou avec la réponse côté serveur.

Créons un intercepteur d'écriture pour ajouter un message à la requête, côté client:

@Provider
public class RequestClientWriterInterceptor implements WriterInterceptor {

    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
      throws IOException, WebApplicationException {
        context.getOutputStream()
          .write(("Message added in the writer interceptor in the client side").getBytes());

        context.proceed();
    }
}

Encore une fois, nous devons appeler la méthodeproceed() pour appeler l'intercepteur suivant.

Lorsque tous les intercepteurs sont exécutés, le rédacteur du corps du message approprié sera appelé.

Don’t forget that you have to register this interceptor in the client configuration, comme nous l'avons fait auparavant avec le filtre client:

private static Client createClient() {
    ClientConfig config = new ClientConfig();
    config.register(RequestClientFilter.class);
    config.register(RequestWriterInterceptor.class);

    return ClientBuilder.newClient(config);
}

5. Ordre d'exécution

Résumons tout ce que nous avons vu jusqu'à présent dans un diagramme qui montre quand les filtres et les intercepteurs sont exécutés lors d'une requête d'un client à un serveur:

image

Comme nous pouvons le voir,the filters are always executed first, and the interceptors are executed right before calling the appropriate message body reader or writer.

Si nous examinons les filtres et les intercepteurs que nous avons créés, ils seront exécutés dans l’ordre suivant:

  1. RequestClientFilter

  2. RequestClientWriterInterceptor

  3. PrématchingRequestFilter

  4. RestrictedOperationsRequestFilter

  5. RequestServerReaderInterceptor

  6. ResponseServerFilter

  7. ResponseClientFilter

De plus, lorsque nous avons plusieurs filtres ou intercepteurs, nous pouvons spécifier l'ordre d'exécution exact en les annotant avec l'annotation@Priority.

La priorité est spécifiée avec unInteger et trie les filtres et les intercepteurs par ordre croissant pour les requêtes et par ordre décroissant pour les réponses.

Ajoutons une priorité à nosRestrictedOperationsRequestFilter:

@Provider
@Priority(Priorities.AUTHORIZATION)
public class RestrictedOperationsRequestFilter implements ContainerRequestFilter {
    // ...
}

Notez que nous avons utilisé une priorité prédéfinie à des fins d'autorisation.

6. Liaison de nom

Les filtres et les intercepteurs que nous avons vus jusqu'à présent sont appelés globaux car ils sont exécutés pour chaque demande et réponse.

Cependant,they can also be defined to be executed only for specific resource methods, qui est appelé liaison de nom.

6.1. Liaison statique

Une façon de faire la liaison de nom est de manière statique en créant une annotation particulière qui sera utilisée dans la ressource souhaitée. Cette annotation doit inclure la méta-annotation@NameBinding.

Créons-en un dans notre application:

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface HelloBinding {
}

Après cela, nous pouvons annoter certaines ressources avec cette annotation@HelloBinding:

@GET
@HelloBinding
public String getHelloGreeting() {
    return "hello";
}

Enfin, nous allons également annoter l'un de nos filtres avec cette annotation, donc ce filtre ne sera exécuté que pour les demandes et les réponses qui accèdent à la méthodegetHelloGreeting():

@Provider
@Priority(Priorities.AUTHORIZATION)
@HelloBinding
public class RestrictedOperationsRequestFilter implements ContainerRequestFilter {
    // ...
}

Gardez à l'esprit que nosRestrictedOperationsRequestFilterne seront plus déclenchés pour le reste des ressources.

6.2. Liaison dynamique

Pour ce faire, vous pouvez également utiliser une liaison dynamique, chargée dans la configuration lors du démarrage.

Commençons par ajouter une autre ressource à notre serveur pour cette section:

@GET
@Path("/hi")
public String getHiGreeting() {
    return "hi";
}

Maintenant, créons une liaison pour cette ressource en implémentant l'interfaceDynamicFeature:

@Provider
public class HelloDynamicBinding implements DynamicFeature {

    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        if (Greetings.class.equals(resourceInfo.getResourceClass())
          && resourceInfo.getResourceMethod().getName().contains("HiGreeting")) {
            context.register(ResponseServerFilter.class);
        }
    }
}

Dans ce cas, nous associons la méthodegetHiGreeting() auxResponseServerFilter que nous avions créés auparavant.

Il est important de se rappeler que nous avons dû supprimer l’annotation@Provider de ce filtre puisque nous sommes en train de la configurer viaDynamicFeature.

Si nous ne le faisons pas, le filtre sera exécuté deux fois: une fois en tant que filtre global et une autre fois en tant que filtre lié à la méthodegetHiGreeting().

7. Conclusion

Dans ce tutoriel, nous nous sommes concentrés sur le fonctionnement des filtres et des intercepteurs dans Jersey 2 et sur la manière de les utiliser dans une application Web.

Comme toujours, le code source complet des exemples est disponibleover on GitHub.