Java Security - Dynamic Proxy

8.9k words

Lời nói đầu

Trước khi đến với Dynamic Proxy, ta sẽ tìm hiểu Proxy Pattern là gì ?

I. Java Proxy Pattern

Để nói về proxy pattern, trước hết ta cần hiểu khái niệm proxy (đối tượng đại diện/trung gian). Trong hình minh họa bên dưới, đối tượng trung gian (hoặc người đại diện) chính là thứ mà chúng ta gọi là proxy.

(Lưu ý: Hình ảnh có thể minh họa kịch bản người thuê nhà → người môi giới/đại diện → chủ nhà.)

Trong Java, Proxy Pattern là một mẫu thiết kế cấu trúc (structural design pattern), nó cung cấp một đối tượng thay thế hoặc đối tượng giữ chỗ (surrogate/placeholder) để kiểm soát việc truy cập tới một đối tượng khác.

!image.png

Giải thích:

  • Người thuê nhà (tenant) chính là bạn – người sử dụng.
  • Người môi giới (agent) đóng vai trò là proxy (đối tượng trung gian).
  • Người môi giới thay mặt bạn làm việc với chủ nhà (landlord – đối tượng thật) và giúp bạn tiếp cận căn hộ cho thuê.
  • Mô hình này phản ánh Proxy Design Pattern trong Java, trong đó đối tượng proxy kiểm soát quyền truy cập tới đối tượng thật.

II. Static Proxy (Proxy tĩnh)

2.1. Hiểu Static Proxy một cách đơn giản

Hãy dùng ví dụ:

Người thuê nhà muốn thuê một căn hộ thông qua người môi giới, và người môi giới sẽ làm việc trực tiếp với chủ nhà.

Để hiện thực ví dụ này trong Java, chúng ta cần 4 file:

  • Rental interface – đại diện cho khái niệm trừu tượng của việc thuê nhà
  • Landlord class – chủ nhà, đối tượng thật
  • Agent class (Proxy) – người môi giới, đối tượng proxy
  • Tenant class (Client) – người thuê nhà, phía client

Một số người có thể thắc mắc vì sao rental property lại được định nghĩa dưới dạng interface.

Điều này phù hợp với các nguyên lý thiết kế của Java – có thể hiểu tương tự như pure virtual function trong C++.

Bạn cũng có thể xem video của Kuang Shen để có giải thích sâu hơn về static proxy.

Rent.java – Interface Rent

1
2
3
4
5
6
package src.JdkProxy.StaticProxy;

// Interface for renting a property
public interface Rent {
void rent();
}
  • Interface này đại diện cho khái niệm thuê nhà (rental property).
  • Nó định nghĩa method rent(), mô tả hành vi thuê mà các class triển khai interface này phải thực hiện.

Host.java - The Landlord Class

1
2
3
4
5
6
7
8
package src.JdkProxy.StaticProxy;

public class Host implements Rent {

public void rent() {
System.out.println("The landlord wants to rent out the property.");
}
}
  • Class này đại diện cho landlord (chủ nhà).
  • Landlord implements interface Rent và cung cấp implementation thực tế cho method rent().
  • Nói cách khác, đây là real object trong Proxy Pattern.

Proxy.java – The Agent (Proxy) Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package src.JdkProxy.StaticProxy;

// The Agent
public class Proxy {

private Host host;

public Proxy() {}

public Proxy(Host host) {
this.host = host;
}

public void rent() {
host.rent();
}
}
  • Class này đại diện cho agent (proxy object).
    • Proxy không extends Host.
    • Thay vào đó, nó giữ một reference private tới object Host
    • Khi client gọi proxy.rent(), proxy sẽ delegate lời gọi này sang host.rent()
  • Đây chính là bản chất của Static Proxy:

👉 logic được chuyển tiếp (delegation) sang real object.

Client.java – The Tenant (Client)

1
2
3
4
5
6
7
8
9
10
package src.JdkProxy.StaticProxy;

// Entry point
public class Client {
public static void main(String[] args) {
Host host = new Host();
Proxy proxy = new Proxy(host);
proxy.rent();
}
}

Đây là startup class, đóng vai trò là client (tenant).

  • Client không làm việc trực tiếp với Host
  • Thay vào đó, client thông qua Proxy
  • Proxy chịu trách nhiệm gọi method của real object

Kết luận

Với setup này, logic “tenant xem nhà thông qua agent” đã được hoàn thiện:

  • Client (tenant) → gọi Proxy (agent)
  • Proxy → kiểm soát và chuyển tiếp request
  • Host (landlord) → thực hiện logic thực tế

Đây chính là Static Proxy Pattern trong Java, nơi:

  • Proxy và Real Object được xác định tại compile time
  • Proxy có thể dễ dàng mở rộng logic (logging, security, validation, transaction, …) mà không sửa code của Host

!image.png

Code Comment:

1
2
// 启动器
// Launcher (or Entry Point)

👉 Comment này có nghĩa là class khởi động chương trình, nơi chương trình bắt đầu chạy (entry point).

Console Output:

1
2
房东要出租房子!
The landlord wants to rent out the house!

👉 Đây là output được in ra trên console khi method rent() của Host được gọi thông qua Proxy.


Nhưng quy trình thuê nhà chỉ kết thúc như vậy thôi sao?

👉 Tất nhiên là chưa.

Bởi vì agent (proxy) vẫn cần phải thu phí môi giới (agency fee).

Ngoài ra, có một số hành động mà:

  • agent có thể làm
  • nhưng landlord không làm trực tiếp

Ví dụ:

  • dẫn khách đi xem nhà (showing the house)
  • thu phí môi giới (collecting agency fees)
  • xử lý giấy tờ, hợp đồng, v.v.

Những hành vi này không thuộc trách nhiệm của landlord, nên không nên đặt trong class Host.


Giải pháp

👉 Chúng ta sẽ implement các hành vi bổ sung này trong Proxy.java.

Điều này thể hiện đúng tinh thần của Proxy Pattern:

  • Proxy mở rộng hành vi trước / sau khi gọi method của real object
  • Không cần sửa code của Host
  • Giữ cho trách nhiệm của mỗi class rõ ràng (Single Responsibility Principle)

Improved Proxy.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package src.JdkProxy.StaticProxy;

// Agent
public class Proxy {

private Host host;

public Proxy() {}

public Proxy(Host host) {
this.host = host;
}

public void rent() {
host.rent();
contract();
fare();
}

// Show the house
public void seeHouse() {
System.out.println("Agent takes you to view the house");
}

// Collect agency fee
public void fare() {
System.out.println("Collecting agency fee");
}

// Sign the rental contract
public void contract() {
System.out.println("Signing the rental contract");
}
}

!image.png

Console Output (Translated):

1
2
3
The landlord wants to rent out the house!
Signing the rental contract
Collecting agency fee

👉 Output này cho thấy proxy không chỉ gọi method của real object (Host), mà còn thực hiện thêm business logic như:

  • ký hợp đồng thuê nhà (signing the rental contract)
  • thu phí môi giới (collecting agency fee)

Advantages (Ưu điểm):

  • Proxy giúp real role (real class) trở nên tập trung và “pure” hơn, không còn phải xử lý các common concerns.
  • Common business logic được đặt trong proxy, từ đó đạt được separation of concerns.
  • Khi cần mở rộng common business logic, việc chỉnh sửa sẽ tập trung, dễ quản lý và dễ bảo trì hơn.

Disadvantages (Nhược điểm):

  • Một real class sẽ tương ứng với một proxy class, dẫn đến:
    • số lượng class tăng gấp đôi
    • code nhiều hơn
    • giảm development efficiency

👉 Chính vì chúng ta muốn giữ ưu điểm của static proxy nhưng tránh nhược điểm của nó, nên Dynamic Proxy ra đời.


2.2. Hiểu sâu hơn về Static Proxy

Hãy xét một ví dụ rất phổ biến trong thực tế:

👉 CRUD operations (Create, Read, Update, Delete).

  • UserService.java - Interface
1
2
3
4
5
6
7
8
9
package src.JdkProxy.MoreStaticProxy;

// Deep understanding of static proxy
public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}

Interface này định nghĩa 4 abstract methods, đại diện cho các basic user operations trong hệ thống.

  • add() → Create
  • delete() → Delete
  • update() → Update
  • query() → Read

Đây là một ví dụ rất điển hình để thấy rõ:

  • Khi số lượng method tăng
  • Static proxy sẽ phình to rất nhanh
  • Và việc maintain proxy trở nên kém hiệu quả

  • UserServiceImpl.java - The Real Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package src.JdkProxy.MoreStaticProxy;

public class UserServiceImpl implements UserService {

@Override
public void add() {
System.out.println("Added a user");
}

@Override
public void delete() {
System.out.println("Deleted a user");
}

@Override
public void update() {
System.out.println("Updated a user");
}

@Override
public void query() {
System.out.println("Queried a user");
}
}
  • Đây là real object, class trực tiếp thực hiện các CRUD actions.

New Requirement

Bây giờ, bạn muốn thêm logging functionality.

Vậy nên implement như thế nào?

Approach 1

👉 Thêm code logging trực tiếp vào implementation class

  • Cách này rườm rà
  • Làm bẩn business logic
  • Vi phạm separation of concerns

Approach 2

👉 Sử dụng proxy

  • Đây là cách tốt nhất để thêm functionality
  • Không cần sửa code gốc
  • Giữ cho original business logic được clean

UserServiceProxy.java: The Static Proxy with Logging

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package src.JdkProxy.MoreStaticProxy;

// Proxy
public class UserServiceProxy implements UserService {
private UserServiceImpl userService;

public void setUserService(UserServiceImpl userService) {
this.userService = userService;
}

public void add() {
log("add");
userService.add();
}

public void delete() {
log("delete");
userService.delete();
}

public void update() {
log("update");
userService.update();
}

public void query() {
log("query");
userService.query();
}

// Logging method
public void log(String msg){
System.out.println("[Debug] Used method: " + msg);
}
}

Class này là static proxy, implement interface UserService.

  • Proxy giữ reference tới UserServiceImpl (real object)
  • Trước khi gọi mỗi business method, proxy sẽ gọi method log()
  • Sau đó mới delegate lời gọi sang real object

Client.java: The Launcher

1
2
3
4
5
6
7
8
9
10
11
package src.JdkProxy.MoreStaticProxy;

public class Client {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();

UserServiceProxy proxy = new UserServiceProxy();
proxy.setUserService(userService);
proxy.add();
}
}

Đây là launcher (entry point) của chương trình.

  • Client tạo real object UserServiceImpl
  • Client tạo proxy object
  • Client inject real object vào proxy
  • Client chỉ làm việc với proxy, không gọi trực tiếp real object

Result

👉 Không cần chỉnh sửa UserServiceImpl, bạn vẫn:

  • Thêm được logging functionality
  • Áp dụng logging cho các business methods

Cách làm này:

  • Giữ cho original code clean
  • Tuân thủ Open-Closed Principle
    (open for extension, closed for modification)
  • Centralize các cross-cutting concerns như logging

Nhưng vấn đề vẫn còn…

  • Mỗi interface mới → cần một proxy mới
  • Mỗi method mới → phải viết lại logic proxy
  • Code lặp lại nhiều, khó scale

👉 Đây chính là lý do Dynamic Proxy ra đời.

!image.png

Console Output (Translated):

1
2
[Debug] Used method: add
Added a user

👉 Output này cho thấy:

  • proxy đã log method add
  • Sau đó real object thực hiện business logic tương ứng