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

Trong bài này, chúng ta sẽ cùng tìm hiểu về Filter, Interceptor và một số cấu hình của Jersey 2.x. Filter và Interceptor có thể được sử dụng ở cả 2 phía Jersey Server và Jersey Client. Filter có thể được sử dụng để chỉnh sửa các dữ liệu đầu vào/ đầu ra từ các request và response, bao gồm: header, data (entity), parameter. 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.

1. Jersey Filter

Như đã giới thiệu, Filter có thể được sử dụng ở cả 2 phía Jersey Server và Jersey Client. Nó có thể được sử dụng để chỉnh sửa các dữ liệu đầu vào/ đầu ra (inbound/ outbound) từ các request và response, bao gồm: header, data (entity), parameter.

Jersey Server filter:

  • ContainerRequestFilter : sử dụng filter này khi cần thực hiện một số hành động trước khi resource method được thực thi. Chẳng hạn, chứng thực user trước khi truy cập resource.
  • ContainerResponseFilter: sử dụng filter này khi cần thực hiện một số hành động trước khi trả kết quả về client. Chẳng hạn, thêm một số thông header gửi về Client.
  • ContainerRequestFilter với Annotation @PreMatching:
    • ContainerRequestFilter mặc định là một post-matching filter, nghĩa là filter được áp dụng chỉ sau khi đã tìm thấy một resource method phù hợp để xử lý request, tức là sau request matching thực thi. Request matching là quá trình tìm resource method để thực thi dựa trên Path, HTTP Method và một và parameter khác. Khi post-matching request filter được gọi, các filter khác sẽ không làm ảnh hưởng đến xử lý resource method matching.
    • @PreMatching : là một request filter được thực thi trước khi request matching bắt đầu. Vì vậy nó có thể làm ảnh hưởng đến kết quả tìm kiếm một resource method thực thi. Chẳng hạn, chúng ta có thể thay đổi request method của Client từ PUT sang POST.

Jersey Client Filter:

  • ClientRequestFilter : sử dụng filter này khi cần thực hiện một số hành động trước khi gửi request lên server. Chẳng hạn, thêm token đã chứng thực lên server.
  • ClientResponseFilter : sử dụng filter này khi cần thực hiện một số hành động sau khi đã nhận response từ server trước khi phương thức ở Client nhận kết quả.

1.1. Tạo REST Web service

OrderService.java

package com.maixuanviet.api;
 
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
 
import com.maixuanviet.model.Order;
 
// URI:
// http(s)://<domain>:(port)/<YourApplicationName>/<UrlPattern in web.xml>/<path>
// http://localhost:8080/RestfulWebServiceExample/rest/orders
@Path("/orders")
public class OrderService {
 
    @GET
    @Path("/{id}")
    public Response get(@PathParam("id") int id) {
        System.out.println("OrderService#get()");
        return Response.ok("OrderService#get()").build();
    }
 
    @POST
    public Response insert(Order order) {
        System.out.println("OrderService#insert()");
        return Response.ok("OrderService#insert()").build();
    }
 
    @PUT
    public Response update(Order order) {
        System.out.println("OrderService#update()");
        return Response.ok("OrderService#update()").build();
    }
 
    @DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") int id) {
        System.out.println("OrderService#delete()");
        return Response.ok("OrderService#delete()").build();
    }
}

1.2. Tạo Server Filter

Tạo ContainerRequestFilter chứng thực user: nếu HTTP request là DELETE thì cần được chứng thực. Nếu chưa được chứng thực thì sẽ trả về Client status Response.Status.UNAUTHORIZED.

AuthorizationRequestFilter.java

package com.maixuanviet.filter;
 
import java.io.IOException;
 
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
 
@Provider
public class AuthorizationRequestFilter implements ContainerRequestFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("AuthorizationRequestFilter running ... ");
 
        // Must be logged-in to perform the delete action
        if ("DELETE".equals(requestContext.getMethod()) 
                && !hasToken(requestContext)) {
            Response response = Response.status(Response.Status.UNAUTHORIZED) //
                    .entity("User cannot access the resource.") //
                    .build();
            requestContext.abortWith(response);
        }
    }
 
    private boolean hasToken(ContainerRequestContext requestContext) {
        // Extract Authorization header details
        String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer")) {
            return false;
        }
        // Extract the token
        String token = authorizationHeader.substring("Bearer".length()).trim();
        System.out.println("token: " + token);
        return token != null && token.trim().length() > 0;
    }
}

Lưu ý: chúng ta cần sử dụng @Provider hoặc gọi phương thức register() trong ResourceConfig để đăng ký với Jersey sử dụng Filter này.

Tạo ContainerResponseFilter thêm một vài thông tin header trước khi trả kết quả về Client:

PoweredByResponseFilter.java

package com.maixuanviet.filter;
 
import java.io.IOException;
 
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
 
@Provider
public class PoweredByResponseFilter implements ContainerResponseFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException {
        System.out.println("PoweredByResponseFilter running ... ");
        responseContext.getHeaders().add("X-Api-Version", "2.x");
        responseContext.getHeaders().add("X-Powered-By", "api.maixuanviet.com");
    }
}

Tạo @PreMatching ContainerRequestFilter để tự động chuyển hướng request của Client từ HTTP PUT sang HTTP POST.

PreMatchingFilter.java

package com.maixuanviet.filter;
 
import java.io.IOException;
 
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.ext.Provider;
 
@Provider
@PreMatching
public class PreMatchingFilter implements ContainerRequestFilter {
 
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        System.out.println("PreMatchingFilter running ... ");
        if (requestContext.getMethod().equals("PUT")) {
            System.out.println("Change PUT methods to POST");
            requestContext.setMethod("POST");
        }
    }
}

1.3. Test Server Filter

Test @GET http://localhost:8080/RestfulWebServiceExample/rest/orders/1

Mở browser và truy cập địa chỉ: http://localhost:8080/RestfulWebServiceExample/rest/orders/1

Mở cửa sổ console của server, bạn sẽ thấy thứ tự thực thi của các filter và request/ response như sau:

Test @PUT http://localhost:8080/RestfulWebServiceExample/rest/orders

Như bạn thấy, request từ Client là PUT, nhưng được filter chuyển sang POST.

Test @DELETE http://localhost:8080/RestfulWebServiceExample/rest/orders/1

Như bạn thấy, request đã không được chấp nhận và response trả về với status Status.UNAUTHORIZED.

1.4. Tạo Client Filter

Tạo ClientRequestFilter để tự động thêm Token đã được chứng thực trước khi gửi request lên server.

CheckClientRequestFilter.java

package com.maixuanviet.filter;
 
import java.io.IOException;
 
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.HttpHeaders;
 
public class CheckClientRequestFilter implements ClientRequestFilter {
 
    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        System.out.println("CheckClientRequestFilter running ... ");
        String authorization = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
        if (authorization == null || authorization.trim().isEmpty()) {
            requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer maixuanviet-token");
        }
    }
}

Tạo ClientResponseFilter để thêm thông tin header sau khi nhận được response từ server.

CheckClientResponseFilter.java

package com.maixuanviet.filter;
 
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
 
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
 
public class CheckClientResponseFilter implements ClientResponseFilter {
 
    @Override
    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) {
        System.out.println("CheckClientResponseFilter running ... ");
        responseContext.getHeaders().add("X-Test-Client", "maixuanviet client filter");
        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy hh:mm:ss")));
    }
}

1.5. Test Client Filter

Trong ví dụ này, tôi sẽ tạo request HTTP DELETE. API delete trên server yêu cầu xác thực mới được thực thi. Do đó, phía Client cần phải gửi thông tin token đã xác thực. Chúng ta sẽ xem 2 cách gửi thông tin xác thực như sau:

  • Sử dụng Invocation.Builder để thêm header.
  • Sử dụng Filter
package com.maixuanviet.client;
 
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.Invocation;
import javax.ws.rs.client.WebTarget;
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.maixuanviet.filter.CheckClientRequestFilter;
import com.maixuanviet.filter.CheckClientResponseFilter;
 
public class OrderServiceClient {
 
    public static final String API_URL = "http://localhost:8080/RestfulWebServiceExample/rest/orders";
    public static final String API_TOKEN = "maixuanviet-token";
 
    public static void main(String[] args) {
        callApiWithoutFilter();
        System.out.println("------");
        callApiWithFilter();
    }
 
    public static void callApiWithFilter() {
        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 CheckClientRequestFilter());
        clientConfig.register(new CheckClientResponseFilter());
 
        Client client = ClientBuilder.newClient(clientConfig);
        WebTarget target = client.target(API_URL).path("1");
 
        Invocation.Builder invocationBuilder = target.request(MediaType.APPLICATION_JSON_TYPE);
        // Don't need to add this line because it's already added automatically by CheckClientRequestFilter
        // invocationBuilder.header("Authorization", "Bearer " + API_TOKEN);
 
        final Response response = invocationBuilder.delete();
        System.out.println("Header added by CheckClientResponseFilter: " + response.getHeaderString("X-Test-Client"));
        System.out.println("Call delete() successful with the result: " + response.readEntity(String.class));
    }
 
    public static void callApiWithoutFilter() {
        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));
 
        Client client = ClientBuilder.newClient(clientConfig);
        WebTarget target = client.target(API_URL).path("1");
 
        Invocation.Builder invocationBuilder = target.request(MediaType.APPLICATION_JSON_TYPE);
        invocationBuilder.header("Authorization", "Bearer " + API_TOKEN);
 
        String result = invocationBuilder.delete(String.class);
        System.out.println("Call delete() successful with the result: " + result);
    }
}

Chạy ứng dụng Client, chúng ta có kết quả như sau:

Console ở Client:

Console ở Server:

Related posts:

Java Program to Search for an Element in a Binary Search Tree
Java Program to Find Number of Articulation points in a Graph
Java Program to Generate Random Partition out of a Given Set of Numbers or Characters
Java Program to Check for balanced parenthesis by using Stacks
Java Program to Implement the Schonhage-Strassen Algorithm for Multiplication of Two Numbers
Tips for dealing with HTTP-related problems
Spring Data JPA Delete and Relationships
Java Program to Implement Fermat Primality Test Algorithm
Migrating from JUnit 4 to JUnit 5
Hướng dẫn Java Design Pattern – Proxy
Java Program to Implement Control Table
Working with Kotlin and JPA
Java Program to Implement Binary Tree
Enum trong java
Mảng (Array) trong Java
Hướng dẫn kết nối cơ sở dữ liệu với Java JDBC
Từ khóa this và super trong Java
Converting Between a List and a Set in Java
Java Program to Perform Inorder Non-Recursive Traversal of a Given Binary Tree
XML Serialization and Deserialization with Jackson
Java Program to Implement Strassen Algorithm
Custom HTTP Header with the HttpClient
Jackson Ignore Properties on Marshalling
Java Program to Implement Self organizing List
Java Program to Implement Johnson’s Algorithm
String Processing with Apache Commons Lang 3
Java Program to find the number of occurrences of a given number using Binary Search approach
How to Use if/else Logic in Java 8 Streams
Period and Duration in Java
String Joiner trong Java 8
A Guide to Spring Boot Admin
Using a List of Values in a JdbcTemplate IN Clause