Filter dan Interceptor Jersey

1. Perkenalan

Pada artikel ini, kami akan menjelaskan cara kerja filter dan interseptor dalam kerangka kerja Jersey, serta perbedaan utama di antara keduanya.

Kami akan menggunakan Jersey 2 di sini, dan kami akan menguji aplikasi kami menggunakan server Tomcat 9.

2. Pengaturan Aplikasi

Mari pertama-tama buat sumber daya sederhana di server kita:

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

Juga, mari buat konfigurasi server yang sesuai untuk aplikasi kita:

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

Jika Anda ingin menggali lebih dalam tentang cara membuat API dengan Jersey, Anda dapat melihat artikel ini.

Anda juga dapat melihat artikel yang berfokus pada klien kami dan mempelajari cara membuat klien Java dengan Jersey.

3. Filter

Sekarang, mari kita mulai dengan filter.

Sederhananya, filter memungkinkan kita mengubah properti permintaan dan tanggapan - misalnya, header HTTP. Filter dapat diterapkan di sisi server dan klien.

Ingatlah bahwa filter selalu dijalankan, terlepas dari apakah sumber daya ditemukan atau tidak.

3.1. Menerapkan Filter Server Permintaan

Mari kita mulai dengan filter di sisi server dan membuat filter permintaan.

Kami akan melakukannya dengan mengimplementasikan antarmuka ContainerRequestFilter dan mendaftarkannya sebagai Penyedia di server kami:

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

Filter sederhana ini hanya menolak permintaan dengan bahasa "EN" dalam permintaan dengan memanggil metode abortWith () .

Seperti yang ditunjukkan contoh, kita harus mengimplementasikan hanya satu metode yang menerima konteks permintaan, yang bisa kita modifikasi sesuai kebutuhan.

Ingatlah bahwa filter ini dijalankan setelah sumber daya cocok.

Jika kita ingin mengeksekusi filter sebelum pencocokan sumber daya, kita dapat menggunakan filter pra-pencocokan dengan memberi anotasi pada filter kita dengan anotasi @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"); } } }

Jika kami mencoba mengakses sumber daya kami sekarang, kami dapat memeriksa apakah filter pra-pencocokan kami dijalankan terlebih dahulu:

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. Menerapkan Filter Server Respons

Kami sekarang akan menerapkan filter respons di sisi server yang hanya akan menambahkan header baru ke respons.

Untuk melakukan itu, filter kita harus menerapkan antarmuka ContainerResponseFilter dan menerapkan satu-satunya metode:

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

Perhatikan bahwa parameter ContainerRequestContext hanya digunakan sebagai baca-saja - karena kita sudah memproses responsnya.

2.3. Menerapkan Filter Klien

Kami sekarang akan bekerja dengan filter di sisi klien. Filter ini bekerja dengan cara yang sama seperti filter server, dan antarmuka yang harus kita terapkan sangat mirip dengan yang ada di sisi server.

Mari kita lihat beraksi dengan filter yang menambahkan properti ke permintaan:

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

Mari kita juga membuat klien Jersey untuk menguji filter ini:

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

Perhatikan bahwa kita harus menambahkan filter ke konfigurasi klien untuk mendaftarkannya.

Terakhir, kami juga akan membuat filter untuk respons di klien.

Ini bekerja dengan cara yang sangat mirip dengan yang ada di server, tetapi menerapkan antarmuka 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"); } }

Sekali lagi, ClientRequestContext adalah untuk tujuan baca-saja.

4. Penangkap

Interceptors lebih terhubung dengan marshalling dan unmarshalling badan pesan HTTP yang terdapat dalam permintaan dan respons. Mereka dapat digunakan baik di server maupun di sisi klien.

Perlu diingat bahwa mereka dijalankan setelah filter dan hanya jika badan pesan ada.

Ada dua jenis interseptor: ReaderInterceptor dan WriterInterceptor , dan keduanya sama untuk server dan sisi klien.

Selanjutnya, kita akan membuat sumber daya lain di server kita - yang diakses melalui POST dan menerima parameter di dalam tubuh, sehingga pencegat akan dieksekusi saat mengaksesnya:

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

Kami juga akan menambahkan metode baru ke klien Jersey kami - untuk menguji sumber daya baru ini:

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

4.1. Menerapkan ReaderInterceptor

Pencegat pembaca memungkinkan kita memanipulasi aliran masuk, sehingga kita dapat menggunakannya untuk mengubah permintaan di sisi server atau respons di sisi klien.

Mari buat interseptor di sisi server untuk menulis pesan khusus di badan permintaan yang dicegat:

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

Perhatikan bahwa kita harus memanggil melanjutkan () metode untuk memanggil pencegat berikutnya dalam rantai . Setelah semua interseptor dieksekusi, pembaca isi pesan yang sesuai akan dipanggil.

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.

Jika kita tidak melakukan ini, filter akan dijalankan dua kali: satu kali sebagai filter global dan satu kali sebagai filter yang terikat ke metode getHiGreeting () .

7. Kesimpulan

Dalam tutorial ini, kami fokus pada pemahaman bagaimana filter dan interseptor bekerja di Jersey 2 dan bagaimana kami dapat menggunakannya dalam aplikasi web.

Seperti biasa, kode sumber lengkap untuk contoh tersedia di GitHub.