Filtri e intercettori Jersey

1. Introduzione

In questo articolo, spiegheremo come funzionano i filtri e gli intercettori nel framework Jersey, nonché le principali differenze tra questi.

Useremo Jersey 2 qui e testeremo la nostra applicazione utilizzando un server Tomcat 9.

2. Configurazione dell'applicazione

Creiamo prima una semplice risorsa sul nostro server:

@Path("/greetings") public class Greetings { @GET public String getHelloGreeting() { return "hello"; } }

Inoltre, creiamo la configurazione del server corrispondente per la nostra applicazione:

@ApplicationPath("/*") public class ServerConfig extends ResourceConfig { public ServerConfig() { packages("com.baeldung.jersey.server"); } }

Se vuoi approfondire come creare un'API con Jersey, puoi consultare questo articolo.

Puoi anche dare un'occhiata al nostro articolo incentrato sul cliente e imparare come creare un client Java con Jersey.

3. Filtri

Ora iniziamo con i filtri.

In poche parole, i filtri ci consentono di modificare le proprietà delle richieste e delle risposte , ad esempio le intestazioni HTTP. I filtri possono essere applicati sia sul lato server che sul lato client.

Tieni presente che i filtri vengono sempre eseguiti, indipendentemente dal fatto che la risorsa sia stata trovata o meno.

3.1. Implementazione di un filtro del server di richiesta

Cominciamo con i filtri lato server e creiamo un filtro di richiesta.

Lo faremo implementando l' interfaccia ContainerRequestFilter e registrandola come Provider nel nostro server:

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

Questo semplice filtro rifiuta semplicemente le richieste con il linguaggio "EN" nella richiesta chiamando il metodo abortWith () .

Come mostra l'esempio, abbiamo dovuto implementare un solo metodo che riceve il contesto della richiesta, che possiamo modificare secondo le nostre necessità.

Teniamo presente che questo filtro viene eseguito dopo che la risorsa è stata abbinata.

Nel caso in cui desideriamo eseguire un filtro prima della corrispondenza delle risorse, possiamo utilizzare un filtro di pre-corrispondenza annotando il nostro filtro con l' annotazione @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"); } } }

Se proviamo ad accedere alla nostra risorsa ora, possiamo verificare che il nostro filtro di pre-corrispondenza venga eseguito per primo:

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. Implementazione di un filtro del server di risposta

Ora implementeremo un filtro di risposta sul lato server che aggiungerà semplicemente una nuova intestazione alla risposta.

Per fare ciò, il nostro filtro deve implementare l' interfaccia ContainerResponseFilter e implementare il suo unico metodo:

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

Si noti che il parametro ContainerRequestContext viene utilizzato solo come di sola lettura, poiché stiamo già elaborando la risposta.

2.3. Implementazione di un filtro client

Ora lavoreremo con i filtri sul lato client. Questi filtri funzionano allo stesso modo dei filtri del server e le interfacce che dobbiamo implementare sono molto simili a quelle per il lato server.

Vediamolo in azione con un filtro che aggiunge una proprietà alla richiesta:

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

Creiamo anche un client Jersey per testare questo filtro:

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

Notare che dobbiamo aggiungere il filtro alla configurazione del client per registrarlo.

Infine, creeremo anche un filtro per la risposta nel client.

Funziona in modo molto simile a quello nel server, ma implementa l' interfaccia 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"); } }

Anche in questo caso, ClientRequestContext è per scopi di sola lettura.

4. Intercettori

Gli intercettatori sono più connessi con il marshalling e l'unmarshalling dei corpi dei messaggi HTTP contenuti nelle richieste e nelle risposte. Possono essere utilizzati sia sul lato server che sul lato client.

Tieni presente che vengono eseguiti dopo i filtri e solo se è presente il corpo del messaggio.

Ci sono due tipi di intercettori: ReaderInterceptor e WriterInterceptor , e sono gli stessi sia per il server e lato client.

Successivamente, creeremo un'altra risorsa sul nostro server, a cui si accede tramite un POST e riceve un parametro nel corpo, quindi gli intercettori verranno eseguiti quando si accede ad esso:

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

Aggiungeremo anche un nuovo metodo al nostro client Jersey, per testare questa nuova risorsa:

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

4.1. Implementazione di un ReaderInterceptor

I Reader Interceptor ci consentono di manipolare i flussi in entrata, quindi possiamo usarli per modificare la richiesta sul lato server o la risposta sul lato client.

Creiamo un intercettore lato server per scrivere un messaggio personalizzato nel corpo della richiesta intercettata:

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

Notare che dobbiamo chiamare il metodo continue () per chiamare il successivo intercettore nella catena . Una volta eseguiti tutti gli intercettori, verrà chiamato il lettore del corpo del messaggio appropriato.

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.

Se non lo facciamo, il filtro verrà eseguito due volte: una volta come filtro globale e un'altra volta come filtro associato al metodo getHiGreeting () .

7. Conclusione

In questo tutorial, ci siamo concentrati sulla comprensione di come funzionano i filtri e gli intercettori in Jersey 2 e come possiamo usarli in un'applicazione web.

Come sempre, il codice sorgente completo per gli esempi è disponibile su GitHub.