Rest Web service: Filter và Interceptor với Jersey 2.x (P2)

Ở bài viết trước, chúng ta đã cùng tìm hiểu về Filter. Trong phần này, chúng ta sẽ tìm hiểu tiếp Interceptor và một số cấu hình khác của Jersey.

1. Jersey Interceptor

Interceptor có thể được sử dụng ở cả 2 phía Jersey Server và Jersey Client. Interceptor chủ yếu được sử dụng để chỉnh sửa data (entity) của input/ output stream. Một ví dụ về trường hợp sử dụng của Intercepter là zip/ unzip các data (entity) của input/ output stream.

Interceptor tham gia trong quá trình marshalling và unmarshalling HTTP message body có trong các request/ response.

Lưu ý: Intercepter được thực thi sau Filter và message body tồn tại.

Có 2 loại Interceptor:

  • ReaderInterceptor : được sử dụng để xử lý inbound entity stream ở phía Server và response entity stream ở phía Client.
  • WriterInterceptor : được sử dụng để xử lý outbound response entity ở phía Server và request entity stream ở phía Client.

1.1. Tạo WriterInterceptor

Tạo WriterInterceptor để wrap OutputStream bởi một GZIPOutputStream và gửi lên Server.

GZIPWriterInterceptor.java

package com.maixuanviet.interceptor;
 
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;
 
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
 
public class GZIPWriterInterceptor implements WriterInterceptor {
 
    @Override
    public void aroundWriteTo(WriterInterceptorContext context) throws IOException {
        final OutputStream outputStream = context.getOutputStream();
        context.setOutputStream(new GZIPOutputStream(outputStream));
        context.proceed();
    }
}

1.2. Tạo ReaderInterceptor

Tạo ReaderInterceptor để wrap InputStream bởi một GZIPInputStream để đọc data đã được compress từ Client gửi lên.

GZIPReaderInterceptor.java

package com.maixuanviet.interceptor;
 
import java.io.IOException;
import java.io.InputStream;
 
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.ReaderInterceptorContext;
 
import com.maixuanviet.util.ZipUtil;
 
@Provider
public class GZIPReaderInterceptor implements ReaderInterceptor {
 
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException {
        final InputStream originalInputStream = context.getInputStream();
        context.setInputStream(ZipUtil.decompressStream(originalInputStream));
        return context.proceed();
    }
}

Lưu ý: Chúng ta sử dụng @Provider để đăng ký Interceptor này với Jersey.

Lớp ZipUtil.java hỗ trợ decompress data đã được gzip:

package com.maixuanviet.util;
 
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.io.RandomAccessFile;
import java.util.zip.GZIPInputStream;
 
public class ZipUtil {
 
    private ZipUtil() {
        super();
    }
 
    /**
     * The method to decompress a input stream if a input stream is a gzip archive.
     * 
     * @param in the input stream to be decompressed.
     * @return
     */
    public static InputStream decompressStream(InputStream input) {
        try {
            // we need a pushbackstream to look ahead
            PushbackInputStream pb = new PushbackInputStream(input, 2);
            byte[] signature = new byte[2];
            int len = pb.read(signature); // read the signature
            pb.unread(signature, 0, len); // push back the signature to the stream
            if (signature[0] == (byte) 0x1f && signature[1] == (byte) 0x8b) {
                // check if matches standard gzip magic number
                return new GZIPInputStream(pb);
            }
            return pb;
        } catch (Exception e) {
            return input;
        }
    }
}

Test Interceptor sử dụng Jersey Client

InterceptorClient.java

package com.maixuanviet.client;
 
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
 
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.logging.LoggingFeature;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.maixuanviet.interceptor.GZIPWriterInterceptor;
import com.maixuanviet.model.Order;
 
public class InterceptorClient {
    public static final String API_URL = "http://localhost:8080/RestfulWebServiceExample/rest/orders";
 
    public static void main(String[] args) {
        ClientConfig clientConfig = new ClientConfig();
 
        // Config logging for client side
        clientConfig.register( //
                new LoggingFeature( //
                        Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), //
                        Level.INFO, //
                        LoggingFeature.Verbosity.PAYLOAD_ANY, //
                        10000));
 
        // Config filters
        clientConfig.register(new GZIPWriterInterceptor());
 
        String payload = convertToJson(new Order(1, "maixuanviet"));
 
        Client client = ClientBuilder.newClient(clientConfig);
        Response response = client.target(API_URL).request(MediaType.APPLICATION_JSON_TYPE)
                .post(Entity.entity(payload, MediaType.APPLICATION_JSON));
        System.out.println(response.readEntity(String.class));
    }
 
    private static String convertToJson(Order order) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(order);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Chạy chương trình trên chúng ta có kết quả như sau:

Console ở Client:

Console ở Server:

Như bạn thấy, dữ liệu ở Client gửi lên, và ở server nhận được đều đã được compress/ decompress với gzip.

Các bạn có thể, bỏ đoạn code đăng ký GZIPWriterInterceptor ở Client. Sau đó chạy lại, bạn sẽ sẽ thấy dữ liệu gửi và nhận ở dạng plain/text như sau:

2. Thứ tự thực thi Filter và Interceptor

Thứ tự thực thi các Filter và Interceptor được minh họa như hình sau:

3. Name binding

Mặc định, các Filter và Interceptor sẽ được áp dụng trên tất cả các resource method. Chúng ta, có thể chỉ định Filter chỉ áp dụng trên một vài resource method bằng cách sử dụng Name binding.

Để làm được điều này, chúng tạo một Annotation và đánh dấu với Annotation @NameBinding. Sau đó, sử dụng Annotation này với resource method cần áp dụng Filter được chỉ định.

Ví dụ:

Tạo một Annotation để đánh dấu binding cho Filter Compress data với các resource method trả về kích thước dữ liệu lớn:

Compress.java

package com.maixuanviet.annotation;
 
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
import javax.ws.rs.NameBinding;
 
// @Compress annotation is the name binding annotation
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface Compress {
}

Tạo Rest Web service:

package com.maixuanviet.api;
 
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
 
import com.maixuanviet.annotation.Compress;
 
//URI:
//http(s)://<domain>:(port)/<YourApplicationName>/<UrlPattern in web.xml>/<path>
//http://localhost:8080/RestfulWebServiceExample/rest/compresses
@Path("/compresses")
public class HeavyService {
 
    @GET
    @Path("normal-data")
    public String getNormalData() {
        return "Normal data";
    }
 
    @GET
    @Path("big-data")
    @Compress
    public String getBigData() {
        return "Big data";
    }
 
    @GET
    @Path("dynamic-data")
    public String getDynamicData() {
        return "Dynamic data";
    }
}

Test ứng dụng:

  • /normal-data : dữ liệu trả về dạng plain-text.
  • /big-data : dữ liệu trả về đã được compress ở định dạng gzip.
  • /dynamic-data : dữ liệu trả về dạng plain-text.

4. Dynamic binding

Ngoài sử dụng custom Annotation @NameBinding, chúng ta có thể áp dụng một Filter trên một số resource method nếu nó thõa mãn một số điều kiện nhất định.

Ví dụ:

Tạo DynamicFeature để kiểm tra nếu tên resource method có chứa “DynamicData” thì sẽ sử dụng GZIPWriterInterceptor.

package com.maixuanviet.interceptor;
 
import javax.ws.rs.container.DynamicFeature;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.ext.Provider;
 
import com.maixuanviet.api.HeavyService;
 
//This dynamic binding provider registers GZIPWriterInterceptor
//only for HelloWorldResource and methods that contain
//"VeryLongString" in their name. It will be executed during
//application initialization phase.
@Provider
public class CompressionDynamicBinding implements DynamicFeature {
 
    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        if (HeavyService.class.equals(resourceInfo.getResourceClass())
                && resourceInfo.getResourceMethod().getName().contains("DynamicData")) {
            context.register(GZIPWriterInterceptor.class);
        }
    }
}

Chạy lại ứng dụng trên, chúng ta có kết quả như sau:

  • /normal-data : dữ liệu trả về dạng plain-text.
  • /big-data : dữ liệu trả về đã được compress ở định dạng gzip.
  • /dynamic-data : dữ liệu trả về đã được compress ở định dạng gzip.

5. Priorities

Trong trường hợp đăng ký nhiều Filter và Interceptor, chúng ta mong muốn ứng dụng thực thi các Filter và Interceptor theo đúng thứ tự được chỉ định. Để làm được điều này, chúng ta sẽ sử dụng Annotation @Priority(number) trên các Fitler và Interceptor class.

  • ContainerRequestFilter, ClientRequestFilter, ReaderInterceptor, WriterInterceptor : được sắp thứ tự ưu tiên tăng dần. Số nhỏ được thực thi trước.
  • ContainerResponseFilter, ClientResponseFilter : được sắp thứ tự ưu tiên giảm dần. Số lớn được thực thi trước.

Ví dụ:

Tạo REST web service:

package com.maixuanviet.api;
 
import javax.ws.rs.POST;
import javax.ws.rs.Path;
 
//URI:
//http(s)://<domain>:(port)/<YourApplicationName>/<UrlPattern in web.xml>/<path>
//http://localhost:8080/RestfulWebServiceExample/rest/priorities
@Path("/priorities")
public class PrioprityService {
 
    @POST
    public String create(String data) {
        return "Created " + data;
    }
}

Chúng ta có các Filter sau:

package com.maixuanviet.filter;
 
import java.io.IOException;
 
import javax.annotation.Priority;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.ext.Provider;
 
@Priority(10)
@Provider
public class Priority1RequestFilter implements ContainerRequestFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("#1 Priority1RequestFilter running ... ");
    }
}
@Priority(20)
@Provider
public class Priority2RequestFilter implements ContainerRequestFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("#2 Priority2RequestFilter running ... ");
    }
}
@Priority(10)
@Provider
public class Priority1ResponseFilter implements ContainerResponseFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException {
        System.out.println("#1 Priority1ResponseFilter running ... ");
    }
}
@Priority(20)
@Provider
public class Priority2ResponseFilter implements ContainerResponseFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException {
        System.out.println("#2 Priority2ResponseFilter running ... ");
    }
}

Và các Interceptor sau:

package com.maixuanviet.interceptor;
 
import java.io.IOException;
 
import javax.annotation.Priority;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
 
@Priority(10)
@Provider
public class Priority1WriterInterceptor implements WriterInterceptor {
 
    @Override
    public void aroundWriteTo(WriterInterceptorContext context) throws IOException {
        System.out.println("#1 Priority1WriterInterceptor running ... ");
        context.proceed();
    }
}
@Priority(20)
@Provider
public class Priority2WriterInterceptor implements WriterInterceptor {
 
    @Override
    public void aroundWriteTo(WriterInterceptorContext context) throws IOException {
        System.out.println("#2 Priority2WriterInterceptor running ... ");
        context.proceed();
    }
}
@Priority(10)
@Provider
public class Priority1ReaderInterceptor implements ReaderInterceptor {
 
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException {
        System.out.println("#1 Priority1ReaderInterceptor running ... ");
        return context.proceed();
    }
}
@Priority(20)
@Provider
public class Priority2ReaderInterceptor implements ReaderInterceptor {
 
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException {
        System.out.println("#2 Priority1ReaderInterceptor running ... ");
        return context.proceed();
    }
}

Test API trên, chúng ta có kết quả sau:

Trên đây là toàn bộ những lý thuyết và ví dụ cơ bản về sử dụng Filter và Interceptor với Jersey. Đây là phần rất là hay, nếu áp dụng tốt sẽ giúp chúng ta đỡ tốn công sức rất là nhiều. Mọi thứ đều được xử lý một cách tự động và thống nhất trên tất cả request/ response. Hy vọng bài viết giúp ích cho các bạn, hẹn gặp lại ở các bài viết tiếp theo!