Filtres et intercepteurs Jersey

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 en utilisant un serveur Tomcat 9.

2. Configuration de l'application

Créons d'abord 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.baeldung.jersey.server"); } }

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

Vous pouvez également consulter notre article axé sur le client et apprendre à créer un client Java avec Jersey.

3. Filtres

Maintenant, commençons avec les filtres.

En termes simples, les filtres nous permettent de modifier les propriétés des demandes et des réponses - par exemple, les en-têtes HTTP. Les filtres peuvent être appliqués à la fois côté serveur et côté client.

Gardez à l'esprit que les filtres sont toujours exécutés, que la ressource ait été trouvée ou non.

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 requête.

Nous le ferons en implémentant l' interface ContainerRequestFilter et en l'enregistrant en tant que fournisseur 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éthode abortWith () .

Comme le montre l'exemple, nous n'avons dû implémenter qu'une seule méthode qui reçoit le contexte de la requête, que nous pouvons modifier selon nos besoins.

Gardons à l'esprit que ce filtre est exécuté après la mise en correspondance de la ressource.

Si nous voulons exécuter un filtre avant la correspondance des ressources, nous pouvons utiliser un filtre de pré-correspondance en annotant notre filtre avec l' annotation @PreMatching :

@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 exécuté en premier:

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, notre filtre doit implémenter l' interface ContainerResponseFilter et implémenter 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ètre ContainerRequestContext 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 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 = "//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' interface ClientResponseFilter :

@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"); } }

Là encore, le ClientRequestContext est à des fins de lecture seule.

4. Intercepteurs

Les intercepteurs sont plus liés au marshalling et à la démarshalling des corps de message HTTP contenus dans les requêtes et les réponses. Ils peuvent être utilisés à la fois côté serveur et côté client.

Gardez à l'esprit qu'ils sont exécutés après les filtres et uniquement si un corps de message est présent.

Il existe deux types d'intercepteurs: ReaderInterceptor et WriterInterceptor , 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 ajouterons également 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'un ReaderInterceptor

Les intercepteurs de lecture nous permettent de manipuler les flux entrants, afin que nous puissions les utiliser pour modifier la requête 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 que nous devons appeler la méthode procède () pour appeler le prochain intercepteur de la chaîne . Une fois tous les intercepteurs exécutés, le lecteur de corps de message approprié sera appelé.

3.2. Implementing a WriterInterceptor

Writer interceptors work in a very similar way to reader interceptors, but they manipulate the outbound streams – so that we can use them with the request in the client side or with the response in the server side.

Let's create a writer interceptor to add a message to the request, on the client side:

@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(); } }

Again, we have to call the method proceed() to call the next interceptor.

When all the interceptors are executed, the appropriate message body writer will be called.

Don't forget that you have to register this interceptor in the client configuration, as we did before with the client filter:

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

5. Execution Order

Let's summarize all that we've seen so far in a diagram that shows when the filters and interceptors are executed during a request from a client to a server:

As we can see, the filters are always executed first, and the interceptors are executed right before calling the appropriate message body reader or writer.

If we take a look at the filters and interceptors that we've created, they will be executed in the following order:

  1. RequestClientFilter
  2. RequestClientWriterInterceptor
  3. PrematchingRequestFilter
  4. RestrictedOperationsRequestFilter
  5. RequestServerReaderInterceptor
  6. ResponseServerFilter
  7. ResponseClientFilter

Furthermore, when we have several filters or interceptors, we can specify the exact executing order by annotating them with the @Priority annotation.

The priority is specified with an Integer and sorts the filters and interceptors in ascending order for the requests and in descending order for the responses.

Let's add a priority to our RestrictedOperationsRequestFilter:

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

Notice that we've used a predefined priority for authorization purposes.

6. Name Binding

The filters and interceptors that we've seen so far are called global because they're executed for every request and response.

However, they can also be defined to be executed only for specific resource methods, which is called name binding.

6.1. Static Binding

One way to do the name binding is statically by creating a particular annotation that will be used in the desired resource. This annotation has to include the @NameBinding meta-annotation.

Let's create one in our application:

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

After that, we can annotate some resources with this @HelloBinding annotation:

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

Finally, we're going to annotate one of our filters with this annotation too, so this filter will be executed only for requests and responses that are accessing the getHelloGreeting() method:

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

Keep in mind that our RestrictedOperationsRequestFilter won't be triggered for the rest of the resources anymore.

6.2. Dynamic Binding

Another way to do this is by using a dynamic binding, which is loaded in the configuration during startup.

Let's first add another resource to our server for this section:

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

Now, let's create a binding for this resource by implementing the DynamicFeature interface:

@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); } } }

In this case, we're associating the getHiGreeting() method to the ResponseServerFilter that we had created before.

It's important to remember that we had to delete the @Provider annotation from this filter since we're now configuring it via DynamicFeature.

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éthode getHiGreeting () .

7. Conclusion

Dans ce didacticiel, nous nous sommes concentrés sur la compréhension du 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 disponible à l'adresse over sur GitHub.