Jersey-Filter und Abfangjäger

1. Einleitung

In diesem Artikel erklären wir, wie Filter und Interceptors im Jersey-Framework funktionieren und welche Hauptunterschiede zwischen diesen bestehen.

Wir werden hier Jersey 2 verwenden und unsere Anwendung mit einem Tomcat 9-Server testen.

2. Anwendungs-Setup

Erstellen wir zunächst eine einfache Ressource auf unserem Server:

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

Erstellen wir außerdem die entsprechende Serverkonfiguration für unsere Anwendung:

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

Wenn Sie mehr über das Erstellen einer API mit Jersey erfahren möchten, lesen Sie diesen Artikel.

Sie können sich auch unseren kundenorientierten Artikel ansehen und erfahren, wie Sie mit Jersey einen Java-Client erstellen.

3. Filter

Beginnen wir jetzt mit Filtern.

Mit Filtern können wir einfach die Eigenschaften von Anforderungen und Antworten ändern, z. B. HTTP-Header. Filter können sowohl auf Server- als auch auf Clientseite angewendet werden.

Beachten Sie, dass Filter immer ausgeführt werden, unabhängig davon, ob die Ressource gefunden wurde oder nicht.

3.1. Implementieren eines Anforderungsserverfilters

Beginnen wir mit den Filtern auf der Serverseite und erstellen einen Anforderungsfilter.

Dazu implementieren wir die ContainerRequestFilter- Schnittstelle und registrieren sie als Provider auf unserem 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()); } } }

Dieser einfache Filter lehnt nur die Anforderungen mit der Sprache "EN" in der Anforderung ab, indem er die abortWith () -Methode aufruft .

Wie das Beispiel zeigt, mussten wir nur eine Methode implementieren, die den Kontext der Anforderung empfängt und die wir nach Bedarf ändern können.

Beachten Sie, dass dieser Filter ausgeführt wird, nachdem die Ressource abgeglichen wurde.

Wenn wir vor dem Ressourcenabgleich einen Filter ausführen möchten, können wir einen Vorabgleichsfilter verwenden, indem wir unseren Filter mit der Annotation @PreMatching versehen :

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

Wenn wir jetzt versuchen, auf unsere Ressource zuzugreifen, können wir überprüfen, ob unser Pre-Matching-Filter zuerst ausgeführt wird:

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. Implementieren eines Response Server-Filters

Wir werden jetzt einen Antwortfilter auf der Serverseite implementieren, der der Antwort lediglich einen neuen Header hinzufügt.

Dazu muss unser Filter die ContainerResponseFilter- Schnittstelle implementieren und seine einzige Methode implementieren:

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

Beachten Sie, dass der Parameter ContainerRequestContext nur als schreibgeschützt verwendet wird, da wir die Antwort bereits verarbeiten.

2.3. Implementieren eines Clientfilters

Wir werden jetzt mit Filtern auf der Client-Seite arbeiten. Diese Filter funktionieren genauso wie Serverfilter, und die Schnittstellen, die wir implementieren müssen, sind denen für die Serverseite sehr ähnlich.

Lassen Sie es uns mit einem Filter in Aktion sehen, der der Anforderung eine Eigenschaft hinzufügt:

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

Erstellen wir auch einen Jersey-Client, um diesen Filter zu testen:

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

Beachten Sie, dass wir den Filter zur Client-Konfiguration hinzufügen müssen, um ihn zu registrieren.

Schließlich erstellen wir auch einen Filter für die Antwort im Client.

This works in a very similar way as the one in the server, but implementing the ClientResponseFilter interface:

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

Again, the ClientRequestContext is for read-only purposes.

4. Interceptors

Interceptors are more connected with the marshalling and unmarshalling of the HTTP message bodies that are contained in the requests and the responses. They can be used both in the server and in the client side.

Keep in mind that they're executed after the filters and only if a message body is present.

There are two types of interceptors: ReaderInterceptor and WriterInterceptor, and they are the same for both the server and the client side.

Next, we're going to create another resource on our server – which is accessed via a POST and receives a parameter in the body, so interceptors will be executed when accessing it:

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

We'll also add a new method to our Jersey client – to test this new resource:

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

4.1. Implementing a ReaderInterceptor

Reader interceptors allow us to manipulate inbound streams, so we can use them to modify the request on the server side or the response on the client side.

Let's create an interceptor on the server side to write a custom message in the body of the request intercepted:

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

Notice that we have to call the proceed() methodto call the next interceptor in the chain. Once all the interceptors are executed, the appropriate message body reader will be called.

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.

Wenn wir dies nicht tun, wird der Filter zweimal ausgeführt: einmal als globaler Filter und ein anderes Mal als Filter, der an die Methode getHiGreeting () gebunden ist .

7. Fazit

In diesem Tutorial haben wir uns darauf konzentriert zu verstehen, wie Filter und Interceptors in Jersey 2 funktionieren und wie wir sie in einer Webanwendung verwenden können.

Wie immer ist der vollständige Quellcode für die Beispiele auf GitHub verfügbar.