Mẫu thiết kế Proxy: Kỹ thuật ủy quyền linh hoạt

Giới thiệu về Mẫu thiết kế Proxy

Mẫu thiết kế Proxy (Đại diện) là một mẫu thiết kế cấu trúc cho phép bạn cung cấp một đối tượng thay thế hoặc giữ chỗ cho một đối tượng khác. Proxy kiểm soát quyền truy cập vào đối tượng gốc, cho phép bạn thực hiện một số hành động bổ sung trước hoặc sau khi yêu cầu được chuyển đến đối tượng gốc. Mục đích chính là cung cấp một lớp gián tiếp, giúp kiểm soát, quản lý vòng đời hoặc thêm các chức năng như bảo mật, ghi nhật ký, bộ đệm mà không làm thay đổi đối tượng thực.

1. Proxy Tĩnh (Static Proxy)

Proxy tĩnh là dạng triển khai mà lớp proxy được định nghĩa rõ ràng trong mã nguồn. Mỗi đối tượng proxy được tạo ra thường có mối quan hệ một-một hoặc một-nhiều với đối tượng thực mà nó đại diện. Các lớp proxy tĩnh phải được tạo thủ công cho từng interface hoặc lớp cụ thể.

Hãy xem xét một ví dụ về một dịch vụ xử lý tài liệu. Chúng ta muốn thêm một lớp kiểm tra bảo mật trước khi tài liệu được xử lý bởi dịch vụ thực.

Giao diện dịch vụ

Đầu tiên, chúng ta định nghĩa một giao diện cho dịch vụ xử lý tài liệu.

interface DocumentProcessor {
    void processDocument(String docId);
}

Đối tượng dịch vụ thực tế

Đây là triển khai thực sự của dịch vụ xử lý tài liệu.

class RealDocumentProcessor implements DocumentProcessor {
    @Override
    public void processDocument(String docId) {
        System.out.println("Real Processor: Đang xử lý tài liệu ID: " + docId + "...");
        // Giả lập công việc xử lý tài liệu phức tạp
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Real Processor: Tài liệu ID: " + docId + " đã được xử lý hoàn tất.");
    }
}

Lớp Proxy Tĩnh

Lớp proxy này sẽ đóng vai trò là một người gác cổng, thực hiện kiểm tra bảo mật trước khi cho phép đối tượng thực xử lý tài liệu.

class SecureDocumentProxy implements DocumentProcessor {
    private DocumentProcessor realProcessor;
    private String userRole; // Vai trò của người dùng truy cập

    public SecureDocumentProxy(DocumentProcessor realProcessor, String userRole) {
        this.realProcessor = realProcessor;
        this.userRole = userRole;
    }

    @Override
    public void processDocument(String docId) {
        System.out.println("Proxy: Yêu cầu xử lý tài liệu ID: " + docId + " từ người dùng có vai trò: " + userRole);
        // Thực hiện các kiểm tra trước khi chuyển yêu cầu đến đối tượng thực
        if (checkAccess(userRole, docId)) {
            System.out.println("Proxy: Quyền truy cập được cấp. Chuyển yêu cầu đến đối tượng thực.");
            realProcessor.processDocument(docId); // Chuyển yêu cầu đến đối tượng thực
            System.out.println("Proxy: Ghi log sau khi xử lý tài liệu " + docId + ".");
        } else {
            System.out.println("Proxy: Truy cập bị TỪ CHỐI cho tài liệu " + docId + " bởi người dùng có vai trò " + userRole + ".");
        }
    }

    // Phương thức giả lập kiểm tra quyền truy cập
    private boolean checkAccess(String role, String docId) {
        // Ví dụ: chỉ admin và editor mới có thể xử lý các tài liệu không công khai
        if ("admin".equals(role)) {
            return true; // Admin có toàn quyền
        }
        if ("editor".equals(role) && !docId.startsWith("private-")) {
            return true; // Editor có thể xử lý tài liệu không phải "private-"
        }
        // Giả lập cho phép người dùng guest xem các tài liệu public
        return "guest".equals(role) && docId.startsWith("public-");
    }
}

Kiểm thử Proxy Tĩnh

Chương trình chính để minh họa cách sử dụng proxy tĩnh.

public class StaticProxyDemo {
    public static void main(String[] args) {
        System.out.println("--- Ví dụ Proxy Tĩnh ---");

        DocumentProcessor realProcessor = new RealDocumentProcessor();

        // Người dùng "admin" sử dụng proxy
        DocumentProcessor adminProxy = new SecureDocumentProxy(realProcessor, "admin");
        adminProxy.processDocument("private-project-report-Q3");
        System.out.println("--------------------");

        // Người dùng "editor" sử dụng proxy
        DocumentProcessor editorProxy = new SecureDocumentProxy(realProcessor, "editor");
        editorProxy.processDocument("public-article-draft");
        editorProxy.processDocument("private-internal-memo"); // Bị từ chối
        System.out.println("--------------------");

        // Người dùng "guest" sử dụng proxy
        DocumentProcessor guestProxy = new SecureDocumentProxy(realProcessor, "guest");
        guestProxy.processDocument("public-release-notes");
        guestProxy.processDocument("private-confidential-data"); // Bị từ chối
    }
}

Tóm tắt Proxy Tĩnh

  • Ưu điểm:
    • Dễ hiểu và triển khai.
    • Khả năng mở rộng chức năng của đối tượng mục tiêu mà vẫn tuân thủ Nguyên tắc Mở/Đóng (Open/Closed Principle).
    • Kiểm soát rõ ràng quyền truy cập vào đối tượng thực.
  • Nhược điểm:
    • Yêu cầu tạo một lớp proxy riêng biệt cho mỗi đối tượng dịch vụ, dẫn đến khối lượng công việc lớn và khó quản lý nếu có nhiều đối tượng hoặc nhiều loại proxy.
    • Nếu giao diện của đối tượng thực thay đổi, các lớp proxy tương ứng cũng cần được cập nhật, gây khó khăn trong việc bảo trì.
    • Khó áp dụng cho các tình huống mà đối tượng được ủy quyền không xác định trước.

2. Proxy Động (Dynamic Proxy)

Proxy động giải quyết nhược điểm của proxy tĩnh bằng cách tạo ra lớp proxy tại thời gian chạy (runtime) thay vì biên dịch trước. Trong Java, JDK cung cấp cơ chế Proxy động thông qua gói java.lang.reflect, sử dụng các interface Proxy và InvocationHandler.

Khi sử dụng proxy động, bạn chỉ cần định nghĩa một InvocationHandler để xử lý tất cả các lời gọi phương thức trên đối tượng proxy được tạo ra.

Giao diện và đối tượng dịch vụ thực tế

Chúng ta sẽ sử dụng lại giao diện DocumentProcessor và lớp RealDocumentProcessor từ ví dụ proxy tĩnh.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// Giao diện dịch vụ (tái sử dụng từ Proxy Tĩnh)
interface DocumentProcessor {
    void processDocument(String docId);
}

// Đối tượng dịch vụ thực tế (tái sử dụng từ Proxy Tĩnh)
class RealDocumentProcessor implements DocumentProcessor {
    @Override
    public void processDocument(String docId) {
        System.out.println("Real Processor: Đang xử lý tài liệu ID: " + docId + "...");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Real Processor: Tài liệu ID: " + docId + " đã được xử lý hoàn tất.");
    }
}

InvocationHandler tùy chỉnh

Chúng ta tạo một lớp triển khai InvocationHandler để định nghĩa logic sẽ được thực hiện khi bất kỳ phương thức nào của đối tượng proxy được gọi.

class DynamicSecurityHandler implements InvocationHandler {
    private Object targetService;
    private String currentUserRole; // Vai trò của người dùng hiện tại

    public DynamicSecurityHandler(Object targetService, String currentUserRole) {
        this.targetService = targetService;
        this.currentUserRole = currentUserRole;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Dynamic Proxy: User '" + currentUserRole + "' trying to execute method '" + method.getName() + "'...");

        // Logic kiểm tra bảo mật trước khi gọi phương thức thực
        if (checkPermission(currentUserRole, method.getName(), args)) {
            System.out.println("Dynamic Proxy: Quyền truy cập được cấp. Gọi phương thức thực.");
            // Gọi phương thức thực tế trên đối tượng mục tiêu
            Object result = method.invoke(targetService, args);
            System.out.println("Dynamic Proxy: Phương thức '" + method.getName() + "' đã thực thi xong.");
            return result;
        } else {
            System.err.println("Dynamic Proxy: Truy cập BỊ TỪ CHỐI cho phương thức '" + method.getName() + "' bởi người dùng '" + currentUserRole + "'.");
            throw new SecurityException("Unauthorized access: User " + currentUserRole + " cannot execute " + method.getName());
        }
    }

    // Giả lập logic kiểm tra quyền động
    private boolean checkPermission(String role, String methodName, Object[] methodArgs) {
        if ("admin".equals(role)) {
            return true; // Admin có toàn quyền
        }

        if ("processDocument".equals(methodName) && methodArgs != null && methodArgs.length > 0) {
            String docId = (String) methodArgs[0];
            // Editor có thể xử lý tài liệu không phải "private-"
            if ("editor".equals(role) && !docId.startsWith("private-")) {
                return true;
            }
            // Guest chỉ có thể xử lý tài liệu public
            if ("guest".equals(role) && docId.startsWith("public-")) {
                return true;
            }
        }
        return false;
    }
}

Factory để tạo Proxy động

Một lớp factory để tạo ra các đối tượng proxy động một cách dễ dàng.

class DynamicProxyFactory {
    @SuppressWarnings("unchecked")
    public static <T> T createSecureProxy(T target, String userRole) {
        // Proxy.newProxyInstance cần:
        // 1. ClassLoader của đối tượng mục tiêu
        // 2. Các interfaces mà đối tượng mục tiêu triển khai (để proxy cũng triển khai chúng)
        // 3. InvocationHandler để xử lý các lời gọi phương thức
        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new DynamicSecurityHandler(target, userRole)
        );
    }
}

Kiểm thử Proxy Động

Chương trình chính để minh họa cách sử dụng proxy động.

public class DynamicProxyDemo {
    public static void main(String[] args) {
        System.out.println("\n--- Ví dụ Proxy Động (JDK Proxy) ---");

        DocumentProcessor realProcessor = new RealDocumentProcessor();

        // Tạo proxy cho người dùng "admin"
        DocumentProcessor adminProxy = DynamicProxyFactory.createSecureProxy(realProcessor, "admin");
        try {
            adminProxy.processDocument("private-budget-2024");
            adminProxy.processDocument("public-roadmap");
        } catch (SecurityException e) {
            System.err.println("Admin Error: " + e.getMessage());
        }
        System.out.println("--------------------");

        // Tạo proxy cho người dùng "editor"
        DocumentProcessor editorProxy = DynamicProxyFactory.createSecureProxy(realProcessor, "editor");
        try {
            editorProxy.processDocument("public-blog-post-draft");
            editorProxy.processDocument("private-confidential-memo"); // Bị từ chối
        } catch (SecurityException e) {
            System.err.println("Editor Error: " + e.getMessage());
        }
        System.out.println("--------------------");

        // Tạo proxy cho người dùng "guest"
        DocumentProcessor guestProxy = DynamicProxyFactory.createSecureProxy(realProcessor, "guest");
        try {
            guestProxy.processDocument("public-company-news");
            guestProxy.processDocument("private-meeting-minutes"); // Bị từ chối
        } catch (SecurityException e) {
            System.err.println("Guest Error: " + e.getMessage());
        }
    }
}

Thẻ: Java design patterns Proxy Pattern Static Proxy Dynamic Proxy

Đăng vào ngày 20 tháng 10 lúc 18:05