Giới Thiệu Về Proxy Pattern Trong Java
Proxy Pattern là một trong những design pattern quan trọng nhất trong lập trình hướng đối tượng, đặc biệt khi làm việc với các hệ thống phần mềm doanh nghiệp cần tái cấu trúc. Pattern này được chia thành hai loại chính: Static Proxy (proxy tĩnh) và Dynamic Proxy (proxy động).
Bài Toán Thực Tế Từ Tiệm Bánh
Hãy tưởng tượng bạn sở hữu một tiệm bánh với nhiều loại máy làm bánh khác nhau. Có máy làm bánh kem, máy làm bánh mì, và mỗi loại lại có nhiều biến thể. Chúng ta sẽ mô hình hóa kịch bản này bằng Java:
// Giao diện máy làm bánh kem
public interface CakeMaker {
void createCake();
}
// Máy làm bánh dâu tây
class StrawberryCakeMaker implements CakeMaker {
@Override
public void createCake() {
System.out.println("Creating a strawberry cake...");
}
}
// Máy làm bánh vani
public class VanillaCakeMaker implements CakeMaker {
@Override
public void createCake() {
System.out.println("Creating a vanilla cake...");
}
}
// Giao diện máy làm bánh mì
public interface BreadMaker {
void createBread();
}
// Máy làm bánh mì trà xanh
public class GreenTeaBreadMaker implements BreadMaker {
@Override
public void createBread() {
System.out.println("Creating green tea bread...");
}
}
// Máy làm bánh mì bơ
public class ButterBreadMaker implements BreadMaker {
@Override
public void createBread() {
System.out.println("Creating butter bread...");
}
}
// Tiệm bánh
public class Bakery {
public static void main(String[] args) {
new StrawberryCakeMaker().createCake();
new VanillaCakeMaker().createCake();
new GreenTeaBreadMaker().createBread();
new ButterBreadMaker().createBread();
}
}
Cấu trúc trên mô tả hai giao diện: CakeMaker và BreadMaker, với các implementation tương ứng. Đây là nền tảng để hiểu về proxy pattern.
Vấn Đề Khi Khách Hàng Yêu Cầu Đặc Biệt
Một ngày nọ, có khách hàng muốn đặt bánh dâu tây nhưng yêu cầu thêm một lớp hạnh nhân bên trên. Giải pháp đầu tiên là sửa đổi máy làm bánh dâu tây để tích hợp thêm chức năng thêm hạnh nhân:
// Máy làm bánh dâu tây với tính năng thêm hạnh nhân
class StrawberryCakeMaker implements CakeMaker {
@Override
public void createCake() {
System.out.println("Creating a strawberry cake...");
System.out.println("Adding almonds on top...");
}
}
Cách tiếp cận này có vấn đề: vi phạm nguyên tắc "Open/Closed Principle" - đóng cho việc sửa đổi, mở cho việc mở rộng. Thay vì sửa code cũ, chúng ta nên tạo một lớp proxy để xử lý yêu cầu đặc biệt này.
Giải Pháp: Static Proxy
Chúng ta sẽ tạo một lớp proxy đứng giữa client và đối tượng thực. Proxy này sẽ thực hiện thêm hạnh nhân sau khi bánh được làm xong:
// Proxy thêm hạnh nhân cho bánh kem
public class AlmondCakeProxy implements CakeMaker {
private CakeMaker cakeMaker;
public AlmondCakeProxy(CakeMaker cakeMaker) {
this.cakeMaker = cakeMaker;
}
@Override
public void createCake() {
cakeMaker.createCake();
System.out.println("Adding almonds on top...");
}
}
// Tiệm bánh
public class Bakery {
public static void main(String[] args) {
// Thêm hạnh nhân vào bánh dâu tây
CakeMaker strawberryCake = new StrawberryCakeMaker();
AlmondCakeProxy almondStrawberry = new AlmondCakeProxy(strawberryCake);
almondStrawberry.createCake();
// Thêm hạnh nhân vào bánh vani
almondStrawberry = new AlmondCakeProxy(new VanillaCakeMaker());
almondStrawberry.createCake();
}
}
Với cách này, lớp AlmondCakeProxy giữ vai trò trung gian, thực hiện hành động bổ sung (thêm hạnh nhân) trước khi chuyển tiếp yêu cầu đến đối tượng thực. Đây chính là Static Proxy.
Tuy nhiên, nếu khách hàng cũng muốn thêm hạnh nhân vào bánh mì, chúng ta cần tạo một proxy khác:
// Proxy thêm hạnh nhân cho bánh mì
public class AlmondBreadProxy implements BreadMaker {
private BreadMaker breadMaker;
public AlmondBreadProxy(BreadMaker breadMaker) {
this.breadMaker = breadMaker;
}
@Override
public void createBread() {
breadMaker.createBread();
System.out.println("Adding almonds on top...");
}
}
// Tiệm bánh
public class Bakery {
public static void main(String[] args) {
// Thêm hạnh nhân vào bánh mì trà xanh
BreadMaker greenTeaBread = new GreenTeaBreadMaker();
AlmondBreadProxy almondGreenTea = new AlmondBreadProxy(greenTeaBread);
almondGreenTea.createBread();
// Thêm hạnh nhân vào bánh mì bơ
almondGreenTea = new AlmondBreadProxy(new ButterBreadMaker());
almondGreenTea.createBread();
}
}
Kết quả:
Creating green tea bread...
Adding almonds on top...
Creating butter bread...
Adding almonds on top...
Vấn đề của Static Proxy: nếu có 100 loại sản phẩm khác nhau (bánh quy, sữa chua,...), chúng ta cần viết 100 lớp proxy tương ứng. Đây là lý do Dynamic Proxy ra đời.
Dynamic Proxy Trong Java
Java cung cấp giao diện InvocationHandler để tạo proxy động. Điểm khác biệt chính:
- Static Proxy: Chỉ hoạt động với một loại sản phẩm cụ thể
- Dynamic Proxy: Có thể hoạt động với mọi loại sản phẩm miễn là chúng implement interface
Triển khai Dynamic Proxy:
// Handler xử lý việc thêm hạnh nhân động
public class AlmondInvocationHandler implements InvocationHandler {
private Object target;
public AlmondInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Gọi phương thức thực tế của đối tượng
Object result = method.invoke(target, args);
// Thêm hạnh nhân sau khi tạo sản phẩm
System.out.println("Adding almonds on top...");
return result;
}
}
Sử dụng Dynamic Proxy trong tiệm bánh:
public class Bakery {
public static void main(String[] args) {
// Thêm hạnh nhân vào bánh kem
StrawberryCakeMaker strawberryMaker = new StrawberryCakeMaker();
AlmondInvocationHandler almondHandler = new AlmondInvocationHandler(strawberryMaker);
CakeMaker proxyCake = (CakeMaker) Proxy.newProxyInstance(
strawberryMaker.getClass().getClassLoader(),
strawberryMaker.getClass().getInterfaces(),
almondHandler
);
proxyCake.createCake();
// Thêm hạnh nhân vào bánh mì
GreenTeaBreadMaker greenTeaMaker = new GreenTeaBreadMaker();
almondHandler = new AlmondInvocationHandler(greenTeaMaker);
BreadMaker proxyBread = (BreadMaker) Proxy.newProxyInstance(
greenTeaMaker.getClass().getClassLoader(),
greenTeaMaker.getClass().getInterfaces(),
almondHandler
);
proxyBread.createBread();
}
}
Kết quả:
Creating a strawberry cake...
Adding almonds on top...
Creating green tea bread...
Adding almonds on top...
Chỉ với một AlmondInvocationHandler, chúng ta có thể thêm hạnh nhân vào bất kỳ loại sản phẩm nào miễn là nó implement interface tương ứng.
Cách Triển Khai Dynamic Proxy
Để triển khai Dynamic Proxy, cần thực hiện hai bước:
Bước 1: Tạo lớp implement InvocationHandler
public class AlmondInvocationHandler implements InvocationHandler {
private Object target;
public AlmondInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(target, args);
System.out.println("Adding almonds on top...");
return result;
}
}
Bước 2: Sử dụng Proxy.newProxyInstance() để tạo proxy
public class Bakery {
public static void main(String[] args) {
StrawberryCakeMaker strawberryMaker = new StrawberryCakeMaker();
AlmondInvocationHandler handler = new AlmondInvocationHandler(strawberryMaker);
CakeMaker proxy = (CakeMaker) Proxy.newProxyInstance(
strawberryMaker.getClass().getClassLoader(),
strawberryMaker.getClass().getInterfaces(),
handler
);
proxy.createCake();
}
}
So Sánh Java Dynamic Proxy Và CGLib
Dynamic Proxy trong Java chỉ hoạt động với các class có interface. CGLib (Code Generation Library) là một giải pháp thay thế, sử dụng kế thừa để tạo proxy cho cả các class không có interface.
Triển khai với CGLib:
// Interceptor cho CGLib
public class AlmondInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
proxy.invokeSuper(obj, args);
System.out.println("Adding almonds on top...");
return obj;
}
}
// Sử dụng CGLib
public class Bakery {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(StrawberryCakeMaker.class);
enhancer.setCallback(new AlmondInterceptor());
StrawberryCakeMaker proxyCake =
(StrawberryCakeMaker) enhancer.create();
proxyCake.createCake();
}
}
Kết quả:
Creating a strawberry cake...
Adding almonds on top...
So sánh:
- Java Dynamic Proxy: Yêu cầu class phải implement interface
- CGLib: Không yêu cầu interface, sử dụng kế thừa
Nguyên Lý Hoạt Động Của Java Dynamic Proxy
Khi gọi Proxy.newProxyInstance(), Java thực hiện:
- Tạo byte code cho class proxy implement tất cả interface được chỉ định
- Load class proxy bằng ClassLoader được cung cấp
- Khởi tạo instance với constructor nhận
InvocationHandler - Trả về instance có thể cast về interface mong muốn
Quá trình này sử dụng ProxyGenerator.generateProxyClass() để sinh ra class proxy tại runtime.
Đối với CGLib, thư viện sử dụng ASM để thao tác với bytecode và tạo class mới kế thừa từ class cần proxy.
Ứng Dụng Thực Tế
Dynamic Proxy được sử dụng rộng rãi trong nhiều framework:
- Spring AOP: Triển khai Aspect-Oriented Programming cho logging, security, transaction management. Mặc định dùng Java Dynamic Proxy, chuyển sang CGLib khi class không có interface.
- Hibernate: Sử dụng proxy để implement lazy loading - chỉ load dữ liệu khi thực sự cần thiết.
- Struts: Interceptor trong Struts cũng được triển khai bằng dynamic proxy.
Kết Luận
Bài viết đã trình bày chi tiết về Static Proxy và Dynamic Proxy trong Java. Static Proxy phù hợp với các trường hợp đơn giản, cần proxy cho một interface cụ thể. Dynamic Proxy với InvocationHandler hoặc CGLib mang lại tính linh hoạt cao hơn, cho phép xử lý nhiều loại object với cùng một logic.
Việc nắm vững Dynamic Proxy giúp hiểu rõ hơn cách các framework như Spring, Hibernate hoạt động bên trong, từ đó viết code tối ưu và dễ bảo trì hơn.